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 - )