diff --git a/README.md b/README.md
index fc53802879..3cee8be654 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.16 | ✅ | ✅ | ✅ |
| WebKit 17.4 | ✅ | ✅ | ✅ |
-| Firefox 119.0 | ✅ | ✅ | ✅ |
+| Firefox 120.0.1 | ✅ | ✅ | ✅ |
## Documentation
diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py
index e7e6f19a86..abb0f1f22c 100644
--- a/playwright/_impl/_browser_context.py
+++ b/playwright/_impl/_browser_context.py
@@ -223,6 +223,8 @@ async def _on_route(self, route: Route) -> None:
for route_handler in route_handlers:
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:
@@ -369,6 +371,11 @@ async def unroute(
)
await self._update_interception_patterns()
+ async def unroute_all(
+ self, behavior: Literal["default", "ignoreErrors", "wait"] = None
+ ) -> None:
+ pass
+
async def _record_into_har(
self,
har: Union[Path, str],
diff --git a/playwright/_impl/_element_handle.py b/playwright/_impl/_element_handle.py
index 6c585bb0d0..558cf3ac9c 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/_locator.py b/playwright/_impl/_locator.py
index 55955d089f..a9cc92abad 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/_page.py b/playwright/_impl/_page.py
index cfa571f74c..cca6f2fc55 100644
--- a/playwright/_impl/_page.py
+++ b/playwright/_impl/_page.py
@@ -240,6 +240,8 @@ async def _on_route(self, route: Route) -> None:
for route_handler in route_handlers:
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:
@@ -593,6 +595,11 @@ async def unroute(
)
await self._update_interception_patterns()
+ async def unroute_all(
+ self, behavior: Literal["default", "ignoreErrors", "wait"] = None
+ ) -> None:
+ pass
+
async def route_from_har(
self,
har: Union[Path, str],
@@ -639,6 +646,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/_set_input_files_helpers.py b/playwright/_impl/_set_input_files_helpers.py
index a5db6c1da9..793144313b 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 d8276a1254..cc3fe02f24 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,
)
)
@@ -4709,8 +4715,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 +6256,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 +9872,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 +9964,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 +10014,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 +10038,7 @@ async def screenshot(
scale=scale,
mask=mapping.to_impl(mask),
maskColor=mask_color,
+ style=style,
)
)
@@ -10362,8 +10408,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 +13691,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 +14765,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 +14922,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`.
@@ -16144,8 +16223,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 +16890,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 +17599,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 +17675,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 +17697,7 @@ async def screenshot(
scale=scale,
mask=mapping.to_impl(mask),
maskColor=mask_color,
+ style=style,
)
)
diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py
index 09a308c2c5..5fbf96d50d 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,
)
)
)
@@ -4799,8 +4805,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 +6376,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 +9938,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 +10032,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 +10082,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 +10107,7 @@ def screenshot(
scale=scale,
mask=mapping.to_impl(mask),
maskColor=mask_color,
+ style=style,
)
)
)
@@ -10442,8 +10488,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 +13749,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 +14831,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 +14990,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`.
@@ -16238,8 +16317,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 +16986,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 +17715,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 +17791,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 +17814,7 @@ def screenshot(
scale=scale,
mask=mapping.to_impl(mask),
maskColor=mask_color,
+ style=style,
)
)
)
diff --git a/setup.py b/setup.py
index 7e77bf8ae3..e50a82b620 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-alpha-1702670966000"
def extractall(zip: zipfile.ZipFile, path: str) -> None:
diff --git a/tests/async/conftest.py b/tests/async/conftest.py
index 490f4440a8..442d059f43 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 084d9eb413..1d4423afb5 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 23fbd27dee..cedaecf179 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:
diff --git a/tests/async/test_browsercontext_request_fallback.py b/tests/async/test_browsercontext_request_fallback.py
index f3959490bc..b7ff846148 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:
@@ -353,48 +296,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 0000000000..5a21b52ee5
--- /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 = 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 is None
+
+
+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 414909b67c..9c6a8aa017 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_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 911d7ddd8a..c5ed597653 100644
--- a/tests/async/test_interception.py
+++ b/tests/async/test_page_route.py
@@ -1009,21 +1009,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/sync/test_sync.py b/tests/sync/test_sync.py
index 3f27a41403..fbd94b932e 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
- )