diff --git a/README.md b/README.md index e597e4778..3bd3ec79d 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 110.0.5481.38 | ✅ | ✅ | ✅ | +| Chromium 111.0.5563.19 | ✅ | ✅ | ✅ | | WebKit 16.4 | ✅ | ✅ | ✅ | -| Firefox 108.0.2 | ✅ | ✅ | ✅ | +| Firefox 109.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py index ddbf3576a..b701555da 100644 --- a/playwright/_impl/_api_structures.py +++ b/playwright/_impl/_api_structures.py @@ -185,7 +185,7 @@ class ExpectedTextValue(TypedDict, total=False): class FrameExpectOptions(TypedDict, total=False): expressionArg: Any expectedText: Optional[List[ExpectedTextValue]] - expectedNumber: Optional[int] + expectedNumber: Optional[float] expectedValue: Optional[Any] useInnerText: Optional[bool] isNot: bool diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 2253bdabd..6f1948edd 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -618,6 +618,25 @@ async def not_to_be_focused( __tracebackhide__ = True await self._not.to_be_focused(timeout) + async def to_be_in_viewport( + self, + ratio: float = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.be.in.viewport", + FrameExpectOptions(timeout=timeout, expectedNumber=ratio), + None, + "Locator expected to be in viewport", + ) + + async def not_to_be_in_viewport( + self, ratio: float = None, timeout: float = None + ) -> None: + __tracebackhide__ = True + await self._not.to_be_in_viewport(ratio=ratio, timeout=timeout) + class APIResponseAssertions: def __init__(self, response: APIResponse, is_not: bool = False) -> None: diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 2cad9af7b..4c1262e5f 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -191,7 +191,11 @@ async def _on_route(self, route: Route) -> None: handled = await route_handler.handle(route) finally: if len(self._routes) == 0: - asyncio.create_task(self._disable_interception()) + asyncio.create_task( + self._connection.wrap_api_call( + lambda: self._update_interception_patterns(), True + ) + ) if handled: return await route._internal_continue(is_internal=True) @@ -304,10 +308,7 @@ async def route( times, ), ) - if len(self._routes) == 1: - await self._channel.send( - "setNetworkInterceptionEnabled", dict(enabled=True) - ) + await self._update_interception_patterns() async def unroute( self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None @@ -318,8 +319,7 @@ async def unroute( self._routes, ) ) - if len(self._routes) == 0: - await self._disable_interception() + await self._update_interception_patterns() async def _record_into_har( self, @@ -360,8 +360,11 @@ async def route_from_har( ) await router.add_context_route(self) - async def _disable_interception(self) -> None: - await self._channel.send("setNetworkInterceptionEnabled", dict(enabled=False)) + async def _update_interception_patterns(self) -> None: + patterns = RouteHandler.prepare_interception_patterns(self._routes) + await self._channel.send( + "setNetworkInterceptionPatterns", {"patterns": patterns} + ) def expect_event( self, diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index a9857591c..065ce101b 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -291,6 +291,28 @@ def impl() -> None: def will_expire(self) -> bool: return self._handled_count + 1 >= self._times + @staticmethod + def prepare_interception_patterns( + handlers: List["RouteHandler"], + ) -> List[Dict[str, str]]: + patterns = [] + all = False + for handler in handlers: + if isinstance(handler.matcher.match, str): + patterns.append({"glob": handler.matcher.match}) + elif isinstance(handler.matcher._regex_obj, re.Pattern): + patterns.append( + { + "regexSource": handler.matcher._regex_obj.pattern, + "regexFlags": escape_regex_flags(handler.matcher._regex_obj), + } + ) + else: + all = True + if all: + return [{"glob": "**/*"}] + return patterns + def is_safe_close_error(error: Exception) -> bool: message = str(error) diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index e233e6727..c70691c07 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -643,7 +643,7 @@ async def _expect( { "selector": self._selector, "expression": expression, - **options, + **({k: v for k, v in options.items() if v is not None}), }, ) if result.get("received"): diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 894e802c7..bdc960647 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -241,6 +241,11 @@ async def _actual_headers(self) -> "RawHeaders": self._all_headers_future.set_result(RawHeaders(headers)) return await self._all_headers_future + def _target_closed_future(self) -> asyncio.Future: + if not hasattr(self.frame, "_page"): + return asyncio.Future() + return self.frame._page._closed_or_crashed_future + class Route(ChannelOwner): def __init__( @@ -348,10 +353,11 @@ async def fetch( method: str = None, headers: Dict[str, str] = None, postData: Union[Any, str, bytes] = None, + maxRedirects: int = None, ) -> "APIResponse": page = self.request.frame._page return await page.context.request._inner_fetch( - self.request, url, method, headers, postData + self.request, url, method, headers, postData, maxRedirects=maxRedirects ) async def fallback( @@ -419,30 +425,22 @@ async def _redirected_navigation_request(self, url: str) -> None: self._report_handled(True) async def _race_with_page_close(self, future: Coroutine) -> None: - if hasattr(self.request.frame, "_page"): - page = self.request.frame._page - # When page closes or crashes, we catch any potential rejects from this Route. - # Note that page could be missing when routing popup's initial request that - # does not have a Page initialized just yet. - fut = asyncio.create_task(future) - # Rewrite the user's stack to the new task which runs in the background. - setattr( - fut, - "__pw_stack__", - getattr( - asyncio.current_task(self._loop), "__pw_stack__", inspect.stack() - ), - ) - await asyncio.wait( - [fut, page._closed_or_crashed_future], - return_when=asyncio.FIRST_COMPLETED, - ) - if fut.done() and fut.exception(): - raise cast(BaseException, fut.exception()) - if page._closed_or_crashed_future.done(): - await asyncio.gather(fut, return_exceptions=True) - else: - await future + fut = asyncio.create_task(future) + # Rewrite the user's stack to the new task which runs in the background. + setattr( + fut, + "__pw_stack__", + getattr(asyncio.current_task(self._loop), "__pw_stack__", inspect.stack()), + ) + target_closed_future = self.request._target_closed_future() + await asyncio.wait( + [fut, target_closed_future], + return_when=asyncio.FIRST_COMPLETED, + ) + if fut.done() and fut.exception(): + raise cast(BaseException, fut.exception()) + if target_closed_future.done(): + await asyncio.gather(fut, return_exceptions=True) class Response(ChannelOwner): @@ -522,7 +520,20 @@ async def security_details(self) -> Optional[SecurityDetails]: return await self._channel.send("securityDetails") async def finished(self) -> None: - await self._finished_future + async def on_finished() -> None: + await self._request._target_closed_future() + raise Error("Target closed") + + on_finished_task = asyncio.create_task(on_finished()) + await asyncio.wait( + cast( + List[Union[asyncio.Task, asyncio.Future]], + [self._finished_future, on_finished_task], + ), + return_when=asyncio.FIRST_COMPLETED, + ) + if on_finished_task.done(): + await on_finished_task async def body(self) -> bytes: binary = await self._channel.send("body") diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index a40fe4d10..22a8f72c6 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -254,7 +254,11 @@ async def _on_route(self, route: Route) -> None: handled = await route_handler.handle(route) finally: if len(self._routes) == 0: - asyncio.create_task(self._disable_interception()) + asyncio.create_task( + self._connection.wrap_api_call( + lambda: self._update_interception_patterns(), True + ) + ) if handled: return await self._browser_context._on_route(route) @@ -594,10 +598,7 @@ async def route( times, ), ) - if len(self._routes) == 1: - await self._channel.send( - "setNetworkInterceptionEnabled", dict(enabled=True) - ) + await self._update_interception_patterns() async def unroute( self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None @@ -608,8 +609,7 @@ async def unroute( self._routes, ) ) - if len(self._routes) == 0: - await self._disable_interception() + await self._update_interception_patterns() async def route_from_har( self, @@ -629,8 +629,11 @@ async def route_from_har( ) await router.add_page_route(self) - async def _disable_interception(self) -> None: - await self._channel.send("setNetworkInterceptionEnabled", dict(enabled=False)) + async def _update_interception_patterns(self) -> None: + patterns = RouteHandler.prepare_interception_patterns(self._routes) + await self._channel.send( + "setNetworkInterceptionPatterns", {"patterns": patterns} + ) async def screenshot( self, diff --git a/playwright/_impl/_str_utils.py b/playwright/_impl/_str_utils.py index 45c80184f..769f530de 100644 --- a/playwright/_impl/_str_utils.py +++ b/playwright/_impl/_str_utils.py @@ -48,5 +48,13 @@ def escape_for_text_selector( def escape_for_attribute_selector(value: str, exact: bool = None) -> str: - suffix = "" if exact else "i" - return '"' + value.replace('"', '\\"') + '"' + suffix + # TODO: this should actually be + # cssEscape(value).replace(/\\ /g, ' ') + # However, our attribute selectors do not conform to CSS parsing spec, + # so we escape them differently. + return ( + '"' + + value.replace("\\", "\\\\").replace('"', '\\"') + + '"' + + ("s" if exact else "i") + ) diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 0595f5751..e8b8757ee 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -743,7 +743,8 @@ async def fetch( url: typing.Optional[str] = None, method: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, - post_data: typing.Optional[typing.Union[typing.Any, str, bytes]] = None + post_data: typing.Optional[typing.Union[typing.Any, str, bytes]] = None, + max_redirects: typing.Optional[int] = None ) -> "APIResponse": """Route.fetch @@ -772,6 +773,12 @@ def handle(route): page.route(\"https://dog.ceo/api/breeds/list/all\", handle) ``` + **Details** + + Note that `headers` option will apply to the fetched request as well as any redirects initiated by it. If you want + to only apply `headers` to the original request, but not to redirects, look into `route.continue_()` + instead. + Parameters ---------- url : Union[str, None] @@ -784,6 +791,9 @@ def handle(route): Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` header will be set to `application/octet-stream` if not explicitly set. + max_redirects : Union[int, None] + Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is + exceeded. Defaults to `20`. Pass `0` to not follow redirects. Returns ------- @@ -796,6 +806,7 @@ def handle(route): method=method, headers=mapping.to_impl(headers), postData=mapping.to_impl(post_data), + maxRedirects=max_redirects, ) ) @@ -964,6 +975,12 @@ def handle(route, request): page.route(\"**/*\", handle) ``` + **Details** + + Note that any overrides such as `url` or `headers` only apply to the request being routed. If this request results + in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header + through redirects, use the combination of `route.fetch()` and `route.fulfill()` instead. + Parameters ---------- url : Union[str, None] @@ -1508,6 +1525,8 @@ async def tap(self, x: float, y: float) -> None: Dispatches a `touchstart` and `touchend` event with a single touch at the position (`x`,`y`). + **NOTE** `page.tap()` the method will throw if `hasTouch` option of the browser context is false. + Parameters ---------- x : float @@ -6858,6 +6877,8 @@ async def register( ) -> None: """Selectors.register + Selectors must be registered before creating the page. + **Usage** An example of registering selector engine that queries elements based on a tag name: @@ -6887,8 +6908,8 @@ async def run(playwright): # Use the selector prefixed with its name. button = await page.query_selector('tag=button') - # Combine it with other selector engines. - await page.locator('tag=div >> text=\"Click me\"').click() + # Combine it with built-in locators. + await page.locator('tag=div').get_by_text('Click me').click() # Can use it in any methods supporting selectors. button_count = await page.locator('tag=button').count() print(button_count) @@ -6925,8 +6946,8 @@ def run(playwright): # Use the selector prefixed with its name. button = page.locator('tag=button') - # Combine it with other selector engines. - page.locator('tag=div >> text=\"Click me\"').click() + # Combine it with built-in locators. + page.locator('tag=div').get_by_text('Click me').click() # Can use it in any methods supporting selectors. button_count = page.locator('tag=button').count() print(button_count) @@ -10090,7 +10111,7 @@ async def tap( When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. Passing zero timeout disables this. - **NOTE** `page.tap()` requires that the `hasTouch` option of the browser context be set to true. + **NOTE** `page.tap()` the method will throw if `hasTouch` option of the browser context is false. Parameters ---------- @@ -15369,7 +15390,7 @@ async def dispatch_event( ) -> None: """Locator.dispatch_event - Programmaticaly dispatch an event on the matching element. + Programmatically dispatch an event on the matching element. **Usage** @@ -16871,7 +16892,7 @@ async def press( ) -> None: """Locator.press - Focuses the mathing element and presses a combintation of the keys. + Focuses the matching element and presses a combination of the keys. **Usage** @@ -17815,7 +17836,8 @@ async def delete( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -17892,7 +17914,8 @@ async def head( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -17981,7 +18004,8 @@ async def get( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -18058,7 +18082,8 @@ async def patch( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -18135,7 +18160,8 @@ async def put( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -18251,7 +18277,8 @@ async def post( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -18358,7 +18385,8 @@ async def fetch( If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) or [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)). If not specified, GET method is used. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -19966,6 +19994,79 @@ async def not_to_be_focused( await self._impl_obj.not_to_be_focused(timeout=timeout) ) + async def to_be_in_viewport( + self, + *, + ratio: typing.Optional[float] = None, + timeout: typing.Optional[float] = None + ) -> None: + """LocatorAssertions.to_be_in_viewport + + Ensures the `Locator` points to an element that intersects viewport, according to the + [intersection observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). + + **Usage** + + ```py + from playwright.async_api import expect + + locator = page.get_by_role(\"button\") + # Make sure at least some part of element intersects viewport. + await expect(locator).to_be_in_viewport() + # Make sure element is fully outside of viewport. + await expect(locator).not_to_be_in_viewport() + # Make sure that at least half of the element intersects viewport. + await expect(locator).to_be_in_viewport(ratio=0.5) + ``` + + ```py + from playwright.sync_api import expect + + locator = page.get_by_role(\"button\") + # Make sure at least some part of element intersects viewport. + expect(locator).to_be_in_viewport() + # Make sure element is fully outside of viewport. + expect(locator).not_to_be_in_viewport() + # Make sure that at least half of the element intersects viewport. + expect(locator).to_be_in_viewport(ratio=0.5) + ``` + + Parameters + ---------- + ratio : Union[float, None] + The minimal ratio of the element to intersect viewport. If equals to `0`, then element should intersect viewport at + any positive ratio. Defaults to `0`. + timeout : Union[float, None] + Time to retry the assertion for. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + await self._impl_obj.to_be_in_viewport(ratio=ratio, timeout=timeout) + ) + + async def not_to_be_in_viewport( + self, + *, + ratio: typing.Optional[float] = None, + timeout: typing.Optional[float] = None + ) -> None: + """LocatorAssertions.not_to_be_in_viewport + + The opposite of `locator_assertions.to_be_in_viewport()`. + + Parameters + ---------- + ratio : Union[float, None] + timeout : Union[float, None] + Time to retry the assertion for. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + await self._impl_obj.not_to_be_in_viewport(ratio=ratio, timeout=timeout) + ) + mapping.register(LocatorAssertionsImpl, LocatorAssertions) diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 5aa50a668..c84abe680 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -753,7 +753,8 @@ def fetch( url: typing.Optional[str] = None, method: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, - post_data: typing.Optional[typing.Union[typing.Any, str, bytes]] = None + post_data: typing.Optional[typing.Union[typing.Any, str, bytes]] = None, + max_redirects: typing.Optional[int] = None ) -> "APIResponse": """Route.fetch @@ -782,6 +783,12 @@ def handle(route): page.route(\"https://dog.ceo/api/breeds/list/all\", handle) ``` + **Details** + + Note that `headers` option will apply to the fetched request as well as any redirects initiated by it. If you want + to only apply `headers` to the original request, but not to redirects, look into `route.continue_()` + instead. + Parameters ---------- url : Union[str, None] @@ -794,6 +801,9 @@ def handle(route): Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` header will be set to `application/octet-stream` if not explicitly set. + max_redirects : Union[int, None] + Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is + exceeded. Defaults to `20`. Pass `0` to not follow redirects. Returns ------- @@ -807,6 +817,7 @@ def handle(route): method=method, headers=mapping.to_impl(headers), postData=mapping.to_impl(post_data), + maxRedirects=max_redirects, ) ) ) @@ -978,6 +989,12 @@ def handle(route, request): page.route(\"**/*\", handle) ``` + **Details** + + Note that any overrides such as `url` or `headers` only apply to the request being routed. If this request results + in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header + through redirects, use the combination of `route.fetch()` and `route.fulfill()` instead. + Parameters ---------- url : Union[str, None] @@ -1506,6 +1523,8 @@ def tap(self, x: float, y: float) -> None: Dispatches a `touchstart` and `touchend` event with a single touch at the position (`x`,`y`). + **NOTE** `page.tap()` the method will throw if `hasTouch` option of the browser context is false. + Parameters ---------- x : float @@ -6972,6 +6991,8 @@ def register( ) -> None: """Selectors.register + Selectors must be registered before creating the page. + **Usage** An example of registering selector engine that queries elements based on a tag name: @@ -7001,8 +7022,8 @@ async def run(playwright): # Use the selector prefixed with its name. button = await page.query_selector('tag=button') - # Combine it with other selector engines. - await page.locator('tag=div >> text=\"Click me\"').click() + # Combine it with built-in locators. + await page.locator('tag=div').get_by_text('Click me').click() # Can use it in any methods supporting selectors. button_count = await page.locator('tag=button').count() print(button_count) @@ -7039,8 +7060,8 @@ def run(playwright): # Use the selector prefixed with its name. button = page.locator('tag=button') - # Combine it with other selector engines. - page.locator('tag=div >> text=\"Click me\"').click() + # Combine it with built-in locators. + page.locator('tag=div').get_by_text('Click me').click() # Can use it in any methods supporting selectors. button_count = page.locator('tag=button').count() print(button_count) @@ -10160,7 +10181,7 @@ def tap( When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. Passing zero timeout disables this. - **NOTE** `page.tap()` requires that the `hasTouch` option of the browser context be set to true. + **NOTE** `page.tap()` the method will throw if `hasTouch` option of the browser context is false. Parameters ---------- @@ -15463,7 +15484,7 @@ def dispatch_event( ) -> None: """Locator.dispatch_event - Programmaticaly dispatch an event on the matching element. + Programmatically dispatch an event on the matching element. **Usage** @@ -16997,7 +17018,7 @@ def press( ) -> None: """Locator.press - Focuses the mathing element and presses a combintation of the keys. + Focuses the matching element and presses a combination of the keys. **Usage** @@ -17959,7 +17980,8 @@ def delete( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -18038,7 +18060,8 @@ def head( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -18129,7 +18152,8 @@ def get( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -18208,7 +18232,8 @@ def patch( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -18287,7 +18312,8 @@ def put( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -18405,7 +18431,8 @@ def post( params : Union[Dict[str, Union[bool, float, str]], None] Query parameters to be sent with the URL. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -18514,7 +18541,8 @@ def fetch( If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) or [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)). If not specified, GET method is used. headers : Union[Dict[str, str], None] - Allows to set HTTP headers. + Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by + it. data : Union[Any, bytes, str, None] Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` @@ -20162,6 +20190,81 @@ def not_to_be_focused(self, *, timeout: typing.Optional[float] = None) -> None: self._sync(self._impl_obj.not_to_be_focused(timeout=timeout)) ) + def to_be_in_viewport( + self, + *, + ratio: typing.Optional[float] = None, + timeout: typing.Optional[float] = None + ) -> None: + """LocatorAssertions.to_be_in_viewport + + Ensures the `Locator` points to an element that intersects viewport, according to the + [intersection observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). + + **Usage** + + ```py + from playwright.async_api import expect + + locator = page.get_by_role(\"button\") + # Make sure at least some part of element intersects viewport. + await expect(locator).to_be_in_viewport() + # Make sure element is fully outside of viewport. + await expect(locator).not_to_be_in_viewport() + # Make sure that at least half of the element intersects viewport. + await expect(locator).to_be_in_viewport(ratio=0.5) + ``` + + ```py + from playwright.sync_api import expect + + locator = page.get_by_role(\"button\") + # Make sure at least some part of element intersects viewport. + expect(locator).to_be_in_viewport() + # Make sure element is fully outside of viewport. + expect(locator).not_to_be_in_viewport() + # Make sure that at least half of the element intersects viewport. + expect(locator).to_be_in_viewport(ratio=0.5) + ``` + + Parameters + ---------- + ratio : Union[float, None] + The minimal ratio of the element to intersect viewport. If equals to `0`, then element should intersect viewport at + any positive ratio. Defaults to `0`. + timeout : Union[float, None] + Time to retry the assertion for. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + self._sync(self._impl_obj.to_be_in_viewport(ratio=ratio, timeout=timeout)) + ) + + def not_to_be_in_viewport( + self, + *, + ratio: typing.Optional[float] = None, + timeout: typing.Optional[float] = None + ) -> None: + """LocatorAssertions.not_to_be_in_viewport + + The opposite of `locator_assertions.to_be_in_viewport()`. + + Parameters + ---------- + ratio : Union[float, None] + timeout : Union[float, None] + Time to retry the assertion for. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.not_to_be_in_viewport(ratio=ratio, timeout=timeout) + ) + ) + mapping.register(LocatorAssertionsImpl, LocatorAssertions) diff --git a/setup.py b/setup.py index 626396466..da75b45a3 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.30.0" +driver_version = "1.31.0-beta-1676906983000" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/async/test_expect_misc.py b/tests/async/test_expect_misc.py new file mode 100644 index 000000000..414909b67 --- /dev/null +++ b/tests/async/test_expect_misc.py @@ -0,0 +1,74 @@ +# 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 pytest + +from playwright.async_api import Page, expect +from tests.server import Server + + +async def test_to_be_in_viewport_should_work(page: Page, server: Server) -> None: + await page.set_content( + """ +
+