diff --git a/README.md b/README.md index b203c6dab..b450b87f2 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 134.0.6998.35 | ✅ | ✅ | ✅ | +| Chromium 136.0.7103.25 | ✅ | ✅ | ✅ | | WebKit 18.4 | ✅ | ✅ | ✅ | -| Firefox 135.0 | ✅ | ✅ | ✅ | +| Firefox 137.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 8ec657531..2a3beb756 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -300,6 +300,45 @@ async def not_to_have_class( __tracebackhide__ = True await self._not.to_have_class(expected, timeout) + async def to_contain_class( + self, + expected: Union[ + Sequence[str], + str, + ], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + if isinstance(expected, collections.abc.Sequence) and not isinstance( + expected, str + ): + expected_text = to_expected_text_values(expected) + await self._expect_impl( + "to.contain.class.array", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + expected, + "Locator expected to contain class names", + ) + else: + expected_text = to_expected_text_values([expected]) + await self._expect_impl( + "to.contain.class", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + expected, + "Locator expected to contain class", + ) + + async def not_to_contain_class( + self, + expected: Union[ + Sequence[str], + str, + ], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_contain_class(expected, timeout) + async def to_have_count( self, count: int, diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index b53e4e629..88f5810ee 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -74,6 +74,7 @@ async def new_context( storageState: Union[StorageState, str, Path] = None, clientCertificates: List[ClientCertificate] = None, failOnStatusCode: bool = None, + maxRedirects: int = None, ) -> "APIRequestContext": params = locals_to_params(locals()) if "storageState" in params: diff --git a/playwright/_impl/_js_handle.py b/playwright/_impl/_js_handle.py index 572d4975e..0d0d7e2ef 100644 --- a/playwright/_impl/_js_handle.py +++ b/playwright/_impl/_js_handle.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 import collections.abc import datetime import math +import struct import traceback from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union @@ -260,6 +262,56 @@ def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any: if "b" in value: return value["b"] + + if "ta" in value: + encoded_bytes = value["ta"]["b"] + decoded_bytes = base64.b64decode(encoded_bytes) + array_type = value["ta"]["k"] + if array_type == "i8": + word_size = 1 + fmt = "b" + elif array_type == "ui8" or array_type == "ui8c": + word_size = 1 + fmt = "B" + elif array_type == "i16": + word_size = 2 + fmt = "h" + elif array_type == "ui16": + word_size = 2 + fmt = "H" + elif array_type == "i32": + word_size = 4 + fmt = "i" + elif array_type == "ui32": + word_size = 4 + fmt = "I" + elif array_type == "f32": + word_size = 4 + fmt = "f" + elif array_type == "f64": + word_size = 8 + fmt = "d" + elif array_type == "bi64": + word_size = 8 + fmt = "q" + elif array_type == "bui64": + word_size = 8 + fmt = "Q" + else: + raise ValueError(f"Unsupported array type: {array_type}") + + byte_len = len(decoded_bytes) + if byte_len % word_size != 0: + raise ValueError( + f"Decoded bytes length {byte_len} is not a multiple of word size {word_size}" + ) + + if byte_len == 0: + return [] + array_len = byte_len // word_size + # "<" denotes little-endian + format_string = f"<{array_len}{fmt}" + return list(struct.unpack(format_string, decoded_bytes)) return value diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 37b1f9441..189485f47 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -540,7 +540,7 @@ async def screenshot( ), ) - async def aria_snapshot(self, timeout: float = None) -> str: + async def aria_snapshot(self, timeout: float = None, ref: bool = None) -> str: return await self._frame._channel.send( "ariaSnapshot", { diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index d2f93dbb6..b622ab858 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -929,6 +929,10 @@ async def handle(route, request): `route.continue_()` will immediately send the request to the network, other matching handlers won't be invoked. Use `route.fallback()` If you want next matching handler in the chain to be invoked. + **NOTE** The `Cookie` header cannot be overridden using this method. If a value is provided, it will be ignored, + and the cookie will be loaded from the browser's cookie store. To set custom cookies, use + `browser_context.add_cookies()`. + Parameters ---------- url : Union[str, None] @@ -9486,8 +9490,8 @@ async def handle_route(route: Route): Parameters ---------- url : Union[Callable[[str], bool], Pattern[str], str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context - options was provided and the passed URL is a path, it gets merged via the + A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If `baseURL` is set in + the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. @@ -13216,8 +13220,8 @@ async def handle_route(route: Route): Parameters ---------- url : Union[Callable[[str], bool], Pattern[str], str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context - options was provided and the passed URL is a path, it gets merged via the + A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If `baseURL` is set in + the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. @@ -13464,9 +13468,6 @@ async def storage_state( state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this. - **NOTE** IndexedDBs with typed arrays are currently not supported. - - Returns ------- {cookies: List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"]}], origins: List[{origin: str, localStorage: List[{name: str, value: str}]}]} @@ -14418,7 +14419,7 @@ async def launch( headless : Union[bool, None] Whether to run browser in headless mode. More details for [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the + [Firefox](https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/). Defaults to `true` unless the `devtools` option is `true`. devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the @@ -14543,11 +14544,15 @@ async def launch_persistent_context( Parameters ---------- user_data_dir : Union[pathlib.Path, str] - Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for + Path to a User Data Directory, which stores browser session data like cookies and local storage. Pass an empty + string to create a temporary directory. + + More details for [Chromium](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile). Note that Chromium's - user data directory is the **parent** directory of the "Profile Path" seen at `chrome://version`. Pass an empty - string to use a temporary directory instead. + [Firefox](https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile). Chromium's user data directory is the + **parent** directory of the "Profile Path" seen at `chrome://version`. + + Note that browsers do not allow launching multiple instances with the same User Data Directory. channel : Union[str, None] Browser distribution channel. @@ -14581,7 +14586,7 @@ async def launch_persistent_context( headless : Union[bool, None] Whether to run browser in headless mode. More details for [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the + [Firefox](https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/). Defaults to `true` unless the `devtools` option is `true`. devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the @@ -15630,8 +15635,8 @@ async def evaluate( arg : Union[Any, None] Optional argument to pass to `expression`. timeout : Union[float, None] - Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can - be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, + evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. Returns ------- @@ -15720,8 +15725,8 @@ async def evaluate_handle( arg : Union[Any, None] Optional argument to pass to `expression`. timeout : Union[float, None] - Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can - be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, + evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. Returns ------- @@ -17215,7 +17220,12 @@ async def screenshot( ) ) - async def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str: + async def aria_snapshot( + self, + *, + timeout: typing.Optional[float] = None, + ref: typing.Optional[bool] = None, + ) -> str: """Locator.aria_snapshot Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/python/docs/aria-snapshots) and @@ -17260,6 +17270,9 @@ async def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str: timeout : Union[float, None] Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + ref : Union[bool, None] + Generate symbolic reference for each element. One can use `aria-ref=` locator immediately after capturing the + snapshot to perform actions on the element. Returns ------- @@ -17267,7 +17280,7 @@ async def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str: """ return mapping.from_maybe_impl( - await self._impl_obj.aria_snapshot(timeout=timeout) + await self._impl_obj.aria_snapshot(timeout=timeout, ref=ref) ) async def scroll_into_view_if_needed( @@ -18700,6 +18713,7 @@ async def new_context( ] = None, client_certificates: typing.Optional[typing.List[ClientCertificate]] = None, fail_on_status_code: typing.Optional[bool] = None, + max_redirects: typing.Optional[int] = None, ) -> "APIRequestContext": """APIRequest.new_context @@ -18751,6 +18765,10 @@ async def new_context( fail_on_status_code : Union[bool, None] Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + 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. This can be overwritten for each request + individually. Returns ------- @@ -18769,6 +18787,7 @@ async def new_context( storageState=storage_state, clientCertificates=client_certificates, failOnStatusCode=fail_on_status_code, + maxRedirects=max_redirects, ) ) @@ -19133,7 +19152,7 @@ async def to_have_class( """LocatorAssertions.to_have_class Ensures the `Locator` points to an element with given CSS classes. When a string is provided, it must fully match - the element's `class` attribute. To match individual classes or perform partial matches, use a regular expression: + the element's `class` attribute. To match individual classes use `locator_assertions.to_contain_class()`. **Usage** @@ -19145,8 +19164,8 @@ async def to_have_class( from playwright.async_api import expect locator = page.locator(\"#component\") - await expect(locator).to_have_class(re.compile(r\"(^|\\\\s)selected(\\\\s|$)\")) await expect(locator).to_have_class(\"middle selected row\") + await expect(locator).to_have_class(re.compile(r\"(^|\\\\s)selected(\\\\s|$)\")) ``` When an array is passed, the method asserts that the list of elements located matches the corresponding list of @@ -19206,6 +19225,92 @@ async def not_to_have_class( ) ) + async def to_contain_class( + self, + expected: typing.Union[typing.Sequence[str], str], + *, + timeout: typing.Optional[float] = None, + ) -> None: + """LocatorAssertions.to_contain_class + + Ensures the `Locator` points to an element with given CSS classes. All classes from the asserted value, separated + by spaces, must be present in the + [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order. + + **Usage** + + ```html +
+ ``` + + ```py + from playwright.async_api import expect + + locator = page.locator(\"#component\") + await expect(locator).to_contain_class(\"middle selected row\") + await expect(locator).to_contain_class(\"selected\") + await expect(locator).to_contain_class(\"row middle\") + ``` + + When an array is passed, the method asserts that the list of elements located matches the corresponding list of + expected class lists. Each element's class attribute is matched against the corresponding class in the array: + + ```html +
+
+
+
+ + ``` + + ```py + from playwright.async_api import expect + + locator = page.locator(\"list > .component\") + await expect(locator).to_contain_class([\"inactive\", \"active\", \"inactive\"]) + ``` + + Parameters + ---------- + expected : Union[Sequence[str], str] + A string containing expected class names, separated by spaces, or a list of such strings to assert multiple + elements. + timeout : Union[float, None] + Time to retry the assertion for in milliseconds. Defaults to `5000`. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + await self._impl_obj.to_contain_class( + expected=mapping.to_impl(expected), timeout=timeout + ) + ) + + async def not_to_contain_class( + self, + expected: typing.Union[typing.Sequence[str], str], + *, + timeout: typing.Optional[float] = None, + ) -> None: + """LocatorAssertions.not_to_contain_class + + The opposite of `locator_assertions.to_contain_class()`. + + Parameters + ---------- + expected : Union[Sequence[str], str] + Expected class or RegExp or a list of those. + timeout : Union[float, None] + Time to retry the assertion for in milliseconds. Defaults to `5000`. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + await self._impl_obj.not_to_contain_class( + expected=mapping.to_impl(expected), timeout=timeout + ) + ) + async def to_have_count( self, count: int, *, timeout: typing.Optional[float] = None ) -> None: diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 619319910..828636efe 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -943,6 +943,10 @@ def handle(route, request): `route.continue_()` will immediately send the request to the network, other matching handlers won't be invoked. Use `route.fallback()` If you want next matching handler in the chain to be invoked. + **NOTE** The `Cookie` header cannot be overridden using this method. If a value is provided, it will be ignored, + and the cookie will be loaded from the browser's cookie store. To set custom cookies, use + `browser_context.add_cookies()`. + Parameters ---------- url : Union[str, None] @@ -9529,8 +9533,8 @@ def handle_route(route: Route): Parameters ---------- url : Union[Callable[[str], bool], Pattern[str], str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context - options was provided and the passed URL is a path, it gets merged via the + A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If `baseURL` is set in + the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. @@ -13245,8 +13249,8 @@ def handle_route(route: Route): Parameters ---------- url : Union[Callable[[str], bool], Pattern[str], str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context - options was provided and the passed URL is a path, it gets merged via the + A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If `baseURL` is set in + the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. @@ -13501,9 +13505,6 @@ def storage_state( state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this. - **NOTE** IndexedDBs with typed arrays are currently not supported. - - Returns ------- {cookies: List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"]}], origins: List[{origin: str, localStorage: List[{name: str, value: str}]}]} @@ -14461,7 +14462,7 @@ def launch( headless : Union[bool, None] Whether to run browser in headless mode. More details for [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the + [Firefox](https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/). Defaults to `true` unless the `devtools` option is `true`. devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the @@ -14588,11 +14589,15 @@ def launch_persistent_context( Parameters ---------- user_data_dir : Union[pathlib.Path, str] - Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for + Path to a User Data Directory, which stores browser session data like cookies and local storage. Pass an empty + string to create a temporary directory. + + More details for [Chromium](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile). Note that Chromium's - user data directory is the **parent** directory of the "Profile Path" seen at `chrome://version`. Pass an empty - string to use a temporary directory instead. + [Firefox](https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile). Chromium's user data directory is the + **parent** directory of the "Profile Path" seen at `chrome://version`. + + Note that browsers do not allow launching multiple instances with the same User Data Directory. channel : Union[str, None] Browser distribution channel. @@ -14626,7 +14631,7 @@ def launch_persistent_context( headless : Union[bool, None] Whether to run browser in headless mode. More details for [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the + [Firefox](https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/). Defaults to `true` unless the `devtools` option is `true`. devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the @@ -15688,8 +15693,8 @@ def evaluate( arg : Union[Any, None] Optional argument to pass to `expression`. timeout : Union[float, None] - Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can - be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, + evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. Returns ------- @@ -15782,8 +15787,8 @@ def evaluate_handle( arg : Union[Any, None] Optional argument to pass to `expression`. timeout : Union[float, None] - Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can - be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, + evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. Returns ------- @@ -17306,7 +17311,12 @@ def screenshot( ) ) - def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str: + def aria_snapshot( + self, + *, + timeout: typing.Optional[float] = None, + ref: typing.Optional[bool] = None, + ) -> str: """Locator.aria_snapshot Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/python/docs/aria-snapshots) and @@ -17351,6 +17361,9 @@ def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str: timeout : Union[float, None] Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + ref : Union[bool, None] + Generate symbolic reference for each element. One can use `aria-ref=` locator immediately after capturing the + snapshot to perform actions on the element. Returns ------- @@ -17358,7 +17371,7 @@ def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str: """ return mapping.from_maybe_impl( - self._sync(self._impl_obj.aria_snapshot(timeout=timeout)) + self._sync(self._impl_obj.aria_snapshot(timeout=timeout, ref=ref)) ) def scroll_into_view_if_needed( @@ -18827,6 +18840,7 @@ def new_context( ] = None, client_certificates: typing.Optional[typing.List[ClientCertificate]] = None, fail_on_status_code: typing.Optional[bool] = None, + max_redirects: typing.Optional[int] = None, ) -> "APIRequestContext": """APIRequest.new_context @@ -18878,6 +18892,10 @@ def new_context( fail_on_status_code : Union[bool, None] Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + 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. This can be overwritten for each request + individually. Returns ------- @@ -18897,6 +18915,7 @@ def new_context( storageState=storage_state, clientCertificates=client_certificates, failOnStatusCode=fail_on_status_code, + maxRedirects=max_redirects, ) ) ) @@ -19278,7 +19297,7 @@ def to_have_class( """LocatorAssertions.to_have_class Ensures the `Locator` points to an element with given CSS classes. When a string is provided, it must fully match - the element's `class` attribute. To match individual classes or perform partial matches, use a regular expression: + the element's `class` attribute. To match individual classes use `locator_assertions.to_contain_class()`. **Usage** @@ -19290,8 +19309,8 @@ def to_have_class( from playwright.sync_api import expect locator = page.locator(\"#component\") - expect(locator).to_have_class(re.compile(r\"(^|\\\\s)selected(\\\\s|$)\")) expect(locator).to_have_class(\"middle selected row\") + expect(locator).to_have_class(re.compile(r\"(^|\\\\s)selected(\\\\s|$)\")) ``` When an array is passed, the method asserts that the list of elements located matches the corresponding list of @@ -19355,6 +19374,96 @@ def not_to_have_class( ) ) + def to_contain_class( + self, + expected: typing.Union[typing.Sequence[str], str], + *, + timeout: typing.Optional[float] = None, + ) -> None: + """LocatorAssertions.to_contain_class + + Ensures the `Locator` points to an element with given CSS classes. All classes from the asserted value, separated + by spaces, must be present in the + [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order. + + **Usage** + + ```html +
+ ``` + + ```py + from playwright.sync_api import expect + + locator = page.locator(\"#component\") + expect(locator).to_contain_class(\"middle selected row\") + expect(locator).to_contain_class(\"selected\") + expect(locator).to_contain_class(\"row middle\") + ``` + + When an array is passed, the method asserts that the list of elements located matches the corresponding list of + expected class lists. Each element's class attribute is matched against the corresponding class in the array: + + ```html +
+
+
+
+ + ``` + + ```py + from playwright.sync_api import expect + + locator = page.locator(\"list > .component\") + await expect(locator).to_contain_class([\"inactive\", \"active\", \"inactive\"]) + ``` + + Parameters + ---------- + expected : Union[Sequence[str], str] + A string containing expected class names, separated by spaces, or a list of such strings to assert multiple + elements. + timeout : Union[float, None] + Time to retry the assertion for in milliseconds. Defaults to `5000`. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.to_contain_class( + expected=mapping.to_impl(expected), timeout=timeout + ) + ) + ) + + def not_to_contain_class( + self, + expected: typing.Union[typing.Sequence[str], str], + *, + timeout: typing.Optional[float] = None, + ) -> None: + """LocatorAssertions.not_to_contain_class + + The opposite of `locator_assertions.to_contain_class()`. + + Parameters + ---------- + expected : Union[Sequence[str], str] + Expected class or RegExp or a list of those. + timeout : Union[float, None] + Time to retry the assertion for in milliseconds. Defaults to `5000`. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.not_to_contain_class( + expected=mapping.to_impl(expected), timeout=timeout + ) + ) + ) + def to_have_count( self, count: int, *, timeout: typing.Optional[float] = None ) -> None: diff --git a/setup.py b/setup.py index 7b32878dd..6f9c7332d 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import zipfile from typing import Dict -driver_version = "1.51.1" +driver_version = "1.52.0" base_wheel_bundles = [ { diff --git a/tests/async/test_accessibility.py b/tests/async/test_accessibility.py index ec7b42190..41fe599c2 100644 --- a/tests/async/test_accessibility.py +++ b/tests/async/test_accessibility.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import sys import pytest @@ -21,8 +20,10 @@ async def test_accessibility_should_work( - page: Page, is_firefox: bool, is_chromium: bool + page: Page, is_firefox: bool, is_chromium: bool, is_webkit: bool ) -> None: + if is_webkit and sys.platform == "darwin": + pytest.skip("Test disabled on WebKit on macOS") await page.set_content( """ Accessibility Test @@ -100,14 +101,7 @@ async def test_accessibility_should_work( {"role": "textbox", "name": "placeholder", "value": "and a value"}, { "role": "textbox", - "name": ( - "placeholder" - if ( - sys.platform == "darwin" - and int(os.uname().release.split(".")[0]) >= 21 - ) - else "This is a description!" - ), + "name": "This is a description!", "value": "and a value", }, # webkit uses the description over placeholder for the name ], diff --git a/tests/async/test_assertions.py b/tests/async/test_assertions.py index 06292aa9b..58f4ea5f5 100644 --- a/tests/async/test_assertions.py +++ b/tests/async/test_assertions.py @@ -145,6 +145,32 @@ async def test_assertions_locator_to_have_class(page: Page, server: Server) -> N await expect(page.locator("div.foobar")).to_have_class("oh-no", timeout=100) +async def test_assertions_locator_to_contain_class(page: Page, server: Server) -> None: + await page.goto(server.EMPTY_PAGE) + await page.set_content("
") + locator = page.locator("div") + await expect(locator).to_contain_class("") + await expect(locator).to_contain_class("bar") + await expect(locator).to_contain_class("baz bar") + await expect(locator).to_contain_class(" bar foo ") + await expect(locator).not_to_contain_class( + " baz not-matching " + ) # Strip whitespace and match individual classes + with pytest.raises(AssertionError) as excinfo: + await expect(locator).to_contain_class("does-not-exist", timeout=100) + + assert excinfo.match("Locator expected to contain class 'does-not-exist'") + assert excinfo.match("Actual value: foo bar baz") + assert excinfo.match("LocatorAssertions.to_contain_class with timeout 100ms") + + await page.set_content( + '
' + ) + await expect(locator).to_contain_class(["foo", "hello", "baz"]) + await expect(locator).not_to_contain_class(["not-there", "hello", "baz"]) + await expect(locator).not_to_contain_class(["foo", "hello"]) + + async def test_assertions_locator_to_have_count(page: Page, server: Server) -> None: await page.goto(server.EMPTY_PAGE) await page.set_content("
kek
kek
") diff --git a/tests/async/test_fetch_global.py b/tests/async/test_fetch_global.py index d37697322..ae394755b 100644 --- a/tests/async/test_fetch_global.py +++ b/tests/async/test_fetch_global.py @@ -524,3 +524,23 @@ async def test_should_not_throw_when_fail_on_status_code_is_false( response = await request.fetch(server.EMPTY_PAGE) assert response.status == 404 await request.dispose() + + +async def test_should_follow_max_redirects( + playwright: Playwright, server: Server +) -> None: + redirect_count = 0 + + def _handle_request(req: TestServerRequest) -> None: + nonlocal redirect_count + redirect_count += 1 + req.setResponseCode(301) + req.setHeader("Location", server.EMPTY_PAGE) + req.finish() + + server.set_route("/empty.html", _handle_request) + request = await playwright.request.new_context(max_redirects=1) + with pytest.raises(Error, match="Max redirect count exceeded"): + await request.fetch(server.EMPTY_PAGE) + assert redirect_count == 2 + await request.dispose() diff --git a/tests/async/test_page_aria_snapshot.py b/tests/async/test_page_aria_snapshot.py index f84440ca4..007d1f56c 100644 --- a/tests/async/test_page_aria_snapshot.py +++ b/tests/async/test_page_aria_snapshot.py @@ -14,6 +14,8 @@ import re +import pytest + from playwright.async_api import Locator, Page, expect @@ -33,7 +35,7 @@ def _unshift(snapshot: str) -> str: async def check_and_match_snapshot(locator: Locator, snapshot: str) -> None: assert await locator.aria_snapshot() == _unshift(snapshot) - await expect(locator).to_match_aria_snapshot(snapshot) + await expect(locator).to_match_aria_snapshot(snapshot, timeout=1000) async def test_should_snapshot(page: Page) -> None: @@ -88,6 +90,128 @@ async def test_should_snapshot_complex(page: Page) -> None: """ - list: - listitem: - - link "link" + - link "link": + - /url: about:blank """, ) + + +async def test_should_snapshot_with_ref(page: Page) -> None: + await page.set_content('') + expected = """ + - list [ref=s1e3]: + - listitem [ref=s1e4]: + - link "link" [ref=s1e5]: + - /url: about:blank + """ + assert await page.locator("body").aria_snapshot(ref=True) == _unshift(expected) + + +async def test_should_snapshot_with_unexpected_children_equal(page: Page) -> None: + await page.set_content( + """ + + """ + ) + await expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - listitem: One + - listitem: Three + """, + ) + with pytest.raises(AssertionError): + await expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - /children: equal + - listitem: One + - listitem: Three + """, + timeout=1000, + ) + + +async def test_should_snapshot_with_unexpected_children_deep_equal(page: Page) -> None: + await page.set_content( + """ + + """ + ) + await expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - listitem: + - list: + - listitem: 1.1 + """, + ) + await expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - /children: equal + - listitem: + - list: + - listitem: 1.1 + """, + ) + with pytest.raises(AssertionError): + await expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - /children: deep-equal + - listitem: + - list: + - listitem: 1.1 + """, + timeout=1000, + ) + + +async def test_should_snapshot_with_restored_contain_mode_inside_deep_equal( + page: Page, +) -> None: + await page.set_content( + """ + + """ + ) + with pytest.raises(AssertionError): + await expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - /children: deep-equal + - listitem: + - list: + - listitem: 1.1 + """, + timeout=1000, + ) + await expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - /children: deep-equal + - listitem: + - list: + - /children: contain + - listitem: 1.1 + """, + ) diff --git a/tests/async/test_page_clock.py b/tests/async/test_page_clock.py index 0676ee581..cbe7740ea 100644 --- a/tests/async/test_page_clock.py +++ b/tests/async/test_page_clock.py @@ -409,7 +409,6 @@ async def test_should_pause(self, page: Page) -> None: await page.goto("data:text/html,") await page.clock.pause_at(1) await page.wait_for_timeout(1000) - await page.clock.resume() now = await page.evaluate("Date.now()") assert 0 <= now <= 1000 diff --git a/tests/async/test_page_evaluate.py b/tests/async/test_page_evaluate.py index 9b7712906..058263b18 100644 --- a/tests/async/test_page_evaluate.py +++ b/tests/async/test_page_evaluate.py @@ -65,6 +65,29 @@ async def test_evaluate_transfer_arrays(page: Page) -> None: assert result == [1, 2, 3] +async def test_evaluate_transfer_typed_arrays(page: Page) -> None: + async def test_typed_array( + typed_array: str, expected: list[float], value_suffix: Optional[str] + ) -> None: + value_suffix = "" if value_suffix is None else value_suffix + result = await page.evaluate( + f"() => new {typed_array}([1{value_suffix}, 2{value_suffix}, 3{value_suffix}])" + ) + assert result == expected + + await test_typed_array("Int8Array", [1, 2, 3], None) + await test_typed_array("Uint8Array", [1, 2, 3], None) + await test_typed_array("Uint8ClampedArray", [1, 2, 3], None) + await test_typed_array("Int16Array", [1, 2, 3], None) + await test_typed_array("Uint16Array", [1, 2, 3], None) + await test_typed_array("Int32Array", [1, 2, 3], None) + await test_typed_array("Uint32Array", [1, 2, 3], None) + await test_typed_array("Float32Array", [1.5, 2.5, 3.5], ".5") + await test_typed_array("Float64Array", [1.5, 2.5, 3.5], ".5") + await test_typed_array("BigInt64Array", [1, 2, 3], "n") + await test_typed_array("BigUint64Array", [1, 2, 3], "n") + + async def test_evaluate_transfer_bigint(page: Page) -> None: assert await page.evaluate("() => 42n") == 42 assert await page.evaluate("a => a", 17) == 17 diff --git a/tests/sync/test_accessibility.py b/tests/sync/test_accessibility.py index 625a46999..10ec5d1b2 100644 --- a/tests/sync/test_accessibility.py +++ b/tests/sync/test_accessibility.py @@ -21,8 +21,10 @@ def test_accessibility_should_work( - page: Page, is_firefox: bool, is_chromium: bool + page: Page, is_firefox: bool, is_chromium: bool, is_webkit: bool ) -> None: + if is_webkit and sys.platform == "darwin": + pytest.skip("Test disabled on WebKit on macOS") page.set_content( """ Accessibility Test diff --git a/tests/sync/test_assertions.py b/tests/sync/test_assertions.py index 6aaffd49b..0dce717d3 100644 --- a/tests/sync/test_assertions.py +++ b/tests/sync/test_assertions.py @@ -124,6 +124,32 @@ def test_assertions_locator_to_have_class(page: Page, server: Server) -> None: expect(page.locator("div.foobar")).to_have_class("oh-no", timeout=100) +def test_assertions_locator_to_contain_class(page: Page, server: Server) -> None: + page.goto(server.EMPTY_PAGE) + page.set_content("
") + locator = page.locator("div") + expect(locator).to_contain_class("") + expect(locator).to_contain_class("bar") + expect(locator).to_contain_class("baz bar") + expect(locator).to_contain_class(" bar foo ") + expect(locator).not_to_contain_class( + " baz not-matching " + ) # Strip whitespace and match individual classes + with pytest.raises(AssertionError) as excinfo: + expect(locator).to_contain_class("does-not-exist", timeout=100) + + assert excinfo.match("Locator expected to contain class 'does-not-exist'") + assert excinfo.match("Actual value: foo bar baz") + assert excinfo.match("LocatorAssertions.to_contain_class with timeout 100ms") + + page.set_content( + '
' + ) + expect(locator).to_contain_class(["foo", "hello", "baz"]) + expect(locator).not_to_contain_class(["not-there", "hello", "baz"]) + expect(locator).not_to_contain_class(["foo", "hello"]) + + def test_assertions_locator_to_have_count(page: Page, server: Server) -> None: page.goto(server.EMPTY_PAGE) page.set_content("
kek
kek
") diff --git a/tests/sync/test_fetch_global.py b/tests/sync/test_fetch_global.py index b7420253b..9efc6e93b 100644 --- a/tests/sync/test_fetch_global.py +++ b/tests/sync/test_fetch_global.py @@ -19,7 +19,7 @@ import pytest from playwright.sync_api import APIResponse, Error, Playwright, StorageState -from tests.server import Server +from tests.server import Server, TestServerRequest @pytest.mark.parametrize( @@ -361,3 +361,21 @@ def test_should_not_throw_when_fail_on_status_code_is_false( response = request.fetch(server.EMPTY_PAGE) assert response.status == 404 request.dispose() + + +def test_should_follow_max_redirects(playwright: Playwright, server: Server) -> None: + redirect_count = 0 + + def _handle_request(req: TestServerRequest) -> None: + nonlocal redirect_count + redirect_count += 1 + req.setResponseCode(301) + req.setHeader("Location", server.EMPTY_PAGE) + req.finish() + + server.set_route("/empty.html", _handle_request) + request = playwright.request.new_context(max_redirects=1) + with pytest.raises(Error, match="Max redirect count exceeded"): + request.fetch(server.EMPTY_PAGE) + assert redirect_count == 2 + request.dispose() diff --git a/tests/sync/test_page_aria_snapshot.py b/tests/sync/test_page_aria_snapshot.py index 481b2bf7a..ca1c48393 100644 --- a/tests/sync/test_page_aria_snapshot.py +++ b/tests/sync/test_page_aria_snapshot.py @@ -14,6 +14,8 @@ import re +import pytest + from playwright.sync_api import Locator, Page, expect @@ -88,6 +90,128 @@ def test_should_snapshot_complex(page: Page) -> None: """ - list: - listitem: - - link "link" + - link "link": + - /url: about:blank """, ) + + +def test_should_snapshot_with_ref(page: Page) -> None: + page.set_content('') + expected = """ + - list [ref=s1e3]: + - listitem [ref=s1e4]: + - link "link" [ref=s1e5]: + - /url: about:blank + """ + assert page.locator("body").aria_snapshot(ref=True) == _unshift(expected) + + +def test_should_snapshot_with_unexpected_children_equal(page: Page) -> None: + page.set_content( + """ + + """ + ) + expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - listitem: One + - listitem: Three + """, + ) + with pytest.raises(AssertionError): + expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - /children: equal + - listitem: One + - listitem: Three + """, + timeout=1000, + ) + + +def test_should_snapshot_with_unexpected_children_deep_equal(page: Page) -> None: + page.set_content( + """ + + """ + ) + expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - listitem: + - list: + - listitem: 1.1 + """, + ) + expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - /children: equal + - listitem: + - list: + - listitem: 1.1 + """, + ) + with pytest.raises(AssertionError): + expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - /children: deep-equal + - listitem: + - list: + - listitem: 1.1 + """, + timeout=1000, + ) + + +def test_should_snapshot_with_restored_contain_mode_inside_deep_equal( + page: Page, +) -> None: + page.set_content( + """ + + """ + ) + with pytest.raises(AssertionError): + expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - /children: deep-equal + - listitem: + - list: + - listitem: 1.1 + """, + timeout=1000, + ) + expect(page.locator("body")).to_match_aria_snapshot( + """ + - list: + - /children: deep-equal + - listitem: + - list: + - /children: contain + - listitem: 1.1 + """, + ) diff --git a/tests/sync/test_page_clock.py b/tests/sync/test_page_clock.py index 025133b57..72d5e5a3e 100644 --- a/tests/sync/test_page_clock.py +++ b/tests/sync/test_page_clock.py @@ -392,7 +392,6 @@ def test_should_pause(self, page: Page) -> None: page.goto("data:text/html,") page.clock.pause_at(1) page.wait_for_timeout(1000) - page.clock.resume() now = page.evaluate("Date.now()") assert 0 <= now <= 1000