diff --git a/README.md b/README.md
index fc5380287..d89a1f0e3 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 120.0.6099.28 | ✅ | ✅ | ✅ |
+| Chromium 121.0.6167.57 | ✅ | ✅ | ✅ |
| WebKit 17.4 | ✅ | ✅ | ✅ |
-| Firefox 119.0 | ✅ | ✅ | ✅ |
+| Firefox 121.0 | ✅ | ✅ | ✅ |
## Documentation
diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py
index e7e6f19a8..c05b427f2 100644
--- a/playwright/_impl/_browser_context.py
+++ b/playwright/_impl/_browser_context.py
@@ -196,6 +196,7 @@ def __init__(
self.Events.Close, lambda context: self._closed_future.set_result(True)
)
self._close_reason: Optional[str] = None
+ self._har_routers: List[HarRouter] = []
self._set_event_to_subscription_mapping(
{
BrowserContext.Events.Console: "console",
@@ -219,10 +220,16 @@ def _on_page(self, page: Page) -> None:
async def _on_route(self, route: Route) -> None:
route._context = self
+ page = route.request._safe_page()
route_handlers = self._routes.copy()
for route_handler in route_handlers:
+ # If the page or the context was closed we stall all requests right away.
+ if (page and page._close_was_called) or self._close_was_called:
+ return
if not route_handler.matches(route.request.url):
continue
+ if route_handler not in self._routes:
+ continue
if route_handler.will_expire:
self._routes.remove(route_handler)
try:
@@ -236,7 +243,12 @@ async def _on_route(self, route: Route) -> None:
)
if handled:
return
- await route._internal_continue(is_internal=True)
+ try:
+ # If the page is closed or unrouteAll() was called without waiting and interception disabled,
+ # the method will throw an error - silence it.
+ await route._internal_continue(is_internal=True)
+ except Exception:
+ pass
def _on_binding(self, binding_call: BindingCall) -> None:
func = self._bindings.get(binding_call._initializer["name"])
@@ -361,13 +373,37 @@ async def route(
async def unroute(
self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None
) -> None:
- self._routes = list(
- filter(
- lambda r: r.matcher.match != url or (handler and r.handler != handler),
- self._routes,
- )
- )
+ removed = []
+ remaining = []
+ for route in self._routes:
+ if route.matcher.match != url or (handler and route.handler != handler):
+ remaining.append(route)
+ else:
+ removed.append(route)
+ await self._unroute_internal(removed, remaining, "default")
+
+ async def _unroute_internal(
+ self,
+ removed: List[RouteHandler],
+ remaining: List[RouteHandler],
+ behavior: Literal["default", "ignoreErrors", "wait"] = None,
+ ) -> None:
+ self._routes = remaining
await self._update_interception_patterns()
+ if behavior is None or behavior == "default":
+ return
+ await asyncio.gather(*map(lambda router: router.stop(behavior), removed)) # type: ignore
+
+ def _dispose_har_routers(self) -> None:
+ for router in self._har_routers:
+ router.dispose()
+ self._har_routers = []
+
+ async def unroute_all(
+ self, behavior: Literal["default", "ignoreErrors", "wait"] = None
+ ) -> None:
+ await self._unroute_internal(self._routes, [], behavior)
+ self._dispose_har_routers()
async def _record_into_har(
self,
@@ -419,6 +455,7 @@ async def route_from_har(
not_found_action=notFound or "abort",
url_matcher=url,
)
+ self._har_routers.append(router)
await router.add_context_route(self)
async def _update_interception_patterns(self) -> None:
@@ -450,6 +487,7 @@ def _on_close(self) -> None:
if self._browser:
self._browser._contexts.remove(self)
+ self._dispose_har_routers()
self.emit(BrowserContext.Events.Close, self)
async def close(self, reason: str = None) -> None:
diff --git a/playwright/_impl/_element_handle.py b/playwright/_impl/_element_handle.py
index 6c585bb0d..558cf3ac9 100644
--- a/playwright/_impl/_element_handle.py
+++ b/playwright/_impl/_element_handle.py
@@ -298,6 +298,7 @@ async def screenshot(
scale: Literal["css", "device"] = None,
mask: Sequence["Locator"] = None,
maskColor: str = None,
+ style: str = None,
) -> bytes:
params = locals_to_params(locals())
if "path" in params:
diff --git a/playwright/_impl/_har_router.py b/playwright/_impl/_har_router.py
index a96ba70bf..3e56fd019 100644
--- a/playwright/_impl/_har_router.py
+++ b/playwright/_impl/_har_router.py
@@ -102,16 +102,14 @@ async def add_context_route(self, context: "BrowserContext") -> None:
url=self._options_url_match or "**/*",
handler=lambda route, _: asyncio.create_task(self._handle(route)),
)
- context.once("close", lambda _: self._dispose())
async def add_page_route(self, page: "Page") -> None:
await page.route(
url=self._options_url_match or "**/*",
handler=lambda route, _: asyncio.create_task(self._handle(route)),
)
- page.once("close", lambda _: self._dispose())
- def _dispose(self) -> None:
+ def dispose(self) -> None:
asyncio.create_task(
self._local_utils._channel.send("harClose", {"harId": self._har_id})
)
diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py
index b68ad6f0b..615cd5264 100644
--- a/playwright/_impl/_helper.py
+++ b/playwright/_impl/_helper.py
@@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
-import inspect
import math
import os
import re
@@ -25,11 +24,11 @@
TYPE_CHECKING,
Any,
Callable,
- Coroutine,
Dict,
List,
Optional,
Pattern,
+ Set,
TypeVar,
Union,
cast,
@@ -257,6 +256,15 @@ def monotonic_time() -> int:
return math.floor(time.monotonic() * 1000)
+class RouteHandlerInvocation:
+ complete: "asyncio.Future"
+ route: "Route"
+
+ def __init__(self, complete: "asyncio.Future", route: "Route") -> None:
+ self.complete = complete
+ self.route = route
+
+
class RouteHandler:
def __init__(
self,
@@ -270,32 +278,57 @@ def __init__(
self._times = times if times else math.inf
self._handled_count = 0
self._is_sync = is_sync
+ self._ignore_exception = False
+ self._active_invocations: Set[RouteHandlerInvocation] = set()
def matches(self, request_url: str) -> bool:
return self.matcher.matches(request_url)
async def handle(self, route: "Route") -> bool:
+ handler_invocation = RouteHandlerInvocation(
+ asyncio.get_running_loop().create_future(), route
+ )
+ self._active_invocations.add(handler_invocation)
+ try:
+ return await self._handle_internal(route)
+ except Exception as e:
+ # If the handler was stopped (without waiting for completion), we ignore all exceptions.
+ if self._ignore_exception:
+ return False
+ raise e
+ finally:
+ handler_invocation.complete.set_result(None)
+ self._active_invocations.remove(handler_invocation)
+
+ async def _handle_internal(self, route: "Route") -> bool:
handled_future = route._start_handling()
- handler_task = []
-
- def impl() -> None:
- self._handled_count += 1
- result = cast(
- Callable[["Route", "Request"], Union[Coroutine, Any]], self.handler
- )(route, route.request)
- if inspect.iscoroutine(result):
- handler_task.append(asyncio.create_task(result))
-
- # As with event handlers, each route handler is a potentially blocking context
- # so it needs a fiber.
+
+ self._handled_count += 1
if self._is_sync:
- g = greenlet(impl)
+ # As with event handlers, each route handler is a potentially blocking context
+ # so it needs a fiber.
+ g = greenlet(lambda: self.handler(route, route.request)) # type: ignore
g.switch()
else:
- impl()
-
- [handled, *_] = await asyncio.gather(handled_future, *handler_task)
- return handled
+ coro_or_future = self.handler(route, route.request) # type: ignore
+ if coro_or_future:
+ # separate task so that we get a proper stack trace for exceptions / tracing api_name extraction
+ await asyncio.ensure_future(coro_or_future)
+ return await handled_future
+
+ async def stop(self, behavior: Literal["ignoreErrors", "wait"]) -> None:
+ # When a handler is manually unrouted or its page/context is closed we either
+ # - wait for the current handler invocations to finish
+ # - or do not wait, if the user opted out of it, but swallow all exceptions
+ # that happen after the unroute/close.
+ if behavior == "ignoreErrors":
+ self._ignore_exception = True
+ else:
+ tasks = []
+ for activation in self._active_invocations:
+ if not activation.route._did_throw:
+ tasks.append(activation.complete)
+ await asyncio.gather(*tasks)
@property
def will_expire(self) -> bool:
diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py
index 55955d089..a9cc92aba 100644
--- a/playwright/_impl/_locator.py
+++ b/playwright/_impl/_locator.py
@@ -523,6 +523,7 @@ async def screenshot(
scale: Literal["css", "device"] = None,
mask: Sequence["Locator"] = None,
maskColor: str = None,
+ style: str = None,
) -> bytes:
params = locals_to_params(locals())
return await self._with_element(
diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py
index 102767cf6..03aa53588 100644
--- a/playwright/_impl/_network.py
+++ b/playwright/_impl/_network.py
@@ -267,6 +267,9 @@ def _target_closed_future(self) -> asyncio.Future:
return asyncio.Future()
return page._closed_or_crashed_future
+ def _safe_page(self) -> "Optional[Page]":
+ return cast("Frame", from_channel(self._initializer["frame"]))._page
+
class Route(ChannelOwner):
def __init__(
@@ -275,6 +278,7 @@ def __init__(
super().__init__(parent, type, guid, initializer)
self._handling_future: Optional[asyncio.Future["bool"]] = None
self._context: "BrowserContext" = cast("BrowserContext", None)
+ self._did_throw = False
def _start_handling(self) -> "asyncio.Future[bool]":
self._handling_future = asyncio.Future()
@@ -298,17 +302,17 @@ def request(self) -> Request:
return from_channel(self._initializer["request"])
async def abort(self, errorCode: str = None) -> None:
- self._check_not_handled()
- await self._race_with_page_close(
- self._channel.send(
- "abort",
- {
- "errorCode": errorCode,
- "requestUrl": self.request._initializer["url"],
- },
+ await self._handle_route(
+ lambda: self._race_with_page_close(
+ self._channel.send(
+ "abort",
+ {
+ "errorCode": errorCode,
+ "requestUrl": self.request._initializer["url"],
+ },
+ )
)
)
- self._report_handled(True)
async def fulfill(
self,
@@ -320,7 +324,22 @@ async def fulfill(
contentType: str = None,
response: "APIResponse" = None,
) -> None:
- self._check_not_handled()
+ await self._handle_route(
+ lambda: self._inner_fulfill(
+ status, headers, body, json, path, contentType, response
+ )
+ )
+
+ async def _inner_fulfill(
+ self,
+ status: int = None,
+ headers: Dict[str, str] = None,
+ body: Union[str, bytes] = None,
+ json: Any = None,
+ path: Union[str, Path] = None,
+ contentType: str = None,
+ response: "APIResponse" = None,
+ ) -> None:
params = locals_to_params(locals())
if json is not None:
@@ -375,7 +394,15 @@ async def fulfill(
params["requestUrl"] = self.request._initializer["url"]
await self._race_with_page_close(self._channel.send("fulfill", params))
- self._report_handled(True)
+
+ async def _handle_route(self, callback: Callable) -> None:
+ self._check_not_handled()
+ try:
+ await callback()
+ self._report_handled(True)
+ except Exception as e:
+ self._did_throw = True
+ raise e
async def fetch(
self,
@@ -418,10 +445,12 @@ async def continue_(
postData: Union[Any, str, bytes] = None,
) -> None:
overrides = cast(FallbackOverrideParameters, locals_to_params(locals()))
- self._check_not_handled()
- self.request._apply_fallback_overrides(overrides)
- await self._internal_continue()
- self._report_handled(True)
+
+ async def _inner() -> None:
+ self.request._apply_fallback_overrides(overrides)
+ await self._internal_continue()
+
+ return await self._handle_route(_inner)
def _internal_continue(
self, is_internal: bool = False
@@ -458,11 +487,11 @@ async def continue_route() -> None:
return continue_route()
async def _redirected_navigation_request(self, url: str) -> None:
- self._check_not_handled()
- await self._race_with_page_close(
- self._channel.send("redirectNavigationRequest", {"url": url})
+ await self._handle_route(
+ lambda: self._race_with_page_close(
+ self._channel.send("redirectNavigationRequest", {"url": url})
+ )
)
- self._report_handled(True)
async def _race_with_page_close(self, future: Coroutine) -> None:
fut = asyncio.create_task(future)
diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py
index cfa571f74..ac6a55002 100644
--- a/playwright/_impl/_page.py
+++ b/playwright/_impl/_page.py
@@ -152,6 +152,8 @@ def __init__(
self._video: Optional[Video] = None
self._opener = cast("Page", from_nullable_channel(initializer.get("opener")))
self._close_reason: Optional[str] = None
+ self._close_was_called = False
+ self._har_routers: List[HarRouter] = []
self._channel.on(
"bindingCall",
@@ -238,8 +240,13 @@ async def _on_route(self, route: Route) -> None:
route._context = self.context
route_handlers = self._routes.copy()
for route_handler in route_handlers:
+ # If the page was closed we stall all requests right away.
+ if self._close_was_called or self.context._close_was_called:
+ return
if not route_handler.matches(route.request.url):
continue
+ if route_handler not in self._routes:
+ continue
if route_handler.will_expire:
self._routes.remove(route_handler)
try:
@@ -272,6 +279,7 @@ def _on_close(self) -> None:
self._browser_context._pages.remove(self)
if self in self._browser_context._background_pages:
self._browser_context._background_pages.remove(self)
+ self._dispose_har_routers()
self.emit(Page.Events.Close, self)
def _on_crash(self) -> None:
@@ -585,13 +593,42 @@ async def route(
async def unroute(
self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None
) -> None:
- self._routes = list(
- filter(
- lambda r: r.matcher.match != url or (handler and r.handler != handler),
- self._routes,
+ removed = []
+ remaining = []
+ for route in self._routes:
+ if route.matcher.match != url or (handler and route.handler != handler):
+ remaining.append(route)
+ else:
+ removed.append(route)
+ await self._unroute_internal(removed, remaining, "default")
+
+ async def _unroute_internal(
+ self,
+ removed: List[RouteHandler],
+ remaining: List[RouteHandler],
+ behavior: Literal["default", "ignoreErrors", "wait"] = None,
+ ) -> None:
+ self._routes = remaining
+ await self._update_interception_patterns()
+ if behavior is None or behavior == "default":
+ return
+ await asyncio.gather(
+ *map(
+ lambda route: route.stop(behavior), # type: ignore
+ removed,
)
)
- await self._update_interception_patterns()
+
+ def _dispose_har_routers(self) -> None:
+ for router in self._har_routers:
+ router.dispose()
+ self._har_routers = []
+
+ async def unroute_all(
+ self, behavior: Literal["default", "ignoreErrors", "wait"] = None
+ ) -> None:
+ await self._unroute_internal(self._routes, [], behavior)
+ self._dispose_har_routers()
async def route_from_har(
self,
@@ -617,6 +654,7 @@ async def route_from_har(
not_found_action=notFound or "abort",
url_matcher=url,
)
+ self._har_routers.append(router)
await router.add_page_route(self)
async def _update_interception_patterns(self) -> None:
@@ -639,6 +677,7 @@ async def screenshot(
scale: Literal["css", "device"] = None,
mask: Sequence["Locator"] = None,
maskColor: str = None,
+ style: str = None,
) -> bytes:
params = locals_to_params(locals())
if "path" in params:
@@ -667,6 +706,7 @@ async def title(self) -> str:
async def close(self, runBeforeUnload: bool = None, reason: str = None) -> None:
self._close_reason = reason
+ self._close_was_called = True
try:
await self._channel.send("close", locals_to_params(locals()))
if self._owned_context:
diff --git a/playwright/_impl/_set_input_files_helpers.py b/playwright/_impl/_set_input_files_helpers.py
index a5db6c1da..793144313 100644
--- a/playwright/_impl/_set_input_files_helpers.py
+++ b/playwright/_impl/_set_input_files_helpers.py
@@ -62,12 +62,14 @@ async def convert_input_files(
assert isinstance(item, (str, Path))
last_modified_ms = int(os.path.getmtime(item) * 1000)
stream: WritableStream = from_channel(
- await context._channel.send(
- "createTempFile",
- {
- "name": os.path.basename(item),
- "lastModifiedMs": last_modified_ms,
- },
+ await context._connection.wrap_api_call(
+ lambda: context._channel.send(
+ "createTempFile",
+ {
+ "name": os.path.basename(cast(str, item)),
+ "lastModifiedMs": last_modified_ms,
+ },
+ )
)
)
await stream.copy(item)
diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py
index d8276a125..59a92a296 100644
--- a/playwright/async_api/_generated.py
+++ b/playwright/async_api/_generated.py
@@ -2769,7 +2769,8 @@ async def screenshot(
caret: typing.Optional[Literal["hide", "initial"]] = None,
scale: typing.Optional[Literal["css", "device"]] = None,
mask: typing.Optional[typing.Sequence["Locator"]] = None,
- mask_color: typing.Optional[str] = None
+ mask_color: typing.Optional[str] = None,
+ style: typing.Optional[str] = None
) -> bytes:
"""ElementHandle.screenshot
@@ -2820,6 +2821,10 @@ async def screenshot(
mask_color : Union[str, None]
Specify the color of the overlay box for masked elements, in
[CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`.
+ style : Union[str, None]
+ Text of the stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make
+ elements invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces
+ the Shadow DOM and applies to the inner frames.
Returns
-------
@@ -2838,6 +2843,7 @@ async def screenshot(
scale=scale,
mask=mapping.to_impl(mask),
maskColor=mask_color,
+ style=style,
)
)
@@ -2997,9 +3003,8 @@ async def wait_for_element_state(
Depending on the `state` parameter, this method waits for one of the [actionability](https://playwright.dev/python/docs/actionability) checks to
pass. This method throws when the element is detached while waiting, unless waiting for the `\"hidden\"` state.
- `\"visible\"` Wait until the element is [visible](https://playwright.dev/python/docs/actionability#visible).
- - `\"hidden\"` Wait until the element is [not visible](https://playwright.dev/python/docs/actionability#visible) or
- [not attached](https://playwright.dev/python/docs/actionability#attached). Note that waiting for hidden does not throw when the element
- detaches.
+ - `\"hidden\"` Wait until the element is [not visible](https://playwright.dev/python/docs/actionability#visible) or not attached. Note that
+ waiting for hidden does not throw when the element detaches.
- `\"stable\"` Wait until the element is both [visible](https://playwright.dev/python/docs/actionability#visible) and
[stable](https://playwright.dev/python/docs/actionability#stable).
- `\"enabled\"` Wait until the element is [enabled](https://playwright.dev/python/docs/actionability#enabled).
@@ -4709,8 +4714,13 @@ def locator(
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
When passed a [string], matching is case-insensitive and searches for a substring.
has : Union[Locator, None]
- Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
- one. For example, `article` that has `text=Playwright` matches `Playwright
`.
+ Narrows down the results of the method to those which contain elements matching this relative locator. For example,
+ `article` that has `text=Playwright` matches `Playwright
`.
+
+ Inner locator **must be relative** to the outer locator and is queried starting with the outer locator match, not
+ the document root. For example, you can find `content` that has `div` in
+ `Playwright
`. However, looking for `content` that has `article
+ div` will fail, because the inner locator must be relative and should not use any elements outside the `content`.
Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s.
has_not : Union[Locator, None]
@@ -6245,8 +6255,13 @@ def locator(
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
When passed a [string], matching is case-insensitive and searches for a substring.
has : Union[Locator, None]
- Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
- one. For example, `article` that has `text=Playwright` matches `Playwright
`.
+ Narrows down the results of the method to those which contain elements matching this relative locator. For example,
+ `article` that has `text=Playwright` matches `Playwright
`.
+
+ Inner locator **must be relative** to the outer locator and is queried starting with the outer locator match, not
+ the document root. For example, you can find `content` that has `div` in
+ `Playwright
`. However, looking for `content` that has `article
+ div` will fail, because the inner locator must be relative and should not use any elements outside the `content`.
Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s.
has_not : Union[Locator, None]
@@ -9856,6 +9871,30 @@ async def unroute(
)
)
+ async def unroute_all(
+ self,
+ *,
+ behavior: typing.Optional[Literal["default", "ignoreErrors", "wait"]] = None
+ ) -> None:
+ """Page.unroute_all
+
+ Removes all routes created with `page.route()` and `page.route_from_har()`.
+
+ Parameters
+ ----------
+ behavior : Union["default", "ignoreErrors", "wait", None]
+ Specifies wether to wait for already running handlers and what to do if they throw errors:
+ - `'default'` - do not wait for current handler calls (if any) to finish, if unrouted handler throws, it may
+ result in unhandled error
+ - `'wait'` - wait for current handler calls (if any) to finish
+ - `'ignoreErrors'` - do not wait for current handler calls (if any) to finish, all errors thrown by the handlers
+ after unrouting are silently caught
+ """
+
+ return mapping.from_maybe_impl(
+ await self._impl_obj.unroute_all(behavior=behavior)
+ )
+
async def route_from_har(
self,
har: typing.Union[pathlib.Path, str],
@@ -9924,7 +9963,8 @@ async def screenshot(
caret: typing.Optional[Literal["hide", "initial"]] = None,
scale: typing.Optional[Literal["css", "device"]] = None,
mask: typing.Optional[typing.Sequence["Locator"]] = None,
- mask_color: typing.Optional[str] = None
+ mask_color: typing.Optional[str] = None,
+ style: typing.Optional[str] = None
) -> bytes:
"""Page.screenshot
@@ -9973,6 +10013,10 @@ async def screenshot(
mask_color : Union[str, None]
Specify the color of the overlay box for masked elements, in
[CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`.
+ style : Union[str, None]
+ Text of the stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make
+ elements invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces
+ the Shadow DOM and applies to the inner frames.
Returns
-------
@@ -9993,6 +10037,7 @@ async def screenshot(
scale=scale,
mask=mapping.to_impl(mask),
maskColor=mask_color,
+ style=style,
)
)
@@ -10362,8 +10407,13 @@ def locator(
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
When passed a [string], matching is case-insensitive and searches for a substring.
has : Union[Locator, None]
- Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
- one. For example, `article` that has `text=Playwright` matches `Playwright
`.
+ Narrows down the results of the method to those which contain elements matching this relative locator. For example,
+ `article` that has `text=Playwright` matches `Playwright
`.
+
+ Inner locator **must be relative** to the outer locator and is queried starting with the outer locator match, not
+ the document root. For example, you can find `content` that has `div` in
+ `Playwright
`. However, looking for `content` that has `article
+ div` will fail, because the inner locator must be relative and should not use any elements outside the `content`.
Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s.
has_not : Union[Locator, None]
@@ -13640,6 +13690,30 @@ async def unroute(
)
)
+ async def unroute_all(
+ self,
+ *,
+ behavior: typing.Optional[Literal["default", "ignoreErrors", "wait"]] = None
+ ) -> None:
+ """BrowserContext.unroute_all
+
+ Removes all routes created with `browser_context.route()` and `browser_context.route_from_har()`.
+
+ Parameters
+ ----------
+ behavior : Union["default", "ignoreErrors", "wait", None]
+ Specifies wether to wait for already running handlers and what to do if they throw errors:
+ - `'default'` - do not wait for current handler calls (if any) to finish, if unrouted handler throws, it may
+ result in unhandled error
+ - `'wait'` - wait for current handler calls (if any) to finish
+ - `'ignoreErrors'` - do not wait for current handler calls (if any) to finish, all errors thrown by the handlers
+ after unrouting are silently caught
+ """
+
+ return mapping.from_maybe_impl(
+ await self._impl_obj.unroute_all(behavior=behavior)
+ )
+
async def route_from_har(
self,
har: typing.Union[pathlib.Path, str],
@@ -14690,8 +14764,10 @@ async def launch(
"msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using
[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.
+
Additional arguments to pass to the browser instance. The list of Chromium flags can be found
- [here](http://peter.sh/experiments/chromium-command-line-switches/).
+ [here](https://peter.sh/experiments/chromium-command-line-switches/).
ignore_default_args : Union[Sequence[str], bool, None]
If `true`, Playwright does not pass its own configurations args and only uses the ones from `args`. If an array is
given, then filters out the given default arguments. Dangerous option; use with care. Defaults to `false`.
@@ -14845,8 +14921,10 @@ async def launch_persistent_context(
resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium,
Firefox or WebKit, use at your own risk.
args : Union[Sequence[str], None]
+ **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
+
Additional arguments to pass to the browser instance. The list of Chromium flags can be found
- [here](http://peter.sh/experiments/chromium-command-line-switches/).
+ [here](https://peter.sh/experiments/chromium-command-line-switches/).
ignore_default_args : Union[Sequence[str], bool, None]
If `true`, Playwright does not pass its own configurations args and only uses the ones from `args`. If an array is
given, then filters out the given default arguments. Dangerous option; use with care. Defaults to `false`.
@@ -15323,14 +15401,14 @@ async def start(
**Usage**
```py
- await context.tracing.start(name=\"trace\", screenshots=True, snapshots=True)
+ await context.tracing.start(screenshots=True, snapshots=True)
page = await context.new_page()
await page.goto(\"https://playwright.dev\")
await context.tracing.stop(path = \"trace.zip\")
```
```py
- context.tracing.start(name=\"trace\", screenshots=True, snapshots=True)
+ context.tracing.start(screenshots=True, snapshots=True)
page = context.new_page()
page.goto(\"https://playwright.dev\")
context.tracing.stop(path = \"trace.zip\")
@@ -15339,8 +15417,9 @@ async def start(
Parameters
----------
name : Union[str, None]
- If specified, the trace is going to be saved into the file with the given name inside the `tracesDir` folder
- specified in `browser_type.launch()`.
+ If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the
+ `tracesDir` folder specified in `browser_type.launch()`. To specify the final trace zip file name, you need
+ to pass `path` option to `tracing.stop()` instead.
title : Union[str, None]
Trace name to be shown in the Trace Viewer.
snapshots : Union[bool, None]
@@ -15375,7 +15454,7 @@ async def start_chunk(
**Usage**
```py
- await context.tracing.start(name=\"trace\", screenshots=True, snapshots=True)
+ await context.tracing.start(screenshots=True, snapshots=True)
page = await context.new_page()
await page.goto(\"https://playwright.dev\")
@@ -15391,7 +15470,7 @@ async def start_chunk(
```
```py
- context.tracing.start(name=\"trace\", screenshots=True, snapshots=True)
+ context.tracing.start(screenshots=True, snapshots=True)
page = context.new_page()
page.goto(\"https://playwright.dev\")
@@ -15411,8 +15490,9 @@ async def start_chunk(
title : Union[str, None]
Trace name to be shown in the Trace Viewer.
name : Union[str, None]
- If specified, the trace is going to be saved into the file with the given name inside the `tracesDir` folder
- specified in `browser_type.launch()`.
+ If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the
+ `tracesDir` folder specified in `browser_type.launch()`. To specify the final trace zip file name, you need
+ to pass `path` option to `tracing.stop_chunk()` instead.
"""
return mapping.from_maybe_impl(
@@ -16144,8 +16224,13 @@ def locator(
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
When passed a [string], matching is case-insensitive and searches for a substring.
has : Union[Locator, None]
- Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
- one. For example, `article` that has `text=Playwright` matches `Playwright
`.
+ Narrows down the results of the method to those which contain elements matching this relative locator. For example,
+ `article` that has `text=Playwright` matches `Playwright
`.
+
+ Inner locator **must be relative** to the outer locator and is queried starting with the outer locator match, not
+ the document root. For example, you can find `content` that has `div` in
+ `Playwright
`. However, looking for `content` that has `article
+ div` will fail, because the inner locator must be relative and should not use any elements outside the `content`.
Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s.
has_not : Union[Locator, None]
@@ -16806,8 +16891,13 @@ def filter(
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
When passed a [string], matching is case-insensitive and searches for a substring.
has : Union[Locator, None]
- Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
- one. For example, `article` that has `text=Playwright` matches `Playwright
`.
+ Narrows down the results of the method to those which contain elements matching this relative locator. For example,
+ `article` that has `text=Playwright` matches `Playwright
`.
+
+ Inner locator **must be relative** to the outer locator and is queried starting with the outer locator match, not
+ the document root. For example, you can find `content` that has `div` in
+ `Playwright
`. However, looking for `content` that has `article
+ div` will fail, because the inner locator must be relative and should not use any elements outside the `content`.
Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s.
has_not : Union[Locator, None]
@@ -17510,7 +17600,8 @@ async def screenshot(
caret: typing.Optional[Literal["hide", "initial"]] = None,
scale: typing.Optional[Literal["css", "device"]] = None,
mask: typing.Optional[typing.Sequence["Locator"]] = None,
- mask_color: typing.Optional[str] = None
+ mask_color: typing.Optional[str] = None,
+ style: typing.Optional[str] = None
) -> bytes:
"""Locator.screenshot
@@ -17585,6 +17676,10 @@ async def screenshot(
mask_color : Union[str, None]
Specify the color of the overlay box for masked elements, in
[CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`.
+ style : Union[str, None]
+ Text of the stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make
+ elements invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces
+ the Shadow DOM and applies to the inner frames.
Returns
-------
@@ -17603,6 +17698,7 @@ async def screenshot(
scale=scale,
mask=mapping.to_impl(mask),
maskColor=mask_color,
+ style=style,
)
)
@@ -19293,8 +19389,8 @@ async def to_contain_text(
) -> None:
"""LocatorAssertions.to_contain_text
- Ensures the `Locator` points to an element that contains the given text. You can use regular expressions for the
- value as well.
+ Ensures the `Locator` points to an element that contains the given text. All nested elements will be considered
+ when computing the text content of the element. You can use regular expressions for the value as well.
**Details**
@@ -20039,8 +20135,8 @@ async def to_have_text(
) -> None:
"""LocatorAssertions.to_have_text
- Ensures the `Locator` points to an element with the given text. You can use regular expressions for the value as
- well.
+ Ensures the `Locator` points to an element with the given text. All nested elements will be considered when
+ computing the text content of the element. You can use regular expressions for the value as well.
**Details**
@@ -20188,7 +20284,8 @@ async def to_be_attached(
) -> None:
"""LocatorAssertions.to_be_attached
- Ensures that `Locator` points to an [attached](https://playwright.dev/python/docs/actionability#attached) DOM node.
+ Ensures that `Locator` points to an element that is
+ [connected](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) to a Document or a ShadowRoot.
**Usage**
@@ -20569,8 +20666,7 @@ async def to_be_visible(
) -> None:
"""LocatorAssertions.to_be_visible
- Ensures that `Locator` points to an [attached](https://playwright.dev/python/docs/actionability#attached) and
- [visible](https://playwright.dev/python/docs/actionability#visible) DOM node.
+ Ensures that `Locator` points to an attached and [visible](https://playwright.dev/python/docs/actionability#visible) DOM node.
To check that at least one element from the list is visible, use `locator.first()`.
diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py
index 09a308c2c..d64175f4f 100644
--- a/playwright/sync_api/_generated.py
+++ b/playwright/sync_api/_generated.py
@@ -2803,7 +2803,8 @@ def screenshot(
caret: typing.Optional[Literal["hide", "initial"]] = None,
scale: typing.Optional[Literal["css", "device"]] = None,
mask: typing.Optional[typing.Sequence["Locator"]] = None,
- mask_color: typing.Optional[str] = None
+ mask_color: typing.Optional[str] = None,
+ style: typing.Optional[str] = None
) -> bytes:
"""ElementHandle.screenshot
@@ -2854,6 +2855,10 @@ def screenshot(
mask_color : Union[str, None]
Specify the color of the overlay box for masked elements, in
[CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`.
+ style : Union[str, None]
+ Text of the stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make
+ elements invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces
+ the Shadow DOM and applies to the inner frames.
Returns
-------
@@ -2873,6 +2878,7 @@ def screenshot(
scale=scale,
mask=mapping.to_impl(mask),
maskColor=mask_color,
+ style=style,
)
)
)
@@ -3037,9 +3043,8 @@ def wait_for_element_state(
Depending on the `state` parameter, this method waits for one of the [actionability](https://playwright.dev/python/docs/actionability) checks to
pass. This method throws when the element is detached while waiting, unless waiting for the `\"hidden\"` state.
- `\"visible\"` Wait until the element is [visible](https://playwright.dev/python/docs/actionability#visible).
- - `\"hidden\"` Wait until the element is [not visible](https://playwright.dev/python/docs/actionability#visible) or
- [not attached](https://playwright.dev/python/docs/actionability#attached). Note that waiting for hidden does not throw when the element
- detaches.
+ - `\"hidden\"` Wait until the element is [not visible](https://playwright.dev/python/docs/actionability#visible) or not attached. Note that
+ waiting for hidden does not throw when the element detaches.
- `\"stable\"` Wait until the element is both [visible](https://playwright.dev/python/docs/actionability#visible) and
[stable](https://playwright.dev/python/docs/actionability#stable).
- `\"enabled\"` Wait until the element is [enabled](https://playwright.dev/python/docs/actionability#enabled).
@@ -4799,8 +4804,13 @@ def locator(
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
When passed a [string], matching is case-insensitive and searches for a substring.
has : Union[Locator, None]
- Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
- one. For example, `article` that has `text=Playwright` matches `Playwright
`.
+ Narrows down the results of the method to those which contain elements matching this relative locator. For example,
+ `article` that has `text=Playwright` matches `Playwright
`.
+
+ Inner locator **must be relative** to the outer locator and is queried starting with the outer locator match, not
+ the document root. For example, you can find `content` that has `div` in
+ `Playwright
`. However, looking for `content` that has `article
+ div` will fail, because the inner locator must be relative and should not use any elements outside the `content`.
Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s.
has_not : Union[Locator, None]
@@ -6365,8 +6375,13 @@ def locator(
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
When passed a [string], matching is case-insensitive and searches for a substring.
has : Union[Locator, None]
- Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
- one. For example, `article` that has `text=Playwright` matches `Playwright
`.
+ Narrows down the results of the method to those which contain elements matching this relative locator. For example,
+ `article` that has `text=Playwright` matches `Playwright
`.
+
+ Inner locator **must be relative** to the outer locator and is queried starting with the outer locator match, not
+ the document root. For example, you can find `content` that has `div` in
+ `Playwright
`. However, looking for `content` that has `article
+ div` will fail, because the inner locator must be relative and should not use any elements outside the `content`.
Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s.
has_not : Union[Locator, None]
@@ -9922,6 +9937,30 @@ def unroute(
)
)
+ def unroute_all(
+ self,
+ *,
+ behavior: typing.Optional[Literal["default", "ignoreErrors", "wait"]] = None
+ ) -> None:
+ """Page.unroute_all
+
+ Removes all routes created with `page.route()` and `page.route_from_har()`.
+
+ Parameters
+ ----------
+ behavior : Union["default", "ignoreErrors", "wait", None]
+ Specifies wether to wait for already running handlers and what to do if they throw errors:
+ - `'default'` - do not wait for current handler calls (if any) to finish, if unrouted handler throws, it may
+ result in unhandled error
+ - `'wait'` - wait for current handler calls (if any) to finish
+ - `'ignoreErrors'` - do not wait for current handler calls (if any) to finish, all errors thrown by the handlers
+ after unrouting are silently caught
+ """
+
+ return mapping.from_maybe_impl(
+ self._sync(self._impl_obj.unroute_all(behavior=behavior))
+ )
+
def route_from_har(
self,
har: typing.Union[pathlib.Path, str],
@@ -9992,7 +10031,8 @@ def screenshot(
caret: typing.Optional[Literal["hide", "initial"]] = None,
scale: typing.Optional[Literal["css", "device"]] = None,
mask: typing.Optional[typing.Sequence["Locator"]] = None,
- mask_color: typing.Optional[str] = None
+ mask_color: typing.Optional[str] = None,
+ style: typing.Optional[str] = None
) -> bytes:
"""Page.screenshot
@@ -10041,6 +10081,10 @@ def screenshot(
mask_color : Union[str, None]
Specify the color of the overlay box for masked elements, in
[CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`.
+ style : Union[str, None]
+ Text of the stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make
+ elements invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces
+ the Shadow DOM and applies to the inner frames.
Returns
-------
@@ -10062,6 +10106,7 @@ def screenshot(
scale=scale,
mask=mapping.to_impl(mask),
maskColor=mask_color,
+ style=style,
)
)
)
@@ -10442,8 +10487,13 @@ def locator(
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
When passed a [string], matching is case-insensitive and searches for a substring.
has : Union[Locator, None]
- Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
- one. For example, `article` that has `text=Playwright` matches `Playwright
`.
+ Narrows down the results of the method to those which contain elements matching this relative locator. For example,
+ `article` that has `text=Playwright` matches `Playwright
`.
+
+ Inner locator **must be relative** to the outer locator and is queried starting with the outer locator match, not
+ the document root. For example, you can find `content` that has `div` in
+ `Playwright
`. However, looking for `content` that has `article
+ div` will fail, because the inner locator must be relative and should not use any elements outside the `content`.
Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s.
has_not : Union[Locator, None]
@@ -13698,6 +13748,30 @@ def unroute(
)
)
+ def unroute_all(
+ self,
+ *,
+ behavior: typing.Optional[Literal["default", "ignoreErrors", "wait"]] = None
+ ) -> None:
+ """BrowserContext.unroute_all
+
+ Removes all routes created with `browser_context.route()` and `browser_context.route_from_har()`.
+
+ Parameters
+ ----------
+ behavior : Union["default", "ignoreErrors", "wait", None]
+ Specifies wether to wait for already running handlers and what to do if they throw errors:
+ - `'default'` - do not wait for current handler calls (if any) to finish, if unrouted handler throws, it may
+ result in unhandled error
+ - `'wait'` - wait for current handler calls (if any) to finish
+ - `'ignoreErrors'` - do not wait for current handler calls (if any) to finish, all errors thrown by the handlers
+ after unrouting are silently caught
+ """
+
+ return mapping.from_maybe_impl(
+ self._sync(self._impl_obj.unroute_all(behavior=behavior))
+ )
+
def route_from_har(
self,
har: typing.Union[pathlib.Path, str],
@@ -14756,8 +14830,10 @@ def launch(
"msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using
[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.
+
Additional arguments to pass to the browser instance. The list of Chromium flags can be found
- [here](http://peter.sh/experiments/chromium-command-line-switches/).
+ [here](https://peter.sh/experiments/chromium-command-line-switches/).
ignore_default_args : Union[Sequence[str], bool, None]
If `true`, Playwright does not pass its own configurations args and only uses the ones from `args`. If an array is
given, then filters out the given default arguments. Dangerous option; use with care. Defaults to `false`.
@@ -14913,8 +14989,10 @@ def launch_persistent_context(
resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium,
Firefox or WebKit, use at your own risk.
args : Union[Sequence[str], None]
+ **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality.
+
Additional arguments to pass to the browser instance. The list of Chromium flags can be found
- [here](http://peter.sh/experiments/chromium-command-line-switches/).
+ [here](https://peter.sh/experiments/chromium-command-line-switches/).
ignore_default_args : Union[Sequence[str], bool, None]
If `true`, Playwright does not pass its own configurations args and only uses the ones from `args`. If an array is
given, then filters out the given default arguments. Dangerous option; use with care. Defaults to `false`.
@@ -15397,14 +15475,14 @@ def start(
**Usage**
```py
- await context.tracing.start(name=\"trace\", screenshots=True, snapshots=True)
+ await context.tracing.start(screenshots=True, snapshots=True)
page = await context.new_page()
await page.goto(\"https://playwright.dev\")
await context.tracing.stop(path = \"trace.zip\")
```
```py
- context.tracing.start(name=\"trace\", screenshots=True, snapshots=True)
+ context.tracing.start(screenshots=True, snapshots=True)
page = context.new_page()
page.goto(\"https://playwright.dev\")
context.tracing.stop(path = \"trace.zip\")
@@ -15413,8 +15491,9 @@ def start(
Parameters
----------
name : Union[str, None]
- If specified, the trace is going to be saved into the file with the given name inside the `tracesDir` folder
- specified in `browser_type.launch()`.
+ If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the
+ `tracesDir` folder specified in `browser_type.launch()`. To specify the final trace zip file name, you need
+ to pass `path` option to `tracing.stop()` instead.
title : Union[str, None]
Trace name to be shown in the Trace Viewer.
snapshots : Union[bool, None]
@@ -15451,7 +15530,7 @@ def start_chunk(
**Usage**
```py
- await context.tracing.start(name=\"trace\", screenshots=True, snapshots=True)
+ await context.tracing.start(screenshots=True, snapshots=True)
page = await context.new_page()
await page.goto(\"https://playwright.dev\")
@@ -15467,7 +15546,7 @@ def start_chunk(
```
```py
- context.tracing.start(name=\"trace\", screenshots=True, snapshots=True)
+ context.tracing.start(screenshots=True, snapshots=True)
page = context.new_page()
page.goto(\"https://playwright.dev\")
@@ -15487,8 +15566,9 @@ def start_chunk(
title : Union[str, None]
Trace name to be shown in the Trace Viewer.
name : Union[str, None]
- If specified, the trace is going to be saved into the file with the given name inside the `tracesDir` folder
- specified in `browser_type.launch()`.
+ If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the
+ `tracesDir` folder specified in `browser_type.launch()`. To specify the final trace zip file name, you need
+ to pass `path` option to `tracing.stop_chunk()` instead.
"""
return mapping.from_maybe_impl(
@@ -16238,8 +16318,13 @@ def locator(
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
When passed a [string], matching is case-insensitive and searches for a substring.
has : Union[Locator, None]
- Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
- one. For example, `article` that has `text=Playwright` matches `Playwright
`.
+ Narrows down the results of the method to those which contain elements matching this relative locator. For example,
+ `article` that has `text=Playwright` matches `Playwright
`.
+
+ Inner locator **must be relative** to the outer locator and is queried starting with the outer locator match, not
+ the document root. For example, you can find `content` that has `div` in
+ `Playwright
`. However, looking for `content` that has `article
+ div` will fail, because the inner locator must be relative and should not use any elements outside the `content`.
Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s.
has_not : Union[Locator, None]
@@ -16902,8 +16987,13 @@ def filter(
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
When passed a [string], matching is case-insensitive and searches for a substring.
has : Union[Locator, None]
- Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
- one. For example, `article` that has `text=Playwright` matches `Playwright
`.
+ Narrows down the results of the method to those which contain elements matching this relative locator. For example,
+ `article` that has `text=Playwright` matches `Playwright
`.
+
+ Inner locator **must be relative** to the outer locator and is queried starting with the outer locator match, not
+ the document root. For example, you can find `content` that has `div` in
+ `Playwright
`. However, looking for `content` that has `article
+ div` will fail, because the inner locator must be relative and should not use any elements outside the `content`.
Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s.
has_not : Union[Locator, None]
@@ -17626,7 +17716,8 @@ def screenshot(
caret: typing.Optional[Literal["hide", "initial"]] = None,
scale: typing.Optional[Literal["css", "device"]] = None,
mask: typing.Optional[typing.Sequence["Locator"]] = None,
- mask_color: typing.Optional[str] = None
+ mask_color: typing.Optional[str] = None,
+ style: typing.Optional[str] = None
) -> bytes:
"""Locator.screenshot
@@ -17701,6 +17792,10 @@ def screenshot(
mask_color : Union[str, None]
Specify the color of the overlay box for masked elements, in
[CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`.
+ style : Union[str, None]
+ Text of the stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make
+ elements invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces
+ the Shadow DOM and applies to the inner frames.
Returns
-------
@@ -17720,6 +17815,7 @@ def screenshot(
scale=scale,
mask=mapping.to_impl(mask),
maskColor=mask_color,
+ style=style,
)
)
)
@@ -19449,8 +19545,8 @@ def to_contain_text(
) -> None:
"""LocatorAssertions.to_contain_text
- Ensures the `Locator` points to an element that contains the given text. You can use regular expressions for the
- value as well.
+ Ensures the `Locator` points to an element that contains the given text. All nested elements will be considered
+ when computing the text content of the element. You can use regular expressions for the value as well.
**Details**
@@ -20217,8 +20313,8 @@ def to_have_text(
) -> None:
"""LocatorAssertions.to_have_text
- Ensures the `Locator` points to an element with the given text. You can use regular expressions for the value as
- well.
+ Ensures the `Locator` points to an element with the given text. All nested elements will be considered when
+ computing the text content of the element. You can use regular expressions for the value as well.
**Details**
@@ -20370,7 +20466,8 @@ def to_be_attached(
) -> None:
"""LocatorAssertions.to_be_attached
- Ensures that `Locator` points to an [attached](https://playwright.dev/python/docs/actionability#attached) DOM node.
+ Ensures that `Locator` points to an element that is
+ [connected](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) to a Document or a ShadowRoot.
**Usage**
@@ -20757,8 +20854,7 @@ def to_be_visible(
) -> None:
"""LocatorAssertions.to_be_visible
- Ensures that `Locator` points to an [attached](https://playwright.dev/python/docs/actionability#attached) and
- [visible](https://playwright.dev/python/docs/actionability#visible) DOM node.
+ Ensures that `Locator` points to an attached and [visible](https://playwright.dev/python/docs/actionability#visible) DOM node.
To check that at least one element from the list is visible, use `locator.first()`.
diff --git a/setup.py b/setup.py
index bbf63928c..7f40b41a8 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@
InWheel = None
from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand
-driver_version = "1.40.0-beta-1700587209000"
+driver_version = "1.41.0-beta-1705101589000"
def extractall(zip: zipfile.ZipFile, path: str) -> None:
diff --git a/tests/async/conftest.py b/tests/async/conftest.py
index 490f4440a..442d059f4 100644
--- a/tests/async/conftest.py
+++ b/tests/async/conftest.py
@@ -100,6 +100,17 @@ async def launch(**kwargs: Any) -> BrowserContext:
await context.close()
+@pytest.fixture(scope="session")
+async def default_same_site_cookie_value(browser_name: str) -> str:
+ if browser_name == "chromium":
+ return "Lax"
+ if browser_name == "firefox":
+ return "None"
+ if browser_name == "webkit":
+ return "None"
+ raise Exception(f"Invalid browser_name: {browser_name}")
+
+
@pytest.fixture
async def context(
context_factory: "Callable[..., asyncio.Future[BrowserContext]]",
diff --git a/tests/async/test_asyncio.py b/tests/async/test_asyncio.py
index 084d9eb41..1d4423afb 100644
--- a/tests/async/test_asyncio.py
+++ b/tests/async/test_asyncio.py
@@ -17,7 +17,7 @@
import pytest
-from playwright.async_api import Page, async_playwright
+from playwright.async_api import async_playwright
from tests.server import Server
from tests.utils import TARGET_CLOSED_ERROR_MESSAGE
@@ -67,21 +67,3 @@ async def test_cancel_pending_protocol_call_on_playwright_stop(server: Server) -
with pytest.raises(Exception) as exc_info:
await pending_task
assert TARGET_CLOSED_ERROR_MESSAGE in str(exc_info.value)
-
-
-async def test_should_collect_stale_handles(page: Page, server: Server) -> None:
- page.on("request", lambda _: None)
- response = await page.goto(server.PREFIX + "/title.html")
- assert response
- for i in range(1000):
- await page.evaluate(
- """async () => {
- const response = await fetch('/');
- await response.text();
- }"""
- )
- with pytest.raises(Exception) as exc_info:
- await response.all_headers()
- assert "The object has been collected to prevent unbounded heap growth." in str(
- exc_info.value
- )
diff --git a/tests/async/test_browsercontext.py b/tests/async/test_browsercontext.py
index 23fbd27de..97c365273 100644
--- a/tests/async/test_browsercontext.py
+++ b/tests/async/test_browsercontext.py
@@ -13,7 +13,6 @@
# limitations under the License.
import asyncio
-import re
from typing import Any, List
from urllib.parse import urlparse
@@ -26,8 +25,6 @@
JSHandle,
Page,
Playwright,
- Request,
- Route,
)
from tests.server import Server
from tests.utils import TARGET_CLOSED_ERROR_MESSAGE
@@ -474,114 +471,6 @@ def logme(t: JSHandle) -> int:
assert result == 17
-async def test_route_should_intercept(context: BrowserContext, server: Server) -> None:
- intercepted = []
-
- def handle(route: Route, request: Request) -> None:
- intercepted.append(True)
- assert "empty.html" in request.url
- assert request.headers["user-agent"]
- assert request.method == "GET"
- assert request.post_data is None
- assert request.is_navigation_request()
- assert request.resource_type == "document"
- assert request.frame == page.main_frame
- assert request.frame.url == "about:blank"
- asyncio.create_task(route.continue_())
-
- await context.route("**/empty.html", lambda route, request: handle(route, request))
- page = await context.new_page()
- response = await page.goto(server.EMPTY_PAGE)
- assert response
- assert response.ok
- assert intercepted == [True]
- await context.close()
-
-
-async def test_route_should_unroute(context: BrowserContext, server: Server) -> None:
- page = await context.new_page()
-
- intercepted: List[int] = []
-
- def handler(route: Route, request: Request, ordinal: int) -> None:
- intercepted.append(ordinal)
- asyncio.create_task(route.continue_())
-
- await context.route("**/*", lambda route, request: handler(route, request, 1))
- await context.route(
- "**/empty.html", lambda route, request: handler(route, request, 2)
- )
- await context.route(
- "**/empty.html", lambda route, request: handler(route, request, 3)
- )
-
- def handler4(route: Route, request: Request) -> None:
- handler(route, request, 4)
-
- await context.route(re.compile("empty.html"), handler4)
-
- await page.goto(server.EMPTY_PAGE)
- assert intercepted == [4]
-
- intercepted = []
- await context.unroute(re.compile("empty.html"), handler4)
- await page.goto(server.EMPTY_PAGE)
- assert intercepted == [3]
-
- intercepted = []
- await context.unroute("**/empty.html")
- await page.goto(server.EMPTY_PAGE)
- assert intercepted == [1]
-
-
-async def test_route_should_yield_to_page_route(
- context: BrowserContext, server: Server
-) -> None:
- await context.route(
- "**/empty.html",
- lambda route, request: asyncio.create_task(
- route.fulfill(status=200, body="context")
- ),
- )
-
- page = await context.new_page()
- await page.route(
- "**/empty.html",
- lambda route, request: asyncio.create_task(
- route.fulfill(status=200, body="page")
- ),
- )
-
- response = await page.goto(server.EMPTY_PAGE)
- assert response
- assert response.ok
- assert await response.text() == "page"
-
-
-async def test_route_should_fall_back_to_context_route(
- context: BrowserContext, server: Server
-) -> None:
- await context.route(
- "**/empty.html",
- lambda route, request: asyncio.create_task(
- route.fulfill(status=200, body="context")
- ),
- )
-
- page = await context.new_page()
- await page.route(
- "**/non-empty.html",
- lambda route, request: asyncio.create_task(
- route.fulfill(status=200, body="page")
- ),
- )
-
- response = await page.goto(server.EMPTY_PAGE)
- assert response
- assert response.ok
- assert await response.text() == "context"
-
-
async def test_auth_should_fail_without_credentials(
context: BrowserContext, server: Server
) -> None:
@@ -723,12 +612,17 @@ async def test_should_fail_with_correct_credentials_and_mismatching_port(
async def test_offline_should_work_with_initial_option(
- browser: Browser, server: Server
+ browser: Browser,
+ server: Server,
+ browser_name: str,
) -> None:
context = await browser.new_context(offline=True)
page = await context.new_page()
+ frame_navigated_task = asyncio.create_task(page.wait_for_event("framenavigated"))
with pytest.raises(Error) as exc_info:
await page.goto(server.EMPTY_PAGE)
+ if browser_name == "firefox":
+ await frame_navigated_task
assert exc_info.value
await context.set_offline(False)
response = await page.goto(server.EMPTY_PAGE)
diff --git a/tests/async/test_browsercontext_request_fallback.py b/tests/async/test_browsercontext_request_fallback.py
index b198a4ebd..9abb14649 100644
--- a/tests/async/test_browsercontext_request_fallback.py
+++ b/tests/async/test_browsercontext_request_fallback.py
@@ -15,9 +15,7 @@
import asyncio
from typing import Any, Callable, Coroutine, cast
-import pytest
-
-from playwright.async_api import BrowserContext, Error, Page, Request, Route
+from playwright.async_api import BrowserContext, Page, Request, Route
from tests.server import Server
@@ -96,61 +94,6 @@ async def test_should_chain_once(
assert body == b"fulfilled one"
-async def test_should_not_chain_fulfill(
- page: Page, context: BrowserContext, server: Server
-) -> None:
- failed = [False]
-
- def handler(route: Route) -> None:
- failed[0] = True
-
- await context.route("**/empty.html", handler)
- await context.route(
- "**/empty.html",
- lambda route: asyncio.create_task(route.fulfill(status=200, body="fulfilled")),
- )
- await context.route(
- "**/empty.html", lambda route: asyncio.create_task(route.fallback())
- )
-
- response = await page.goto(server.EMPTY_PAGE)
- assert response
- body = await response.body()
- assert body == b"fulfilled"
- assert not failed[0]
-
-
-async def test_should_not_chain_abort(
- page: Page,
- context: BrowserContext,
- server: Server,
- is_webkit: bool,
- is_firefox: bool,
-) -> None:
- failed = [False]
-
- def handler(route: Route) -> None:
- failed[0] = True
-
- await context.route("**/empty.html", handler)
- await context.route(
- "**/empty.html", lambda route: asyncio.create_task(route.abort())
- )
- await context.route(
- "**/empty.html", lambda route: asyncio.create_task(route.fallback())
- )
-
- with pytest.raises(Error) as excinfo:
- await page.goto(server.EMPTY_PAGE)
- if is_webkit:
- assert "Blocked by Web Inspector" in excinfo.value.message
- elif is_firefox:
- assert "NS_ERROR_FAILURE" in excinfo.value.message
- else:
- assert "net::ERR_FAILED" in excinfo.value.message
- assert not failed[0]
-
-
async def test_should_fall_back_after_exception(
page: Page, context: BrowserContext, server: Server
) -> None:
@@ -352,48 +295,3 @@ def _handler2(route: Route) -> None:
assert post_data_buffer == ["\x00\x01\x02\x03\x04"]
assert server_request.method == b"POST"
assert server_request.post_body == b"\x00\x01\x02\x03\x04"
-
-
-async def test_should_chain_fallback_into_page(
- context: BrowserContext, page: Page, server: Server
-) -> None:
- intercepted = []
-
- def _handler1(route: Route) -> None:
- intercepted.append(1)
- asyncio.create_task(route.fallback())
-
- await context.route("**/empty.html", _handler1)
-
- def _handler2(route: Route) -> None:
- intercepted.append(2)
- asyncio.create_task(route.fallback())
-
- await context.route("**/empty.html", _handler2)
-
- def _handler3(route: Route) -> None:
- intercepted.append(3)
- asyncio.create_task(route.fallback())
-
- await context.route("**/empty.html", _handler3)
-
- def _handler4(route: Route) -> None:
- intercepted.append(4)
- asyncio.create_task(route.fallback())
-
- await page.route("**/empty.html", _handler4)
-
- def _handler5(route: Route) -> None:
- intercepted.append(5)
- asyncio.create_task(route.fallback())
-
- await page.route("**/empty.html", _handler5)
-
- def _handler6(route: Route) -> None:
- intercepted.append(6)
- asyncio.create_task(route.fallback())
-
- await page.route("**/empty.html", _handler6)
-
- await page.goto(server.EMPTY_PAGE)
- assert intercepted == [6, 5, 4, 3, 2, 1]
diff --git a/tests/async/test_browsercontext_route.py b/tests/async/test_browsercontext_route.py
new file mode 100644
index 000000000..d629be467
--- /dev/null
+++ b/tests/async/test_browsercontext_route.py
@@ -0,0 +1,516 @@
+# 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 asyncio
+import re
+from typing import Awaitable, Callable, List
+
+import pytest
+
+from playwright.async_api import (
+ Browser,
+ BrowserContext,
+ Error,
+ Page,
+ Request,
+ Route,
+ expect,
+)
+from tests.server import Server, TestServerRequest
+from tests.utils import must
+
+
+async def test_route_should_intercept(context: BrowserContext, server: Server) -> None:
+ intercepted = []
+
+ def handle(route: Route, request: Request) -> None:
+ intercepted.append(True)
+ assert "empty.html" in request.url
+ assert request.headers["user-agent"]
+ assert request.method == "GET"
+ assert request.post_data is None
+ assert request.is_navigation_request()
+ assert request.resource_type == "document"
+ assert request.frame == page.main_frame
+ assert request.frame.url == "about:blank"
+ asyncio.create_task(route.continue_())
+
+ await context.route("**/empty.html", lambda route, request: handle(route, request))
+ page = await context.new_page()
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+ assert intercepted == [True]
+ await context.close()
+
+
+async def test_route_should_unroute(context: BrowserContext, server: Server) -> None:
+ page = await context.new_page()
+
+ intercepted: List[int] = []
+
+ def handler(route: Route, request: Request, ordinal: int) -> None:
+ intercepted.append(ordinal)
+ asyncio.create_task(route.continue_())
+
+ await context.route("**/*", lambda route, request: handler(route, request, 1))
+ await context.route(
+ "**/empty.html", lambda route, request: handler(route, request, 2)
+ )
+ await context.route(
+ "**/empty.html", lambda route, request: handler(route, request, 3)
+ )
+
+ def handler4(route: Route, request: Request) -> None:
+ handler(route, request, 4)
+
+ await context.route(re.compile("empty.html"), handler4)
+
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [4]
+
+ intercepted = []
+ await context.unroute(re.compile("empty.html"), handler4)
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [3]
+
+ intercepted = []
+ await context.unroute("**/empty.html")
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [1]
+
+
+async def test_route_should_yield_to_page_route(
+ context: BrowserContext, server: Server
+) -> None:
+ await context.route(
+ "**/empty.html",
+ lambda route, request: asyncio.create_task(
+ route.fulfill(status=200, body="context")
+ ),
+ )
+
+ page = await context.new_page()
+ await page.route(
+ "**/empty.html",
+ lambda route, request: asyncio.create_task(
+ route.fulfill(status=200, body="page")
+ ),
+ )
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+ assert await response.text() == "page"
+
+
+async def test_route_should_fall_back_to_context_route(
+ context: BrowserContext, server: Server
+) -> None:
+ await context.route(
+ "**/empty.html",
+ lambda route, request: asyncio.create_task(
+ route.fulfill(status=200, body="context")
+ ),
+ )
+
+ page = await context.new_page()
+ await page.route(
+ "**/non-empty.html",
+ lambda route, request: asyncio.create_task(
+ route.fulfill(status=200, body="page")
+ ),
+ )
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+ assert await response.text() == "context"
+
+
+async def test_should_support_set_cookie_header(
+ context_factory: "Callable[..., Awaitable[BrowserContext]]",
+ default_same_site_cookie_value: str,
+) -> None:
+ context = await context_factory()
+ page = await context.new_page()
+ await page.route(
+ "https://example.com/",
+ lambda route: route.fulfill(
+ headers={
+ "Set-Cookie": "name=value; domain=.example.com; Path=/",
+ },
+ content_type="text/html",
+ body="done",
+ ),
+ )
+ await page.goto("https://example.com")
+ cookies = await context.cookies()
+ assert len(cookies) == 1
+ assert cookies[0] == {
+ "sameSite": default_same_site_cookie_value,
+ "name": "name",
+ "value": "value",
+ "domain": ".example.com",
+ "path": "/",
+ "expires": -1,
+ "httpOnly": False,
+ "secure": False,
+ }
+
+
+@pytest.mark.skip_browser("webkit")
+async def test_should_ignore_secure_set_cookie_header_for_insecure_request(
+ context_factory: "Callable[..., Awaitable[BrowserContext]]",
+) -> None:
+ context = await context_factory()
+ page = await context.new_page()
+ await page.route(
+ "http://example.com/",
+ lambda route: route.fulfill(
+ headers={
+ "Set-Cookie": "name=value; domain=.example.com; Path=/; Secure",
+ },
+ content_type="text/html",
+ body="done",
+ ),
+ )
+ await page.goto("http://example.com")
+ cookies = await context.cookies()
+ assert len(cookies) == 0
+
+
+async def test_should_use_set_cookie_header_in_future_requests(
+ context_factory: "Callable[..., Awaitable[BrowserContext]]",
+ server: Server,
+ default_same_site_cookie_value: str,
+) -> None:
+ context = await context_factory()
+ page = await context.new_page()
+
+ await page.route(
+ server.EMPTY_PAGE,
+ lambda route: route.fulfill(
+ headers={
+ "Set-Cookie": "name=value",
+ },
+ content_type="text/html",
+ body="done",
+ ),
+ )
+ await page.goto(server.EMPTY_PAGE)
+ assert await context.cookies() == [
+ {
+ "sameSite": default_same_site_cookie_value,
+ "name": "name",
+ "value": "value",
+ "domain": "localhost",
+ "path": "/",
+ "expires": -1,
+ "httpOnly": False,
+ "secure": False,
+ }
+ ]
+
+ cookie = ""
+
+ def _handle_request(request: TestServerRequest) -> None:
+ nonlocal cookie
+ cookie = must(request.getHeader("cookie"))
+ request.finish()
+
+ server.set_route("/foo.html", _handle_request)
+ await page.goto(server.PREFIX + "/foo.html")
+ assert cookie == "name=value"
+
+
+async def test_should_work_with_ignore_https_errors(
+ browser: Browser, https_server: Server
+) -> None:
+ context = await browser.new_context(ignore_https_errors=True)
+ page = await context.new_page()
+
+ await page.route("**/*", lambda route: route.continue_())
+ response = await page.goto(https_server.EMPTY_PAGE)
+ assert must(response).status == 200
+ await context.close()
+
+
+async def test_should_support_the_times_parameter_with_route_matching(
+ context: BrowserContext, page: Page, server: Server
+) -> None:
+ intercepted: List[int] = []
+
+ async def _handle_request(route: Route) -> None:
+ intercepted.append(1)
+ await route.continue_()
+
+ await context.route("**/empty.html", _handle_request, times=1)
+ await page.goto(server.EMPTY_PAGE)
+ await page.goto(server.EMPTY_PAGE)
+ await page.goto(server.EMPTY_PAGE)
+ assert len(intercepted) == 1
+
+
+async def test_should_work_if_handler_with_times_parameter_was_removed_from_another_handler(
+ context: BrowserContext, page: Page, server: Server
+) -> None:
+ intercepted = []
+
+ async def _handler(route: Route) -> None:
+ intercepted.append("first")
+ await route.continue_()
+
+ await context.route("**/*", _handler, times=1)
+
+ async def _handler2(route: Route) -> None:
+ intercepted.append("second")
+ await context.unroute("**/*", _handler)
+ await route.fallback()
+
+ await context.route("**/*", _handler2)
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == ["second"]
+ intercepted.clear()
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == ["second"]
+
+
+async def test_should_support_async_handler_with_times(
+ context: BrowserContext, page: Page, server: Server
+) -> None:
+ async def _handler(route: Route) -> None:
+ await asyncio.sleep(0.1)
+ await route.fulfill(
+ body="intercepted",
+ content_type="text/html",
+ )
+
+ await context.route("**/empty.html", _handler, times=1)
+ await page.goto(server.EMPTY_PAGE)
+ await expect(page.locator("body")).to_have_text("intercepted")
+ await page.goto(server.EMPTY_PAGE)
+ await expect(page.locator("body")).not_to_have_text("intercepted")
+
+
+async def test_should_override_post_body_with_empty_string(
+ context: BrowserContext, server: Server, page: Page
+) -> None:
+ await context.route(
+ "**/empty.html",
+ lambda route: route.continue_(
+ post_data="",
+ ),
+ )
+
+ req = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ page.set_content(
+ """
+
+ """
+ % server.EMPTY_PAGE
+ ),
+ )
+
+ assert req[0].post_body == b""
+
+
+async def test_should_chain_fallback(
+ context: BrowserContext, page: Page, server: Server
+) -> None:
+ intercepted: List[int] = []
+
+ async def _handler1(route: Route) -> None:
+ intercepted.append(1)
+ await route.fallback()
+
+ await context.route("**/empty.html", _handler1)
+
+ async def _handler2(route: Route) -> None:
+ intercepted.append(2)
+ await route.fallback()
+
+ await context.route("**/empty.html", _handler2)
+
+ async def _handler3(route: Route) -> None:
+ intercepted.append(3)
+ await route.fallback()
+
+ await context.route("**/empty.html", _handler3)
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [3, 2, 1]
+
+
+async def test_should_chain_fallback_with_dynamic_url(
+ context: BrowserContext, page: Page, server: Server
+) -> None:
+ intercepted: List[int] = []
+
+ async def _handler1(route: Route) -> None:
+ intercepted.append(1)
+ await route.fallback(url=server.EMPTY_PAGE)
+
+ await context.route("**/bar", _handler1)
+
+ async def _handler2(route: Route) -> None:
+ intercepted.append(2)
+ await route.fallback(url="http://localhost/bar")
+
+ await context.route("**/foo", _handler2)
+
+ async def _handler3(route: Route) -> None:
+ intercepted.append(3)
+ await route.fallback(url="http://localhost/foo")
+
+ await context.route("**/empty.html", _handler3)
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [3, 2, 1]
+
+
+async def test_should_not_chain_fulfill(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ failed = [False]
+
+ def handler(route: Route) -> None:
+ failed[0] = True
+
+ await context.route("**/empty.html", handler)
+ await context.route(
+ "**/empty.html",
+ lambda route: asyncio.create_task(route.fulfill(status=200, body="fulfilled")),
+ )
+ await context.route(
+ "**/empty.html", lambda route: asyncio.create_task(route.fallback())
+ )
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ body = await response.body()
+ assert body == b"fulfilled"
+ assert not failed[0]
+
+
+async def test_should_not_chain_abort(
+ page: Page,
+ context: BrowserContext,
+ server: Server,
+ is_webkit: bool,
+ is_firefox: bool,
+) -> None:
+ failed = [False]
+
+ def handler(route: Route) -> None:
+ failed[0] = True
+
+ await context.route("**/empty.html", handler)
+ await context.route(
+ "**/empty.html", lambda route: asyncio.create_task(route.abort())
+ )
+ await context.route(
+ "**/empty.html", lambda route: asyncio.create_task(route.fallback())
+ )
+
+ with pytest.raises(Error) as excinfo:
+ await page.goto(server.EMPTY_PAGE)
+ if is_webkit:
+ assert "Blocked by Web Inspector" in excinfo.value.message
+ elif is_firefox:
+ assert "NS_ERROR_FAILURE" in excinfo.value.message
+ else:
+ assert "net::ERR_FAILED" in excinfo.value.message
+ assert not failed[0]
+
+
+async def test_should_chain_fallback_into_page(
+ context: BrowserContext, page: Page, server: Server
+) -> None:
+ intercepted = []
+
+ def _handler1(route: Route) -> None:
+ intercepted.append(1)
+ asyncio.create_task(route.fallback())
+
+ await context.route("**/empty.html", _handler1)
+
+ def _handler2(route: Route) -> None:
+ intercepted.append(2)
+ asyncio.create_task(route.fallback())
+
+ await context.route("**/empty.html", _handler2)
+
+ def _handler3(route: Route) -> None:
+ intercepted.append(3)
+ asyncio.create_task(route.fallback())
+
+ await context.route("**/empty.html", _handler3)
+
+ def _handler4(route: Route) -> None:
+ intercepted.append(4)
+ asyncio.create_task(route.fallback())
+
+ await page.route("**/empty.html", _handler4)
+
+ def _handler5(route: Route) -> None:
+ intercepted.append(5)
+ asyncio.create_task(route.fallback())
+
+ await page.route("**/empty.html", _handler5)
+
+ def _handler6(route: Route) -> None:
+ intercepted.append(6)
+ asyncio.create_task(route.fallback())
+
+ await page.route("**/empty.html", _handler6)
+
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [6, 5, 4, 3, 2, 1]
+
+
+async def test_should_fall_back_async(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ intercepted = []
+
+ async def _handler1(route: Route) -> None:
+ intercepted.append(1)
+ await asyncio.sleep(0.1)
+ await route.fallback()
+
+ await context.route("**/empty.html", _handler1)
+
+ async def _handler2(route: Route) -> None:
+ intercepted.append(2)
+ await asyncio.sleep(0.1)
+ await route.fallback()
+
+ await context.route("**/empty.html", _handler2)
+
+ async def _handler3(route: Route) -> None:
+ intercepted.append(3)
+ await asyncio.sleep(0.1)
+ await route.fallback()
+
+ await context.route("**/empty.html", _handler3)
+
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [3, 2, 1]
diff --git a/tests/async/test_expect_misc.py b/tests/async/test_expect_misc.py
index 414909b67..9c6a8aa01 100644
--- a/tests/async/test_expect_misc.py
+++ b/tests/async/test_expect_misc.py
@@ -14,7 +14,7 @@
import pytest
-from playwright.async_api import Page, expect
+from playwright.async_api import Page, TimeoutError, expect
from tests.server import Server
@@ -72,3 +72,9 @@ async def test_to_be_in_viewport_should_report_intersection_even_if_fully_covere
"""
)
await expect(page.locator("h1")).to_be_in_viewport()
+
+
+async def test_should_have_timeout_error_name(page: Page) -> None:
+ with pytest.raises(TimeoutError) as exc_info:
+ await page.wait_for_selector("#not-found", timeout=1)
+ assert exc_info.value.name == "TimeoutError"
diff --git a/tests/async/test_har.py b/tests/async/test_har.py
index 31a34f8fa..7e02776f1 100644
--- a/tests/async/test_har.py
+++ b/tests/async/test_har.py
@@ -18,12 +18,13 @@
import re
import zipfile
from pathlib import Path
-from typing import cast
+from typing import Awaitable, Callable, cast
import pytest
from playwright.async_api import Browser, BrowserContext, Error, Page, Route, expect
from tests.server import Server
+from tests.utils import must
async def test_should_work(browser: Browser, server: Server, tmpdir: Path) -> None:
@@ -647,6 +648,44 @@ async def test_should_update_har_zip_for_context(
)
+async def test_page_unroute_all_should_stop_page_route_from_har(
+ context_factory: Callable[[], Awaitable[BrowserContext]],
+ server: Server,
+ assetdir: Path,
+) -> None:
+ har_path = assetdir / "har-fulfill.har"
+ context1 = await context_factory()
+ page1 = await context1.new_page()
+ # The har file contains requests for another domain, so the router
+ # is expected to abort all requests.
+ await page1.route_from_har(har_path, not_found="abort")
+ with pytest.raises(Error) as exc_info:
+ await page1.goto(server.EMPTY_PAGE)
+ assert exc_info.value
+ await page1.unroute_all(behavior="wait")
+ response = must(await page1.goto(server.EMPTY_PAGE))
+ assert response.ok
+
+
+async def test_context_unroute_call_should_stop_context_route_from_har(
+ context_factory: Callable[[], Awaitable[BrowserContext]],
+ server: Server,
+ assetdir: Path,
+) -> None:
+ har_path = assetdir / "har-fulfill.har"
+ context1 = await context_factory()
+ page1 = await context1.new_page()
+ # The har file contains requests for another domain, so the router
+ # is expected to abort all requests.
+ await context1.route_from_har(har_path, not_found="abort")
+ with pytest.raises(Error) as exc_info:
+ await page1.goto(server.EMPTY_PAGE)
+ assert exc_info.value
+ await context1.unroute_all(behavior="wait")
+ response = must(await page1.goto(server.EMPTY_PAGE))
+ assert must(response).ok
+
+
async def test_should_update_har_zip_for_page(
browser: Browser, server: Server, tmpdir: Path
) -> None:
diff --git a/tests/async/test_keyboard.py b/tests/async/test_keyboard.py
index 8e8a162c9..9f8db104e 100644
--- a/tests/async/test_keyboard.py
+++ b/tests/async/test_keyboard.py
@@ -519,27 +519,16 @@ async def test_should_support_macos_shortcuts(
)
-async def test_should_press_the_meta_key(
- page: Page, server: Server, is_firefox: bool, is_mac: bool
-) -> None:
+async def test_should_press_the_meta_key(page: Page) -> None:
lastEvent = await captureLastKeydown(page)
await page.keyboard.press("Meta")
v = await lastEvent.json_value()
metaKey = v["metaKey"]
key = v["key"]
code = v["code"]
- if is_firefox and not is_mac:
- assert key == "OS"
- else:
- assert key == "Meta"
-
- if is_firefox:
- assert code == "MetaLeft"
-
- if is_firefox and not is_mac:
- assert metaKey is False
- else:
- assert metaKey
+ assert key == "Meta"
+ assert code == "MetaLeft"
+ assert metaKey
async def test_should_work_after_a_cross_origin_navigation(
diff --git a/tests/async/test_interception.py b/tests/async/test_page_route.py
similarity index 98%
rename from tests/async/test_interception.py
rename to tests/async/test_page_route.py
index 01f932360..8e0b74130 100644
--- a/tests/async/test_interception.py
+++ b/tests/async/test_page_route.py
@@ -1010,21 +1010,28 @@ async def handle_request(route: Route) -> None:
assert len(intercepted) == 1
-async def test_context_route_should_support_times_parameter(
+async def test_should_work_if_handler_with_times_parameter_was_removed_from_another_handler(
context: BrowserContext, page: Page, server: Server
) -> None:
intercepted = []
- async def handle_request(route: Route) -> None:
+ async def handler(route: Route) -> None:
+ intercepted.append("first")
await route.continue_()
- intercepted.append(True)
- await context.route("**/empty.html", handle_request, times=1)
+ await page.route("**/*", handler, times=1)
+ async def handler2(route: Route) -> None:
+ intercepted.append("second")
+ await page.unroute("**/*", handler)
+ await route.fallback()
+
+ await page.route("**/*", handler2)
await page.goto(server.EMPTY_PAGE)
+ assert intercepted == ["second"]
+ intercepted.clear()
await page.goto(server.EMPTY_PAGE)
- await page.goto(server.EMPTY_PAGE)
- assert len(intercepted) == 1
+ assert intercepted == ["second"]
async def test_should_fulfill_with_global_fetch_result(
diff --git a/tests/async/test_unroute_behavior.py b/tests/async/test_unroute_behavior.py
new file mode 100644
index 000000000..8a9b46b3b
--- /dev/null
+++ b/tests/async/test_unroute_behavior.py
@@ -0,0 +1,451 @@
+# 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 asyncio
+import re
+
+from playwright.async_api import BrowserContext, Error, Page, Route
+from tests.server import Server
+from tests.utils import must
+
+
+async def test_context_unroute_should_not_wait_for_pending_handlers_to_complete(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.continue_()
+
+ await context.route(
+ re.compile(".*"),
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ await route.fallback()
+
+ await context.route(
+ re.compile(".*"),
+ _handler2,
+ )
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ await context.unroute(
+ re.compile(".*"),
+ _handler2,
+ )
+ route_barrier_future.set_result(None)
+ await navigation_task
+ assert second_handler_called
+
+
+async def test_context_unroute_all_removes_all_handlers(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ await context.route(
+ "**/*",
+ lambda route: route.abort(),
+ )
+ await context.route(
+ "**/empty.html",
+ lambda route: route.abort(),
+ )
+ await context.unroute_all()
+ await page.goto(server.EMPTY_PAGE)
+
+
+async def test_context_unroute_all_should_not_wait_for_pending_handlers_to_complete(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.abort()
+
+ await context.route(
+ re.compile(".*"),
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ await route.fallback()
+
+ await context.route(
+ re.compile(".*"),
+ _handler2,
+ )
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ did_unroute = False
+
+ async def _unroute_promise() -> None:
+ nonlocal did_unroute
+ await context.unroute_all(behavior="wait")
+ did_unroute = True
+
+ unroute_task = asyncio.create_task(_unroute_promise())
+ await asyncio.sleep(0.5)
+ assert did_unroute is False
+ route_barrier_future.set_result(None)
+ await unroute_task
+ assert did_unroute
+ await navigation_task
+ assert second_handler_called is False
+
+
+async def test_context_unroute_all_should_not_wait_for_pending_handlers_to_complete_if_behavior_is_ignore_errors(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.abort()
+
+ await context.route(
+ re.compile(".*"),
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ raise Exception("Handler error")
+
+ await context.route(
+ re.compile(".*"),
+ _handler2,
+ )
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ did_unroute = False
+
+ async def _unroute_promise() -> None:
+ await context.unroute_all(behavior="ignoreErrors")
+ nonlocal did_unroute
+ did_unroute = True
+
+ unroute_task = asyncio.create_task(_unroute_promise())
+ await asyncio.sleep(0.5)
+ await unroute_task
+ assert did_unroute
+ route_barrier_future.set_result(None)
+ try:
+ await navigation_task
+ except Error:
+ pass
+ # The error in the unrouted handler should be silently caught and remaining handler called.
+ assert not second_handler_called
+
+
+async def test_page_close_should_not_wait_for_active_route_handlers_on_the_owning_context(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await context.route(
+ re.compile(".*"),
+ lambda route: route_future.set_result(route),
+ )
+ await page.route(
+ re.compile(".*"),
+ lambda route: route.fallback(),
+ )
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ await route_future
+ await page.close()
+
+
+async def test_context_close_should_not_wait_for_active_route_handlers_on_the_owned_pages(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route(
+ re.compile(".*"),
+ lambda route: route_future.set_result(route),
+ )
+ await page.route(re.compile(".*"), lambda route: route.fallback())
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ await route_future
+ await context.close()
+
+
+async def test_page_unroute_should_not_wait_for_pending_handlers_to_complete(
+ page: Page, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.continue_()
+
+ await page.route(
+ re.compile(".*"),
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ await route.fallback()
+
+ await page.route(
+ re.compile(".*"),
+ _handler2,
+ )
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ await page.unroute(
+ re.compile(".*"),
+ _handler2,
+ )
+ route_barrier_future.set_result(None)
+ await navigation_task
+ assert second_handler_called
+
+
+async def test_page_unroute_all_removes_all_routes(page: Page, server: Server) -> None:
+ await page.route(
+ "**/*",
+ lambda route: route.abort(),
+ )
+ await page.route(
+ "**/empty.html",
+ lambda route: route.abort(),
+ )
+ await page.unroute_all()
+ response = must(await page.goto(server.EMPTY_PAGE))
+ assert response.ok
+
+
+async def test_page_unroute_should_wait_for_pending_handlers_to_complete(
+ page: Page, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.abort()
+
+ await page.route(
+ "**/*",
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ await route.fallback()
+
+ await page.route(
+ "**/*",
+ _handler2,
+ )
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ did_unroute = False
+
+ async def _unroute_promise() -> None:
+ await page.unroute_all(behavior="wait")
+ nonlocal did_unroute
+ did_unroute = True
+
+ unroute_task = asyncio.create_task(_unroute_promise())
+ await asyncio.sleep(0.5)
+ assert did_unroute is False
+ route_barrier_future.set_result(None)
+ await unroute_task
+ assert did_unroute
+ await navigation_task
+ assert second_handler_called is False
+
+
+async def test_page_unroute_all_should_not_wait_for_pending_handlers_to_complete_if_behavior_is_ignore_errors(
+ page: Page, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.abort()
+
+ await page.route(re.compile(".*"), _handler1)
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ raise Exception("Handler error")
+
+ await page.route(re.compile(".*"), _handler2)
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ did_unroute = False
+
+ async def _unroute_promise() -> None:
+ await page.unroute_all(behavior="ignoreErrors")
+ nonlocal did_unroute
+ did_unroute = True
+
+ unroute_task = asyncio.create_task(_unroute_promise())
+ await asyncio.sleep(0.5)
+ await unroute_task
+ assert did_unroute
+ route_barrier_future.set_result(None)
+ try:
+ await navigation_task
+ except Error:
+ pass
+ # The error in the unrouted handler should be silently caught.
+ assert not second_handler_called
+
+
+async def test_page_close_does_not_wait_for_active_route_handlers(
+ page: Page, server: Server
+) -> None:
+ second_handler_called = False
+
+ def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+
+ await page.route(
+ "**/*",
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await asyncio.Future()
+
+ await page.route(
+ "**/*",
+ _handler2,
+ )
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ await route_future
+ await page.close()
+ await asyncio.sleep(0.5)
+ assert not second_handler_called
+
+
+async def test_route_continue_should_not_throw_if_page_has_been_closed(
+ page: Page, server: Server
+) -> None:
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route(
+ re.compile(".*"),
+ lambda route: route_future.set_result(route),
+ )
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ route = await route_future
+ await page.close()
+ # Should not throw.
+ await route.continue_()
+
+
+async def test_route_fallback_should_not_throw_if_page_has_been_closed(
+ page: Page, server: Server
+) -> None:
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route(
+ re.compile(".*"),
+ lambda route: route_future.set_result(route),
+ )
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ route = await route_future
+ await page.close()
+ # Should not throw.
+ await route.fallback()
+
+
+async def test_route_fulfill_should_not_throw_if_page_has_been_closed(
+ page: Page, server: Server
+) -> None:
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route(
+ "**/*",
+ lambda route: route_future.set_result(route),
+ )
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ route = await route_future
+ await page.close()
+ # Should not throw.
+ await route.fulfill()
diff --git a/tests/sync/test_sync.py b/tests/sync/test_sync.py
index 3f27a4140..fbd94b932 100644
--- a/tests/sync/test_sync.py
+++ b/tests/sync/test_sync.py
@@ -344,21 +344,3 @@ def test_call_sync_method_after_playwright_close_with_own_loop(
p.start()
p.join()
assert p.exitcode == 0
-
-
-def test_should_collect_stale_handles(page: Page, server: Server) -> None:
- page.on("request", lambda request: None)
- response = page.goto(server.PREFIX + "/title.html")
- assert response
- for i in range(1000):
- page.evaluate(
- """async () => {
- const response = await fetch('/');
- await response.text();
- }"""
- )
- with pytest.raises(Exception) as exc_info:
- response.all_headers()
- assert "The object has been collected to prevent unbounded heap growth." in str(
- exc_info.value
- )
diff --git a/tests/sync/test_unroute_behavior.py b/tests/sync/test_unroute_behavior.py
new file mode 100644
index 000000000..12ae9e22d
--- /dev/null
+++ b/tests/sync/test_unroute_behavior.py
@@ -0,0 +1,46 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from playwright.sync_api import BrowserContext, Page
+from tests.server import Server
+from tests.utils import must
+
+
+def test_context_unroute_all_removes_all_handlers(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ context.route(
+ "**/*",
+ lambda route: route.abort(),
+ )
+ context.route(
+ "**/empty.html",
+ lambda route: route.abort(),
+ )
+ context.unroute_all()
+ page.goto(server.EMPTY_PAGE)
+
+
+def test_page_unroute_all_removes_all_routes(page: Page, server: Server) -> None:
+ page.route(
+ "**/*",
+ lambda route: route.abort(),
+ )
+ page.route(
+ "**/empty.html",
+ lambda route: route.abort(),
+ )
+ page.unroute_all()
+ response = must(page.goto(server.EMPTY_PAGE))
+ assert response.ok