From 74e217770e62de63bdc16f1c383d850ba2e6dd28 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 28 Apr 2025 05:41:15 -0700 Subject: [PATCH 01/84] chore(roll): roll Playwright to 1.52.0 (omitting glob changes) (#2823) --- README.md | 4 +- playwright/_impl/_assertions.py | 39 +++++++ playwright/_impl/_fetch.py | 1 + playwright/_impl/_js_handle.py | 52 +++++++++ playwright/_impl/_locator.py | 2 +- playwright/async_api/_generated.py | 147 ++++++++++++++++++++---- playwright/sync_api/_generated.py | 151 +++++++++++++++++++++---- setup.py | 2 +- tests/async/test_accessibility.py | 14 +-- tests/async/test_assertions.py | 26 +++++ tests/async/test_fetch_global.py | 20 ++++ tests/async/test_page_aria_snapshot.py | 128 ++++++++++++++++++++- tests/async/test_page_clock.py | 1 - tests/async/test_page_evaluate.py | 23 ++++ tests/sync/test_accessibility.py | 4 +- tests/sync/test_assertions.py | 26 +++++ tests/sync/test_fetch_global.py | 20 +++- tests/sync/test_page_aria_snapshot.py | 126 ++++++++++++++++++++- tests/sync/test_page_clock.py | 1 - 19 files changed, 724 insertions(+), 63 deletions(-) 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( + """ +
    +
  • One
  • +
  • Two
  • +
  • Three
  • +
+ """ + ) + 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( + """ +
    +
  • +
      +
    • 1.1
    • +
    • 1.2
    • +
    +
  • +
+ """ + ) + 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( + """ +
    +
  • +
      +
    • 1.1
    • +
    • 1.2
    • +
    +
  • +
+ """ + ) + 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( + """ +
    +
  • One
  • +
  • Two
  • +
  • Three
  • +
+ """ + ) + 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( + """ +
    +
  • +
      +
    • 1.1
    • +
    • 1.2
    • +
    +
  • +
+ """ + ) + 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( + """ +
    +
  • +
      +
    • 1.1
    • +
    • 1.2
    • +
    +
  • +
+ """ + ) + 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 From 353c9d5f00e24be30726d37bdbdb0a2aca2ecbb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:05:49 +0200 Subject: [PATCH 02/84] build(deps): bump pytest-cov from 6.0.0 to 6.1.1 (#2811) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index f0afc5355..36a5a8512 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -10,7 +10,7 @@ pre-commit==3.5.0 pyOpenSSL==25.0.0 pytest==8.3.5 pytest-asyncio==0.26.0 -pytest-cov==6.0.0 +pytest-cov==6.1.1 pytest-repeat==0.9.3 pytest-rerunfailures==15.0 pytest-timeout==2.3.1 From 10e9ea3d5ff72df99fcd6ac5a1b3af1e069962fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:06:15 +0200 Subject: [PATCH 03/84] build(deps): bump typing-extensions from 4.12.2 to 4.13.2 (#2817) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5298f1ff4..850fc18c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ greenlet==3.1.1 # via playwright (pyproject.toml) pyee==13.0.0 # via playwright (pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.13.2 # via pyee From 805147f896c476889046b3d0836382465cfb572d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:06:29 +0200 Subject: [PATCH 04/84] build(deps): bump actions/create-github-app-token from 1 to 2 in the actions group (#2809) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/trigger_internal_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trigger_internal_tests.yml b/.github/workflows/trigger_internal_tests.yml index b301a7b6e..2bbdeb565 100644 --- a/.github/workflows/trigger_internal_tests.yml +++ b/.github/workflows/trigger_internal_tests.yml @@ -11,7 +11,7 @@ jobs: name: "trigger" runs-on: ubuntu-24.04 steps: - - uses: actions/create-github-app-token@v1 + - uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.PLAYWRIGHT_APP_ID }} From dc525e708f644589eacd26cbfaeb14d97593d95a Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 28 Apr 2025 15:54:45 +0200 Subject: [PATCH 05/84] chore: adjust license metadata in pyproject.toml (#2828) --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 52ed67370..3c90282a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ {name = "Microsoft Corporation"} ] readme = "README.md" -license = {text = "Apache-2.0"} +license = "Apache-2.0" dynamic = ["version"] requires-python = ">=3.9" # Please when changing dependencies run the following commands to update requirements.txt: @@ -29,7 +29,6 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", ] From 0e23e33a4199923be40f02c3067c67cbcb3cfac5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:18:10 +0200 Subject: [PATCH 06/84] build(deps): bump pytest-repeat from 0.9.3 to 0.9.4 (#2812) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 36a5a8512..0506bd86d 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -11,7 +11,7 @@ pyOpenSSL==25.0.0 pytest==8.3.5 pytest-asyncio==0.26.0 pytest-cov==6.1.1 -pytest-repeat==0.9.3 +pytest-repeat==0.9.4 pytest-rerunfailures==15.0 pytest-timeout==2.3.1 pytest-xdist==3.6.1 From ec92f20d56e4f5a4acfb3be26bc20a5dafa8d068 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:18:32 +0200 Subject: [PATCH 07/84] build(deps): bump types-requests from 2.32.0.20250306 to 2.32.0.20250328 (#2807) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 0506bd86d..c7dac72c7 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -19,4 +19,4 @@ requests==2.32.3 service_identity==24.2.0 twisted==24.11.0 types-pyOpenSSL==24.1.0.20240722 -types-requests==2.32.0.20250306 +types-requests==2.32.0.20250328 From f5857df8a3bfdab8182e4261476f557a531a5fb4 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 28 Apr 2025 16:25:16 +0200 Subject: [PATCH 08/84] chore: publish win32-arm64 wheel (#2800) --- setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup.py b/setup.py index 6f9c7332d..ed46af1aa 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,12 @@ "platform": "win32", "zip_name": "win32_x64", }, + { + "wheel": "win_arm64.whl", + "machine": "arm64", + "platform": "win32", + "zip_name": "win32_arm64", + }, ] if len(sys.argv) == 2 and sys.argv[1] == "--list-wheels": From eec856f5d1f3e0f25565b5573c6596558ed94418 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 28 Apr 2025 08:43:20 -0700 Subject: [PATCH 09/84] chore(roll): roll glob changes from Playwright 1.52.0 (#2824) --- playwright/_impl/_glob.py | 20 ++-- playwright/_impl/_helper.py | 106 ++++++++++++++++---- playwright/_impl/_network.py | 2 +- tests/async/test_page_route.py | 136 +++++++++++++++++++++++--- tests/async/test_request_intercept.py | 14 +++ tests/sync/test_request_intercept.py | 14 +++ 6 files changed, 248 insertions(+), 44 deletions(-) diff --git a/playwright/_impl/_glob.py b/playwright/_impl/_glob.py index 2d899a789..08b7ce466 100644 --- a/playwright/_impl/_glob.py +++ b/playwright/_impl/_glob.py @@ -11,13 +11,12 @@ # 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 re # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping escaped_chars = {"$", "^", "+", ".", "*", "(", ")", "|", "\\", "?", "{", "}", "[", "]"} -def glob_to_regex(glob: str) -> "re.Pattern[str]": +def glob_to_regex_pattern(glob: str) -> str: tokens = ["^"] in_group = False @@ -46,23 +45,20 @@ def glob_to_regex(glob: str) -> "re.Pattern[str]": else: tokens.append("([^/]*)") else: - if c == "?": - tokens.append(".") - elif c == "[": - tokens.append("[") - elif c == "]": - tokens.append("]") - elif c == "{": + if c == "{": in_group = True tokens.append("(") elif c == "}": in_group = False tokens.append(")") - elif c == "," and in_group: - tokens.append("|") + elif c == ",": + if in_group: + tokens.append("|") + else: + tokens.append("\\" + c) else: tokens.append("\\" + c if c in escaped_chars else c) i += 1 tokens.append("$") - return re.compile("".join(tokens)) + return "".join(tokens) diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 2f7ab57b0..96acb8857 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -44,7 +44,7 @@ is_target_closed_error, rewrite_error, ) -from playwright._impl._glob import glob_to_regex +from playwright._impl._glob import glob_to_regex_pattern from playwright._impl._greenlets import RouteGreenlet from playwright._impl._str_utils import escape_regex_flags @@ -144,31 +144,103 @@ class FrameNavigatedEvent(TypedDict): def url_matches( - base_url: Optional[str], url_string: str, match: Optional[URLMatch] + base_url: Optional[str], + url_string: str, + match: Optional[URLMatch], + websocket_url: bool = None, ) -> bool: if not match: return True - if isinstance(match, str) and match[0] != "*": - # Allow http(s) baseURL to match ws(s) urls. - if ( - base_url - and re.match(r"^https?://", base_url) - and re.match(r"^wss?://", url_string) - ): - base_url = re.sub(r"^http", "ws", base_url) - if base_url: - match = urljoin(base_url, match) - parsed = urlparse(match) - if parsed.path == "": - parsed = parsed._replace(path="/") - match = parsed.geturl() if isinstance(match, str): - match = glob_to_regex(match) + match = re.compile( + resolve_glob_to_regex_pattern(base_url, match, websocket_url) + ) if isinstance(match, Pattern): return bool(match.search(url_string)) return match(url_string) +def resolve_glob_to_regex_pattern( + base_url: Optional[str], glob: str, websocket_url: bool = None +) -> str: + if websocket_url: + base_url = to_websocket_base_url(base_url) + glob = resolve_glob_base(base_url, glob) + return glob_to_regex_pattern(glob) + + +def to_websocket_base_url(base_url: Optional[str]) -> Optional[str]: + if base_url is not None and re.match(r"^https?://", base_url): + base_url = re.sub(r"^http", "ws", base_url) + return base_url + + +def resolve_glob_base(base_url: Optional[str], match: str) -> str: + if match[0] == "*": + return match + + token_map: Dict[str, str] = {} + + def map_token(original: str, replacement: str) -> str: + if len(original) == 0: + return "" + token_map[replacement] = original + return replacement + + # Escaped `\\?` behaves the same as `?` in our glob patterns. + match = match.replace(r"\\?", "?") + # Glob symbols may be escaped in the URL and some of them such as ? affect resolution, + # so we replace them with safe components first. + processed_parts = [] + for index, token in enumerate(match.split("/")): + if token in (".", "..", ""): + processed_parts.append(token) + continue + # Handle special case of http*://, note that the new schema has to be + # a web schema so that slashes are properly inserted after domain. + if index == 0 and token.endswith(":"): + # Using a simple replacement for the scheme part + processed_parts.append(map_token(token, "http:")) + continue + question_index = token.find("?") + if question_index == -1: + processed_parts.append(map_token(token, f"$_{index}_$")) + else: + new_prefix = map_token(token[:question_index], f"$_{index}_$") + new_suffix = map_token(token[question_index:], f"?$_{index}_$") + processed_parts.append(new_prefix + new_suffix) + + relative_path = "/".join(processed_parts) + resolved_url = urljoin(base_url if base_url is not None else "", relative_path) + + for replacement, original in token_map.items(): + resolved_url = resolved_url.replace(replacement, original, 1) + + return ensure_trailing_slash(resolved_url) + + +# In Node.js, new URL('http://localhost') returns 'http://localhost/'. +# To ensure the same url matching behavior, do the same. +def ensure_trailing_slash(url: str) -> str: + split = url.split("://", maxsplit=1) + if len(split) == 2: + # URL parser doesn't like strange/unknown schemes, so we replace it for parsing, then put it back + parsable_url = "http://" + split[1] + else: + # Given current rules, this should never happen _and_ still be a valid matcher. We require the protocol to be part of the match, + # so either the user is using a glob that starts with "*" (and none of this code is running), or the user actually has `something://` in `match` + parsable_url = url + parsed = urlparse(parsable_url, allow_fragments=True) + if len(split) == 2: + # Replace the scheme that we removed earlier + parsed = parsed._replace(scheme=split[0]) + if parsed.path == "": + parsed = parsed._replace(path="/") + url = parsed.geturl() + + return url + + class HarLookupResult(TypedDict, total=False): action: Literal["error", "redirect", "fulfill", "noentry"] message: Optional[str] diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 4b15531af..6492c4311 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -754,7 +754,7 @@ def prepare_interception_patterns( return patterns def matches(self, ws_url: str) -> bool: - return url_matches(self._base_url, ws_url, self.url) + return url_matches(self._base_url, ws_url, self.url, True) async def handle(self, websocket_route: "WebSocketRoute") -> None: coro_or_future = self.handler(websocket_route) diff --git a/tests/async/test_page_route.py b/tests/async/test_page_route.py index 017bdac9a..b04f96145 100644 --- a/tests/async/test_page_route.py +++ b/tests/async/test_page_route.py @@ -20,7 +20,8 @@ import pytest -from playwright._impl._glob import glob_to_regex +from playwright._impl._glob import glob_to_regex_pattern +from playwright._impl._helper import url_matches from playwright.async_api import ( Browser, BrowserContext, @@ -29,6 +30,7 @@ Playwright, Request, Route, + expect, ) from tests.server import Server, TestServerRequest from tests.utils import must @@ -1051,17 +1053,19 @@ async def handle_request(route: Route) -> None: assert await response.json() == {"foo": "bar"} -async def test_glob_to_regex() -> None: +async def test_should_work_with_glob() -> None: + def glob_to_regex(pattern: str) -> re.Pattern: + return re.compile(glob_to_regex_pattern(pattern)) + assert glob_to_regex("**/*.js").match("https://localhost:8080/foo.js") assert not glob_to_regex("**/*.css").match("https://localhost:8080/foo.js") - assert not glob_to_regex("*.js").match("https://localhost:8080/foo.js") + assert not glob_to_regex("*.js").match( + "https://localhost:8080/foo.js" + ) # Doesn"t match path separator assert glob_to_regex("https://**/*.js").match("https://localhost:8080/foo.js") assert glob_to_regex("http://localhost:8080/simple/path.js").match( "http://localhost:8080/simple/path.js" ) - assert glob_to_regex("http://localhost:8080/?imple/path.js").match( - "http://localhost:8080/Simple/path.js" - ) assert glob_to_regex("**/{a,b}.js").match("https://localhost:8080/a.js") assert glob_to_regex("**/{a,b}.js").match("https://localhost:8080/b.js") assert not glob_to_regex("**/{a,b}.js").match("https://localhost:8080/c.js") @@ -1081,15 +1085,119 @@ async def test_glob_to_regex() -> None: "http://localhost:3000/signin-oidcnice" ) - assert glob_to_regex("**/three-columns/settings.html?**id=[a-z]**").match( + # range [] is NOT supported + assert glob_to_regex("**/api/v[0-9]").fullmatch("http://example.com/api/v[0-9]") + assert not glob_to_regex("**/api/v[0-9]").fullmatch( + "http://example.com/api/version" + ) + assert not glob_to_regex("**/api/v[0-9]").fullmatch( + "http://example.com/api/v1" + ) # Should not match if [] is literal + + # query params + assert glob_to_regex("**/api\\?param").match("http://example.com/api?param") + assert not glob_to_regex("**/api\\?param").match("http://example.com/api-param") + + assert glob_to_regex("**/three-columns/settings.html\\?**id=settings-**").match( "http://mydomain:8080/blah/blah/three-columns/settings.html?id=settings-e3c58efe-02e9-44b0-97ac-dd138100cf7c&blah" ) - assert glob_to_regex("\\?") == re.compile(r"^\?$") - assert glob_to_regex("\\") == re.compile(r"^\\$") - assert glob_to_regex("\\\\") == re.compile(r"^\\$") - assert glob_to_regex("\\[") == re.compile(r"^\[$") - assert glob_to_regex("[a-z]") == re.compile(r"^[a-z]$") - assert glob_to_regex("$^+.\\*()|\\?\\{\\}\\[\\]") == re.compile( - r"^\$\^\+\.\*\(\)\|\?\{\}\[\]$" + assert glob_to_regex("\\?").pattern == r"^\?$" + assert glob_to_regex("\\").pattern == r"^\\$" + assert glob_to_regex("\\\\").pattern == r"^\\$" + assert glob_to_regex("\\[").pattern == r"^\[$" + assert glob_to_regex("[a-z]").pattern == r"^\[a-z\]$" + assert ( + glob_to_regex("$^+.\\*()|\\?\\{\\}\\[\\]").pattern + == r"^\$\^\+\.\*\(\)\|\?\{\}\[\]$" + ) + + # --- url_matches tests --- + # Basic exact and wildcard matching + assert url_matches(None, "http://playwright.dev/", "http://playwright.dev") + assert url_matches(None, "http://playwright.dev/?a=b", "http://playwright.dev?a=b") + assert url_matches(None, "http://playwright.dev/", "h*://playwright.dev") + assert url_matches( + None, "http://api.playwright.dev/?x=y", "http://*.playwright.dev?x=y" + ) + assert url_matches(None, "http://playwright.dev/foo/bar", "**/foo/**") + + # Relative path matching with base URL + assert url_matches("http://playwright.dev", "http://playwright.dev/?x=y", "?x=y") + assert url_matches( + "http://playwright.dev/foo/", "http://playwright.dev/foo/bar?x=y", "./bar?x=y" + ) + + # This is not supported, we treat ? as a query separator. + assert not url_matches( + None, + "http://localhost:8080/Simple/path.js", + "http://localhost:8080/?imple/path.js", + ) + assert not url_matches(None, "http://playwright.dev/", "http://playwright.?ev") + assert url_matches(None, "http://playwright./?ev", "http://playwright.?ev") + assert not url_matches( + None, "http://playwright.dev/foo", "http://playwright.dev/f??" + ) + assert url_matches(None, "http://playwright.dev/f??", "http://playwright.dev/f??") + assert url_matches( + None, "http://playwright.dev/?x=y", r"http://playwright.dev\?x=y" + ) + assert url_matches( + None, "http://playwright.dev/?x=y", r"http://playwright.dev/\?x=y" + ) + assert url_matches( + "http://playwright.dev/foo", "http://playwright.dev/foo?bar", "?bar" + ) + assert url_matches( + "http://playwright.dev/foo", "http://playwright.dev/foo?bar", r"\\?bar" + ) + assert url_matches("http://first.host/", "http://second.host/foo", "**/foo") + assert url_matches("http://playwright.dev/", "http://localhost/", "*//localhost/") + + # Added for Python implementation + assert url_matches( + None, + "custom://example.com/foo/bar?id=123", + "{custom,another}://example.com/foo/bar?id=123", + ) + assert not url_matches( + None, "custom://example.com/foo/bar?id=123", "**example.com/foo/bar?id=123" ) + + +async def test_should_not_support_question_in_glob_pattern( + page: Page, playwright: Playwright, server: Server +) -> None: + server.set_route("/index", lambda req: (req.write(b"index-no-hello"), req.finish())) + server.set_route( + "/index123hello", lambda req: (req.write(b"index123hello"), req.finish()) + ) + server.set_route( + "/index?hello", lambda req: (req.write(b"index?hello"), req.finish()) + ) + server.set_route( + "/index1hello", lambda req: (req.write(b"index1hello"), req.finish()) + ) + + async def handle_any_char(route: Route) -> None: + await route.fulfill(body="intercepted any character") + + await page.route("**/index?hello", handle_any_char) + + async def handle_question_mark(route: Route) -> None: + await route.fulfill(body="intercepted question mark") + + await page.route(r"**/index\?hello", handle_question_mark) + + await page.goto(server.PREFIX + "/index?hello") + await expect(page.locator("body")).to_have_text("intercepted question mark") + + await page.goto(server.PREFIX + "/index") + await expect(page.locator("body")).to_have_text("index-no-hello") + + await page.goto(server.PREFIX + "/index1hello") + await expect(page.locator("body")).to_have_text("index1hello") + + await page.goto(server.PREFIX + "/index123hello") + await expect(page.locator("body")).to_have_text("index123hello") diff --git a/tests/async/test_request_intercept.py b/tests/async/test_request_intercept.py index 316e0b102..75746bbca 100644 --- a/tests/async/test_request_intercept.py +++ b/tests/async/test_request_intercept.py @@ -175,3 +175,17 @@ async def test_should_give_access_to_the_intercepted_response_body( route.fulfill(response=response), eval_task, ) + + +async def test_should_intercept_by_glob(page: Page, server: Server) -> None: + await page.goto(server.EMPTY_PAGE) + await page.route( + "http://localhos**?*oo", + lambda route: route.fulfill(body="intercepted", status=200), + ) + + result = await page.evaluate( + "url => fetch(url).then(r => r.text())", server.PREFIX + "/?foo" + ) + + assert result == "intercepted" diff --git a/tests/sync/test_request_intercept.py b/tests/sync/test_request_intercept.py index 8df41c0c2..a54c0ad71 100644 --- a/tests/sync/test_request_intercept.py +++ b/tests/sync/test_request_intercept.py @@ -131,3 +131,17 @@ def handle_route(route: Route) -> None: assert request.uri.decode() == "/title.html" original = (assetdir / "title.html").read_text() assert response.text() == original + + +def test_should_intercept_by_glob(page: Page, server: Server) -> None: + page.goto(server.EMPTY_PAGE) + page.route( + "http://localhos**?*oo", + lambda route: route.fulfill(body="intercepted", status=200), + ) + + result = page.evaluate( + "url => fetch(url).then(r => r.text())", server.PREFIX + "/?foo" + ) + + assert result == "intercepted" From 02187b2c9140aeb14ff39a4d74cdde008cac2e30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:30:21 +0200 Subject: [PATCH 10/84] build(deps): bump greenlet from 3.1.1 to 3.2.1 (#2832) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 850fc18c9..6c5b7b1c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile pyproject.toml -o requirements.txt -greenlet==3.1.1 +greenlet==3.2.1 # via playwright (pyproject.toml) pyee==13.0.0 # via playwright (pyproject.toml) From 6eda6337776954dfb32f22970414cab513fa0abe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:30:35 +0200 Subject: [PATCH 11/84] build(deps): bump pillow from 11.1.0 to 11.2.1 (#2833) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index c7dac72c7..b11ba7896 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -4,7 +4,7 @@ build==1.2.2.post1 flake8==7.2.0 mypy==1.15.0 objgraph==3.6.2 -Pillow==11.1.0 +Pillow==11.2.1 pixelmatch==0.3.0 pre-commit==3.5.0 pyOpenSSL==25.0.0 From ecd6af8e94be41fb9d880dd087d37221c27e32a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:30:52 +0200 Subject: [PATCH 12/84] build(deps): bump setuptools-scm from 8.2.0 to 8.3.1 (#2831) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3c90282a0..42278b93e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==78.1.0", "setuptools-scm==8.2.0", "wheel==0.45.1", "auditwheel==6.2.0"] +requires = ["setuptools==78.1.0", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] build-backend = "setuptools.build_meta" [project] From 7f5db36853c225a9ea670dce94f73018b1f0a660 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:52:01 +0200 Subject: [PATCH 13/84] build(deps): bump setuptools from 78.1.0 to 80.0.0 (#2830) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 42278b93e..1b76f8759 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==78.1.0", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] +requires = ["setuptools==80.0.1", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] build-backend = "setuptools.build_meta" [project] From fe886f72a316040faf69aa22c5caf899398293f6 Mon Sep 17 00:00:00 2001 From: Eli Black Date: Wed, 30 Apr 2025 20:55:56 +0800 Subject: [PATCH 14/84] chore(perf): don't request inspect context when inspecting stack (#2835) --- playwright/_impl/_connection.py | 16 ++++++++-------- tests/async/test_asyncio.py | 12 ++++++++++++ tests/sync/test_sync.py | 12 ++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 027daf69d..2d1dad933 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -362,12 +362,7 @@ def _send_message_to_server( "params": self._replace_channels_with_guids(params), "metadata": metadata, } - if ( - self._tracing_count > 0 - and frames - and frames - and object._guid != "localUtils" - ): + if self._tracing_count > 0 and frames and object._guid != "localUtils": self.local_utils.add_stack_to_tracing_no_reply(id, frames) self._transport.send(message) @@ -519,7 +514,10 @@ async def wrap_api_call( if self._api_zone.get(): return await cb() task = asyncio.current_task(self._loop) - st: List[inspect.FrameInfo] = getattr(task, "__pw_stack__", inspect.stack()) + st: List[inspect.FrameInfo] = getattr( + task, "__pw_stack__", None + ) or inspect.stack(0) + parsed_st = _extract_stack_trace_information_from_stack(st, is_internal) self._api_zone.set(parsed_st) try: @@ -535,7 +533,9 @@ def wrap_api_call_sync( if self._api_zone.get(): return cb() task = asyncio.current_task(self._loop) - st: List[inspect.FrameInfo] = getattr(task, "__pw_stack__", inspect.stack()) + st: List[inspect.FrameInfo] = getattr( + task, "__pw_stack__", None + ) or inspect.stack(0) parsed_st = _extract_stack_trace_information_from_stack(st, is_internal) self._api_zone.set(parsed_st) try: diff --git a/tests/async/test_asyncio.py b/tests/async/test_asyncio.py index 33edc71ce..971c65473 100644 --- a/tests/async/test_asyncio.py +++ b/tests/async/test_asyncio.py @@ -87,3 +87,15 @@ async def raise_exception() -> None: assert "Something went wrong" in str(exc_info.value.exceptions[0]) assert isinstance(exc_info.value.exceptions[0], ValueError) assert await page.evaluate("() => 11 * 11") == 121 + + +async def test_should_return_proper_api_name_on_error(page: Page) -> None: + try: + await page.evaluate("does_not_exist") + + assert ( + False + ), "Accessing undefined JavaScript variable should have thrown exception" + except Exception as error: + # Each browser returns slightly different error messages, but they should all start with "Page.evaluate:", because that was the Playwright method where the error originated + assert str(error).startswith("Page.evaluate:") diff --git a/tests/sync/test_sync.py b/tests/sync/test_sync.py index 64eace1e9..92d40c19a 100644 --- a/tests/sync/test_sync.py +++ b/tests/sync/test_sync.py @@ -346,3 +346,15 @@ def test_call_sync_method_after_playwright_close_with_own_loop( p.start() p.join() assert p.exitcode == 0 + + +def test_should_return_proper_api_name_on_error(page: Page) -> None: + try: + page.evaluate("does_not_exist") + + assert ( + False + ), "Accessing undefined JavaScript variable should have thrown exception" + except Exception as error: + # Each browser returns slightly different error messages, but they should all start with "Page.evaluate:", because that was the Playwright method where the error originated + assert str(error).startswith("Page.evaluate:") From e6c5ba4fe2342e73ec85f2cdf8ef80681bbdcc01 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 1 May 2025 19:22:12 +0200 Subject: [PATCH 15/84] devops: add linux-arm64 Docker tests (#2837) --- .github/workflows/test_docker.yml | 11 +++++--- .github/workflows/trigger_internal_tests.yml | 27 -------------------- 2 files changed, 8 insertions(+), 30 deletions(-) delete mode 100644 .github/workflows/trigger_internal_tests.yml diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index 7f0ca3088..c1f2be3de 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -19,13 +19,16 @@ on: jobs: build: timeout-minutes: 120 - runs-on: ubuntu-24.04 + runs-on: ${{ matrix.runs-on }} strategy: fail-fast: false matrix: docker-image-variant: - jammy - noble + runs-on: + - ubuntu-24.04 + - ubuntu-24.04-arm steps: - uses: actions/checkout@v4 - name: Set up Python @@ -39,10 +42,12 @@ jobs: pip install -r requirements.txt pip install -e . - name: Build Docker image - run: bash utils/docker/build.sh --amd64 ${{ matrix.docker-image-variant }} playwright-python:localbuild-${{ matrix.docker-image-variant }} + run: | + ARCH="${{ matrix.runs-on == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }}" + bash utils/docker/build.sh --$ARCH ${{ matrix.docker-image-variant }} playwright-python:localbuild-${{ matrix.docker-image-variant }} - name: Test run: | - CONTAINER_ID="$(docker run --rm -v $(pwd):/root/playwright --name playwright-docker-test --workdir /root/playwright/ -d -t playwright-python:localbuild-${{ matrix.docker-image-variant }} /bin/bash)" + CONTAINER_ID="$(docker run --rm -e CI -v $(pwd):/root/playwright --name playwright-docker-test --workdir /root/playwright/ -d -t playwright-python:localbuild-${{ matrix.docker-image-variant }} /bin/bash)" # Fix permissions for Git inside the container docker exec "${CONTAINER_ID}" chown -R root:root /root/playwright docker exec "${CONTAINER_ID}" pip install -r local-requirements.txt diff --git a/.github/workflows/trigger_internal_tests.yml b/.github/workflows/trigger_internal_tests.yml deleted file mode 100644 index 2bbdeb565..000000000 --- a/.github/workflows/trigger_internal_tests.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: "Internal Tests" - -on: - push: - branches: - - main - - release-* - -jobs: - trigger: - name: "trigger" - runs-on: ubuntu-24.04 - steps: - - uses: actions/create-github-app-token@v2 - id: app-token - with: - app-id: ${{ vars.PLAYWRIGHT_APP_ID }} - private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }} - repositories: playwright-browsers - - run: | - curl -X POST --fail \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${GH_TOKEN}" \ - --data "{\"event_type\": \"playwright_tests_python\", \"client_payload\": {\"ref\": \"${GITHUB_SHA}\"}}" \ - https://api.github.com/repos/microsoft/playwright-browsers/dispatches - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} From 85a57cdf54c7c843d27256980cf2f3a9ad4d5bc4 Mon Sep 17 00:00:00 2001 From: Eli Black Date: Fri, 2 May 2025 23:34:32 +0800 Subject: [PATCH 16/84] chore: use tmp file when downloading driver (#2838) --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ed46af1aa..abe2fd6e2 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,8 @@ def extractall(zip: zipfile.ZipFile, path: str) -> None: def download_driver(zip_name: str) -> None: zip_file = f"playwright-{driver_version}-{zip_name}.zip" - if os.path.exists("driver/" + zip_file): + destination_path = "driver/" + zip_file + if os.path.exists(destination_path): return url = "https://playwright.azureedge.net/builds/driver/" if ( @@ -109,9 +110,11 @@ def download_driver(zip_name: str) -> None: ): url = url + "next/" url = url + zip_file + temp_destination_path = destination_path + ".tmp" print(f"Fetching {url}") # Don't replace this with urllib - Python won't have certificates to do SSL on all platforms. - subprocess.check_call(["curl", url, "-o", "driver/" + zip_file]) + subprocess.check_call(["curl", url, "-o", temp_destination_path]) + os.rename(temp_destination_path, destination_path) class PlaywrightBDistWheelCommand(BDistWheelCommand): From 9ab4db2df90af654b4762c3c37ddc73f70e62a61 Mon Sep 17 00:00:00 2001 From: Eli Black Date: Sun, 4 May 2025 12:19:28 +0800 Subject: [PATCH 17/84] chore: fix type warnings on windows (#2840) --- playwright/_impl/_browser_type.py | 2 +- tests/common/test_signals.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index b34d224d6..bedc5ea73 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -171,7 +171,7 @@ def _user_data_dir(self, userDataDir: Optional[Union[str, Path]]) -> str: # Can be dropped once we drop Python 3.9 support (10/2025): # https://github.com/python/cpython/issues/82852 if sys.platform == "win32" and sys.version_info[:2] < (3, 10): - return pathlib.Path.cwd() / userDataDir + return str(pathlib.Path.cwd() / userDataDir) return str(Path(userDataDir).resolve()) return str(Path(userDataDir)) diff --git a/tests/common/test_signals.py b/tests/common/test_signals.py index 472e74042..174eaf6f2 100644 --- a/tests/common/test_signals.py +++ b/tests/common/test_signals.py @@ -27,6 +27,10 @@ def _test_signals_async( browser_name: str, launch_arguments: Dict, wait_queue: "multiprocessing.Queue[str]" ) -> None: + # On Windows, hint to mypy and pyright that they shouldn't check this function + if sys.platform == "win32": + return + os.setpgrp() sigint_received = False @@ -67,6 +71,10 @@ async def main() -> None: def _test_signals_sync( browser_name: str, launch_arguments: Dict, wait_queue: "multiprocessing.Queue[str]" ) -> None: + # On Windows, hint to mypy and pyright that they shouldn't check this function + if sys.platform == "win32": + return + os.setpgrp() sigint_received = False @@ -103,6 +111,10 @@ def my_sig_handler(signum: int, frame: Any) -> None: def _create_signals_test( target: Any, browser_name: str, launch_arguments: Dict ) -> None: + # On Windows, hint to mypy and pyright that they shouldn't check this function + if sys.platform == "win32": + return + wait_queue: "multiprocessing.Queue[str]" = multiprocessing.Queue() process = multiprocessing.Process( target=target, args=[browser_name, launch_arguments, wait_queue] From 2ca8764621f4a68a9fab0875ee8513f03c708094 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 13:46:12 +0200 Subject: [PATCH 18/84] build(deps): bump setuptools from 80.0.1 to 80.3.1 (#2843) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b76f8759..2dc0a7133 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.0.1", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] +requires = ["setuptools==80.3.1", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] build-backend = "setuptools.build_meta" [project] From 871149c7532a495f8bd6c2648c45f42a4e1e980d Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 12 May 2025 16:02:04 +0200 Subject: [PATCH 19/84] test: unflake test_context_cookies_should_work test in Edge (#2847) --- tests/async/test_defaultbrowsercontext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py index 60f8d83fd..cc42a9c33 100644 --- a/tests/async/test_defaultbrowsercontext.py +++ b/tests/async/test_defaultbrowsercontext.py @@ -78,7 +78,7 @@ async def test_context_cookies_should_work( ) assert document_cookie == "username=John Doe" - assert await page.context.cookies() == [ + assert _filter_cookies(await page.context.cookies()) == [ { "name": "username", "value": "John Doe", From 55b28001dff23c148c68c9f8d9fdc68326a52c4a Mon Sep 17 00:00:00 2001 From: Eli Black Date: Tue, 13 May 2025 01:55:03 +0800 Subject: [PATCH 20/84] chore: more stack-related performance improvements (#2844) --- playwright/_impl/_connection.py | 6 ++---- playwright/_impl/_network.py | 2 +- playwright/_impl/_path_utils.py | 6 ++++-- playwright/_impl/_sync_base.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 2d1dad933..1328e7c97 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -333,7 +333,7 @@ def _send_message_to_server( task = asyncio.current_task(self._loop) callback.stack_trace = cast( traceback.StackSummary, - getattr(task, "__pw_stack_trace__", traceback.extract_stack()), + getattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)), ) callback.no_reply = no_reply self._callbacks[id] = callback @@ -387,9 +387,7 @@ def dispatch(self, msg: ParsedMessagePayload) -> None: parsed_error = parse_error( error["error"], format_call_log(msg.get("log")) # type: ignore ) - parsed_error._stack = "".join( - traceback.format_list(callback.stack_trace)[-10:] - ) + parsed_error._stack = "".join(callback.stack_trace.format()) callback.future.set_exception(parsed_error) else: result = self._replace_guids_with_channels(msg.get("result")) diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 6492c4311..768c22f0c 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -530,7 +530,7 @@ async def _race_with_page_close(self, future: Coroutine) -> None: setattr( fut, "__pw_stack__", - getattr(asyncio.current_task(self._loop), "__pw_stack__", inspect.stack()), + getattr(asyncio.current_task(self._loop), "__pw_stack__", inspect.stack(0)), ) target_closed_future = self.request._target_closed_future() await asyncio.wait( diff --git a/playwright/_impl/_path_utils.py b/playwright/_impl/_path_utils.py index 267a82ab0..b405a0675 100644 --- a/playwright/_impl/_path_utils.py +++ b/playwright/_impl/_path_utils.py @@ -14,12 +14,14 @@ import inspect from pathlib import Path +from types import FrameType +from typing import cast def get_file_dirname() -> Path: """Returns the callee (`__file__`) directory name""" - frame = inspect.stack()[1] - module = inspect.getmodule(frame[0]) + frame = cast(FrameType, inspect.currentframe()).f_back + module = inspect.getmodule(frame) assert module assert module.__file__ return Path(module.__file__).parent.absolute() diff --git a/playwright/_impl/_sync_base.py b/playwright/_impl/_sync_base.py index b50c7479d..e6fac9750 100644 --- a/playwright/_impl/_sync_base.py +++ b/playwright/_impl/_sync_base.py @@ -105,8 +105,8 @@ def _sync( g_self = greenlet.getcurrent() task: asyncio.tasks.Task[Any] = self._loop.create_task(coro) - setattr(task, "__pw_stack__", inspect.stack()) - setattr(task, "__pw_stack_trace__", traceback.extract_stack()) + setattr(task, "__pw_stack__", inspect.stack(0)) + setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)) task.add_done_callback(lambda _: g_self.switch()) while not task.done(): From 919701b6628048400ec9c612a6f19a90e8b601bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 10:25:07 +0200 Subject: [PATCH 21/84] build(deps): bump pytest-rerunfailures from 15.0 to 15.1 (#2851) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index b11ba7896..b3ed62a96 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -12,7 +12,7 @@ pytest==8.3.5 pytest-asyncio==0.26.0 pytest-cov==6.1.1 pytest-repeat==0.9.4 -pytest-rerunfailures==15.0 +pytest-rerunfailures==15.1 pytest-timeout==2.3.1 pytest-xdist==3.6.1 requests==2.32.3 From fc0081acbf004106b504efecd95782f50ea9a029 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 10:25:52 +0200 Subject: [PATCH 22/84] build(deps): bump setuptools from 80.3.1 to 80.4.0 (#2849) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2dc0a7133..cad4d18e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.3.1", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] +requires = ["setuptools==80.4.0", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] build-backend = "setuptools.build_meta" [project] From bd4fa2a25c79d77857b9e37172311196a54d0c04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 10:25:57 +0200 Subject: [PATCH 23/84] build(deps): bump greenlet from 3.2.1 to 3.2.2 (#2848) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6c5b7b1c9..28863d0dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile pyproject.toml -o requirements.txt -greenlet==3.2.1 +greenlet==3.2.2 # via playwright (pyproject.toml) pyee==13.0.0 # via playwright (pyproject.toml) From 1a6ab165c5e0e7d936f6ddb8ec161d4273d6ce80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 12:59:02 +0200 Subject: [PATCH 24/84] build(deps): bump pytest-timeout from 2.3.1 to 2.4.0 (#2850) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index b3ed62a96..bb834e828 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -13,7 +13,7 @@ pytest-asyncio==0.26.0 pytest-cov==6.1.1 pytest-repeat==0.9.4 pytest-rerunfailures==15.1 -pytest-timeout==2.3.1 +pytest-timeout==2.4.0 pytest-xdist==3.6.1 requests==2.32.3 service_identity==24.2.0 From 05a45601e9917618ba22f75d0395e9695d98a12a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 11:48:27 -0700 Subject: [PATCH 25/84] build(deps): bump pyopenssl from 25.0.0 to 25.1.0 (#2862) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index bb834e828..6d6bcd552 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -7,7 +7,7 @@ objgraph==3.6.2 Pillow==11.2.1 pixelmatch==0.3.0 pre-commit==3.5.0 -pyOpenSSL==25.0.0 +pyOpenSSL==25.1.0 pytest==8.3.5 pytest-asyncio==0.26.0 pytest-cov==6.1.1 From fc0e73de37c53036c429e432deed15606bda88ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 11:48:41 -0700 Subject: [PATCH 26/84] build(deps): bump types-requests from 2.32.0.20250328 to 2.32.0.20250515 (#2861) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 6d6bcd552..2fc05a12c 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -19,4 +19,4 @@ requests==2.32.3 service_identity==24.2.0 twisted==24.11.0 types-pyOpenSSL==24.1.0.20240722 -types-requests==2.32.0.20250328 +types-requests==2.32.0.20250515 From a2f9320a0c7173d336c272a8f45e4c51ebd535eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 11:48:55 -0700 Subject: [PATCH 27/84] build(deps): bump setuptools from 80.4.0 to 80.7.1 (#2860) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cad4d18e5..0b26f3944 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.4.0", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] +requires = ["setuptools==80.7.1", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] build-backend = "setuptools.build_meta" [project] From 3222ef88122b8157c0510c3e51102e3cd6942f71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 12:10:30 -0700 Subject: [PATCH 28/84] build(deps): bump setuptools from 80.7.1 to 80.8.0 (#2866) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0b26f3944..7d72d4778 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.7.1", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] +requires = ["setuptools==80.8.0", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] build-backend = "setuptools.build_meta" [project] From b98dd6c0a733d9600888b664d9da320d75a00d0c Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 27 May 2025 11:11:27 -0700 Subject: [PATCH 29/84] chore: bump pytest-asyncio to v1.0.0 (#2868) --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 2fc05a12c..2e8ddd874 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -9,7 +9,7 @@ pixelmatch==0.3.0 pre-commit==3.5.0 pyOpenSSL==25.1.0 pytest==8.3.5 -pytest-asyncio==0.26.0 +pytest-asyncio==1.0.0 pytest-cov==6.1.1 pytest-repeat==0.9.4 pytest-rerunfailures==15.1 From 398b0bc08a9ac72fdfe24a4eff60dddb9322956b Mon Sep 17 00:00:00 2001 From: campersau Date: Fri, 30 May 2025 17:13:49 +0200 Subject: [PATCH 30/84] fix(docker): set default shell encoding (#2871) --- utils/docker/Dockerfile.jammy | 3 +++ utils/docker/Dockerfile.noble | 3 +++ 2 files changed, 6 insertions(+) diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index 8dab1e1d1..7692ad7c5 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -4,6 +4,9 @@ ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/python:v%version%-jammy" +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 + # === INSTALL Python === RUN apt-get update && \ diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 8262bf6a9..2458236a3 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -4,6 +4,9 @@ ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/python:v%version%-noble" +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 + # === INSTALL Python === RUN apt-get update && \ From 3909baf88efa1830ab8a47ab64124e71558cf691 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 08:40:08 +0100 Subject: [PATCH 31/84] build(deps): bump mypy from 1.15.0 to 1.16.0 (#2874) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 2e8ddd874..5183c951e 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -2,7 +2,7 @@ autobahn==23.1.2 black==25.1.0 build==1.2.2.post1 flake8==7.2.0 -mypy==1.15.0 +mypy==1.16.0 objgraph==3.6.2 Pillow==11.2.1 pixelmatch==0.3.0 From 3d964961e9abddd980339bbf6ace7ff0aa34a0e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 08:40:22 +0100 Subject: [PATCH 32/84] build(deps): bump setuptools from 80.8.0 to 80.9.0 (#2873) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7d72d4778..1ff674eab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.8.0", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] +requires = ["setuptools==80.9.0", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] build-backend = "setuptools.build_meta" [project] From e3270574c697118a148ecad7eae7e15beb6b8255 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 08:40:56 +0100 Subject: [PATCH 33/84] build(deps): bump pytest from 8.3.5 to 8.4.0 (#2872) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 5183c951e..ab142cd16 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -8,7 +8,7 @@ Pillow==11.2.1 pixelmatch==0.3.0 pre-commit==3.5.0 pyOpenSSL==25.1.0 -pytest==8.3.5 +pytest==8.4.0 pytest-asyncio==1.0.0 pytest-cov==6.1.1 pytest-repeat==0.9.4 From e87e340fae92cb52f98dacdd80bb55c04e9bc4e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 08:53:38 +0100 Subject: [PATCH 34/84] build(deps): bump types-requests from 2.32.0.20250515 to 2.32.0.20250602 (#2875) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index ab142cd16..56b7edd22 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -19,4 +19,4 @@ requests==2.32.3 service_identity==24.2.0 twisted==24.11.0 types-pyOpenSSL==24.1.0.20240722 -types-requests==2.32.0.20250515 +types-requests==2.32.0.20250602 From 54765828e1fd58a21052674e6945cbc1f03858b3 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 10 Jun 2025 16:52:54 +0200 Subject: [PATCH 35/84] test: use tmp_path instead of tmpdir fixture (#2884) --- .../test_browsercontext_storage_state.py | 4 +- tests/async/test_browsertype_connect.py | 4 +- tests/async/test_chromium_tracing.py | 22 ++--- tests/async/test_defaultbrowsercontext.py | 14 ++-- tests/async/test_download.py | 34 ++++---- tests/async/test_fetch_global.py | 4 +- tests/async/test_har.py | 80 +++++++++---------- tests/async/test_headful.py | 8 +- tests/async/test_launcher.py | 4 +- tests/async/test_page_base_url.py | 4 +- tests/async/test_pdf.py | 10 +-- tests/async/test_tracing.py | 34 ++++---- tests/async/test_video.py | 28 +++---- .../sync/test_browsercontext_storage_state.py | 4 +- tests/sync/test_fetch_global.py | 4 +- tests/sync/test_har.py | 78 +++++++++--------- tests/sync/test_launcher.py | 4 +- tests/sync/test_pdf.py | 4 +- tests/sync/test_tracing.py | 34 ++++---- tests/sync/test_video.py | 36 ++++----- 20 files changed, 210 insertions(+), 204 deletions(-) diff --git a/tests/async/test_browsercontext_storage_state.py b/tests/async/test_browsercontext_storage_state.py index a7e853391..5004844ff 100644 --- a/tests/async/test_browsercontext_storage_state.py +++ b/tests/async/test_browsercontext_storage_state.py @@ -97,7 +97,7 @@ async def test_should_set_local_storage(browser: Browser) -> None: async def test_should_round_trip_through_the_file( - browser: Browser, context: BrowserContext, tmpdir: Path + browser: Browser, context: BrowserContext, tmp_path: Path ) -> None: page1 = await context.new_page() await page1.route( @@ -113,7 +113,7 @@ async def test_should_round_trip_through_the_file( }""" ) - path = tmpdir / "storage-state.json" + path = tmp_path / "storage-state.json" state = await context.storage_state(path=path) with open(path, "r") as f: written = json.load(f) diff --git a/tests/async/test_browsertype_connect.py b/tests/async/test_browsertype_connect.py index c2d8471d9..8295a6960 100644 --- a/tests/async/test_browsertype_connect.py +++ b/tests/async/test_browsertype_connect.py @@ -208,12 +208,12 @@ def handle_download(request: TestServerRequest) -> None: async def test_prevent_getting_video_path( browser_type: BrowserType, launch_server: Callable[[], RemoteServer], - tmpdir: Path, + tmp_path: Path, server: Server, ) -> None: remote_server = launch_server() browser = await browser_type.connect(remote_server.ws_endpoint) - page = await browser.new_page(record_video_dir=tmpdir) + page = await browser.new_page(record_video_dir=tmp_path) await page.goto(server.PREFIX + "/grid.html") await browser.close() assert page.video diff --git a/tests/async/test_chromium_tracing.py b/tests/async/test_chromium_tracing.py index 4cbd77a21..23608e009 100644 --- a/tests/async/test_chromium_tracing.py +++ b/tests/async/test_chromium_tracing.py @@ -24,9 +24,9 @@ @pytest.mark.only_browser("chromium") async def test_should_output_a_trace( - browser: Browser, page: Page, server: Server, tmpdir: Path + browser: Browser, page: Page, server: Server, tmp_path: Path ) -> None: - output_file = tmpdir / "trace.json" + output_file = tmp_path / "trace.json" await browser.start_tracing(page=page, screenshots=True, path=output_file) await page.goto(server.PREFIX + "/grid.html") await browser.stop_tracing() @@ -35,9 +35,9 @@ async def test_should_output_a_trace( @pytest.mark.only_browser("chromium") async def test_should_create_directories_as_needed( - browser: Browser, page: Page, server: Server, tmpdir: Path + browser: Browser, page: Page, server: Server, tmp_path: Path ) -> None: - output_file = tmpdir / "these" / "are" / "directories" / "trace.json" + output_file = tmp_path / "these" / "are" / "directories" / "trace.json" await browser.start_tracing(page=page, screenshots=True, path=output_file) await page.goto(server.PREFIX + "/grid.html") await browser.stop_tracing() @@ -46,9 +46,9 @@ async def test_should_create_directories_as_needed( @pytest.mark.only_browser("chromium") async def test_should_run_with_custom_categories_if_provided( - browser: Browser, page: Page, tmpdir: Path + browser: Browser, page: Page, tmp_path: Path ) -> None: - output_file = tmpdir / "trace.json" + output_file = tmp_path / "trace.json" await browser.start_tracing( page=page, screenshots=True, @@ -66,11 +66,11 @@ async def test_should_run_with_custom_categories_if_provided( @pytest.mark.only_browser("chromium") async def test_should_throw_if_tracing_on_two_pages( - browser: Browser, page: Page, tmpdir: Path + browser: Browser, page: Page, tmp_path: Path ) -> None: - output_file_1 = tmpdir / "trace1.json" + output_file_1 = tmp_path / "trace1.json" await browser.start_tracing(page=page, screenshots=True, path=output_file_1) - output_file_2 = tmpdir / "trace2.json" + output_file_2 = tmp_path / "trace2.json" with pytest.raises(Exception): await browser.start_tracing(page=page, screenshots=True, path=output_file_2) await browser.stop_tracing() @@ -78,9 +78,9 @@ async def test_should_throw_if_tracing_on_two_pages( @pytest.mark.only_browser("chromium") async def test_should_return_a_buffer( - browser: Browser, page: Page, server: Server, tmpdir: Path + browser: Browser, page: Page, server: Server, tmp_path: Path ) -> None: - output_file = tmpdir / "trace.json" + output_file = tmp_path / "trace.json" await browser.start_tracing(page=page, path=output_file, screenshots=True) await page.goto(server.PREFIX + "/grid.html") value = await browser.stop_tracing() diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py index cc42a9c33..67de51702 100644 --- a/tests/async/test_defaultbrowsercontext.py +++ b/tests/async/test_defaultbrowsercontext.py @@ -45,7 +45,7 @@ @pytest.fixture() async def launch_persistent( - tmpdir: Path, launch_arguments: Dict, browser_type: BrowserType + tmp_path: Path, launch_arguments: Dict, browser_type: BrowserType ) -> AsyncGenerator[Callable[..., Awaitable[Tuple[Page, BrowserContext]]], None]: context: Optional[BrowserContext] = None @@ -54,7 +54,7 @@ async def _launch(**options: Any) -> Tuple[Page, BrowserContext]: if context: raise ValueError("can only launch one persistent context") context = await browser_type.launch_persistent_context( - str(tmpdir), **{**launch_arguments, **options} + str(tmp_path), **{**launch_arguments, **options} ) assert context return (context.pages[0], context) @@ -373,14 +373,14 @@ async def test_should_support_extra_http_headers_option( async def test_should_accept_user_data_dir( - tmpdir: Path, + tmp_path: Path, launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]", ) -> None: (page, context) = await launch_persistent() # Note: we need an open page to make sure its functional. - assert len(os.listdir(tmpdir)) > 0 + assert len(os.listdir(tmp_path)) > 0 await context.close() - assert len(os.listdir(tmpdir)) > 0 + assert len(os.listdir(tmp_path)) > 0 async def test_should_restore_state_from_userDataDir( @@ -426,11 +426,11 @@ async def test_should_have_default_url_when_launching_browser( @pytest.mark.skip_browser("firefox") async def test_should_throw_if_page_argument_is_passed( - browser_type: BrowserType, server: Server, tmpdir: Path, launch_arguments: Dict + browser_type: BrowserType, server: Server, tmp_path: Path, launch_arguments: Dict ) -> None: options = {**launch_arguments, "args": [server.EMPTY_PAGE]} with pytest.raises(Error) as exc: - await browser_type.launch_persistent_context(tmpdir, **options) + await browser_type.launch_persistent_context(tmp_path, **options) assert "can not specify page" in exc.value.message diff --git a/tests/async/test_download.py b/tests/async/test_download.py index 082fcac26..6b0d6be1a 100644 --- a/tests/async/test_download.py +++ b/tests/async/test_download.py @@ -83,14 +83,14 @@ async def test_should_report_downloads_with_accept_downloads_true( async def test_should_save_to_user_specified_path( - tmpdir: Path, browser: Browser, server: Server + tmp_path: Path, browser: Browser, server: Server ) -> None: page = await browser.new_page(accept_downloads=True) await page.set_content(f'download') async with page.expect_download() as download_info: await page.click("a") download = await download_info.value - user_path = tmpdir / "download.txt" + user_path = tmp_path / "download.txt" await download.save_as(user_path) assert user_path.exists() assert user_path.read_text("utf-8") == "Hello world" @@ -98,14 +98,14 @@ async def test_should_save_to_user_specified_path( async def test_should_save_to_user_specified_path_without_updating_original_path( - tmpdir: Path, browser: Browser, server: Server + tmp_path: Path, browser: Browser, server: Server ) -> None: page = await browser.new_page(accept_downloads=True) await page.set_content(f'download') async with page.expect_download() as download_info: await page.click("a") download = await download_info.value - user_path = tmpdir / "download.txt" + user_path = tmp_path / "download.txt" await download.save_as(user_path) assert user_path.exists() assert user_path.read_text("utf-8") == "Hello world" @@ -117,19 +117,19 @@ async def test_should_save_to_user_specified_path_without_updating_original_path async def test_should_save_to_two_different_paths_with_multiple_save_as_calls( - tmpdir: Path, browser: Browser, server: Server + tmp_path: Path, browser: Browser, server: Server ) -> None: page = await browser.new_page(accept_downloads=True) await page.set_content(f'download') async with page.expect_download() as download_info: await page.click("a") download = await download_info.value - user_path = tmpdir / "download.txt" + user_path = tmp_path / "download.txt" await download.save_as(user_path) assert user_path.exists() assert user_path.read_text("utf-8") == "Hello world" - anotheruser_path = tmpdir / "download (2).txt" + anotheruser_path = tmp_path / "download (2).txt" await download.save_as(anotheruser_path) assert anotheruser_path.exists() assert anotheruser_path.read_text("utf-8") == "Hello world" @@ -137,32 +137,32 @@ async def test_should_save_to_two_different_paths_with_multiple_save_as_calls( async def test_should_save_to_overwritten_filepath( - tmpdir: Path, browser: Browser, server: Server + tmp_path: Path, browser: Browser, server: Server ) -> None: page = await browser.new_page(accept_downloads=True) await page.set_content(f'download') async with page.expect_download() as download_info: await page.click("a") download = await download_info.value - user_path = tmpdir / "download.txt" + user_path = tmp_path / "download.txt" await download.save_as(user_path) - assert len(list(Path(tmpdir).glob("*.*"))) == 1 + assert len(list(tmp_path.glob("*.*"))) == 1 await download.save_as(user_path) - assert len(list(Path(tmpdir).glob("*.*"))) == 1 + assert len(list(tmp_path.glob("*.*"))) == 1 assert user_path.exists() assert user_path.read_text("utf-8") == "Hello world" await page.close() async def test_should_create_subdirectories_when_saving_to_non_existent_user_specified_path( - tmpdir: Path, browser: Browser, server: Server + tmp_path: Path, browser: Browser, server: Server ) -> None: page = await browser.new_page(accept_downloads=True) await page.set_content(f'download') async with page.expect_download() as download_info: await page.click("a") download = await download_info.value - nested_path = tmpdir / "these" / "are" / "directories" / "download.txt" + nested_path = tmp_path / "these" / "are" / "directories" / "download.txt" await download.save_as(nested_path) assert nested_path.exists() assert nested_path.read_text("utf-8") == "Hello world" @@ -170,14 +170,14 @@ async def test_should_create_subdirectories_when_saving_to_non_existent_user_spe async def test_should_error_when_saving_with_downloads_disabled( - tmpdir: Path, browser: Browser, server: Server + tmp_path: Path, browser: Browser, server: Server ) -> None: page = await browser.new_page(accept_downloads=False) await page.set_content(f'download') async with page.expect_download() as download_info: await page.click("a") download = await download_info.value - user_path = tmpdir / "download.txt" + user_path = tmp_path / "download.txt" with pytest.raises(Error) as exc: await download.save_as(user_path) assert ( @@ -192,14 +192,14 @@ async def test_should_error_when_saving_with_downloads_disabled( async def test_should_error_when_saving_after_deletion( - tmpdir: Path, browser: Browser, server: Server + tmp_path: Path, browser: Browser, server: Server ) -> None: page = await browser.new_page(accept_downloads=True) await page.set_content(f'download') async with page.expect_download() as download_info: await page.click("a") download = await download_info.value - user_path = tmpdir / "download.txt" + user_path = tmp_path / "download.txt" await download.delete() with pytest.raises(Error) as exc: await download.save_as(user_path) diff --git a/tests/async/test_fetch_global.py b/tests/async/test_fetch_global.py index ae394755b..6b74208e2 100644 --- a/tests/async/test_fetch_global.py +++ b/tests/async/test_fetch_global.py @@ -289,7 +289,7 @@ async def test_should_return_empty_body(playwright: Playwright, server: Server) async def test_storage_state_should_round_trip_through_file( - playwright: Playwright, tmpdir: Path + playwright: Playwright, tmp_path: Path ) -> None: expected: StorageState = { "cookies": [ @@ -307,7 +307,7 @@ async def test_storage_state_should_round_trip_through_file( "origins": [], } request = await playwright.request.new_context(storage_state=expected) - path = tmpdir / "storage-state.json" + path = tmp_path / "storage-state.json" actual = await request.storage_state(path=path) assert actual == expected diff --git a/tests/async/test_har.py b/tests/async/test_har.py index b7875ea35..0ea5ee054 100644 --- a/tests/async/test_har.py +++ b/tests/async/test_har.py @@ -27,8 +27,8 @@ from tests.utils import must -async def test_should_work(browser: Browser, server: Server, tmpdir: Path) -> None: - path = os.path.join(tmpdir, "log.har") +async def test_should_work(browser: Browser, server: Server, tmp_path: Path) -> None: + path = os.path.join(tmp_path, "log.har") context = await browser.new_context(record_har_path=path) page = await context.new_page() await page.goto(server.EMPTY_PAGE) @@ -39,9 +39,9 @@ async def test_should_work(browser: Browser, server: Server, tmpdir: Path) -> No async def test_should_omit_content( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = await browser.new_context( record_har_path=path, record_har_content="omit", @@ -59,9 +59,9 @@ async def test_should_omit_content( async def test_should_omit_content_legacy( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = await browser.new_context( record_har_path=path, record_har_omit_content=True ) @@ -78,9 +78,9 @@ async def test_should_omit_content_legacy( async def test_should_attach_content( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - path = os.path.join(tmpdir, "log.har.zip") + path = os.path.join(tmp_path, "log.har.zip") context = await browser.new_context( record_har_path=path, record_har_content="attach", @@ -137,9 +137,9 @@ async def test_should_attach_content( async def test_should_not_omit_content( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = await browser.new_context( record_har_path=path, record_har_omit_content=False ) @@ -153,9 +153,9 @@ async def test_should_not_omit_content( async def test_should_include_content( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = await browser.new_context(record_har_path=path) page = await context.new_page() await page.goto(server.PREFIX + "/har.html") @@ -171,9 +171,9 @@ async def test_should_include_content( async def test_should_default_to_full_mode( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = await browser.new_context( record_har_path=path, ) @@ -188,9 +188,9 @@ async def test_should_default_to_full_mode( async def test_should_support_minimal_mode( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = await browser.new_context( record_har_path=path, record_har_mode="minimal", @@ -206,9 +206,9 @@ async def test_should_support_minimal_mode( async def test_should_filter_by_glob( - browser: Browser, server: Server, tmpdir: str + browser: Browser, server: Server, tmp_path: str ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = await browser.new_context( base_url=server.PREFIX, record_har_path=path, @@ -227,9 +227,9 @@ async def test_should_filter_by_glob( async def test_should_filter_by_regexp( - browser: Browser, server: Server, tmpdir: str + browser: Browser, server: Server, tmp_path: str ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = await browser.new_context( base_url=server.PREFIX, record_har_path=path, @@ -303,9 +303,9 @@ async def test_by_default_should_abort_requests_not_found_in_har( async def test_fallback_continue_should_continue_requests_on_bad_har( - context: BrowserContext, server: Server, tmpdir: Path + context: BrowserContext, server: Server, tmp_path: Path ) -> None: - path_to_invalid_har = tmpdir / "invalid.har" + path_to_invalid_har = tmp_path / "invalid.har" with path_to_invalid_har.open("w") as f: json.dump({"log": {}}, f) await context.route_from_har(har=path_to_invalid_har, not_found="fallback") @@ -500,9 +500,9 @@ async def test_should_fulfill_from_har_with_content_in_a_file( async def test_should_round_trip_har_zip( - browser: Browser, server: Server, assetdir: Path, tmpdir: Path + browser: Browser, server: Server, assetdir: Path, tmp_path: Path ) -> None: - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context_1 = await browser.new_context( record_har_mode="minimal", record_har_path=har_path ) @@ -521,7 +521,7 @@ async def test_should_round_trip_har_zip( async def test_should_round_trip_har_with_post_data( - browser: Browser, server: Server, assetdir: Path, tmpdir: Path + browser: Browser, server: Server, assetdir: Path, tmp_path: Path ) -> None: server.set_route("/echo", lambda req: (req.write(req.post_body), req.finish())) fetch_function = """ @@ -530,7 +530,7 @@ async def test_should_round_trip_har_with_post_data( return await response.text(); }; """ - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context_1 = await browser.new_context( record_har_mode="minimal", record_har_path=har_path ) @@ -554,7 +554,7 @@ async def test_should_round_trip_har_with_post_data( async def test_should_disambiguate_by_header( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: server.set_route( "/echo", @@ -574,7 +574,7 @@ async def test_should_disambiguate_by_header( return await response.text(); }; """ - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context_1 = await browser.new_context( record_har_mode="minimal", record_har_path=har_path ) @@ -597,9 +597,9 @@ async def test_should_disambiguate_by_header( async def test_should_produce_extracted_zip( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.har" + har_path = tmp_path / "har.har" context = await browser.new_context( record_har_mode="minimal", record_har_path=har_path, record_har_content="attach" ) @@ -624,9 +624,9 @@ async def test_should_produce_extracted_zip( async def test_should_update_har_zip_for_context( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context = await browser.new_context() await context.route_from_har(har_path, update=True) page_1 = await context.new_page() @@ -684,9 +684,9 @@ async def test_context_unroute_call_should_stop_context_route_from_har( async def test_should_update_har_zip_for_page( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context = await browser.new_context() page_1 = await context.new_page() await page_1.route_from_har(har_path, update=True) @@ -706,9 +706,9 @@ async def test_should_update_har_zip_for_page( async def test_should_update_har_zip_for_page_with_different_options( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context1 = await browser.new_context() page1 = await context1.new_page() await page1.route_from_har( @@ -729,9 +729,9 @@ async def test_should_update_har_zip_for_page_with_different_options( async def test_should_update_extracted_har_zip_for_page( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.har" + har_path = tmp_path / "har.har" context = await browser.new_context() page_1 = await context.new_page() await page_1.route_from_har(har_path, update=True) @@ -757,9 +757,9 @@ async def test_should_update_extracted_har_zip_for_page( async def test_should_ignore_aborted_requests( context_factory: Callable[[], Awaitable[BrowserContext]], server: Server, - tmpdir: Path, + tmp_path: Path, ) -> None: - path = tmpdir / "test.har" + path = tmp_path / "test.har" server.set_route("/x", lambda request: request.loseConnection()) context1 = await context_factory() await context1.route_from_har(har=path, update=True) diff --git a/tests/async/test_headful.py b/tests/async/test_headful.py index 2e0dd026f..2b0b64c8e 100644 --- a/tests/async/test_headful.py +++ b/tests/async/test_headful.py @@ -23,10 +23,10 @@ async def test_should_have_default_url_when_launching_browser( - browser_type: BrowserType, launch_arguments: Dict, tmpdir: Path + browser_type: BrowserType, launch_arguments: Dict, tmp_path: Path ) -> None: browser_context = await browser_type.launch_persistent_context( - tmpdir, **{**launch_arguments, "headless": False} + tmp_path, **{**launch_arguments, "headless": False} ) urls = [page.url for page in browser_context.pages] assert urls == ["about:blank"] @@ -34,10 +34,10 @@ async def test_should_have_default_url_when_launching_browser( async def test_should_close_browser_with_beforeunload_page( - browser_type: BrowserType, launch_arguments: Dict, server: Server, tmpdir: Path + browser_type: BrowserType, launch_arguments: Dict, server: Server, tmp_path: Path ) -> None: browser_context = await browser_type.launch_persistent_context( - tmpdir, **{**launch_arguments, "headless": False} + tmp_path, **{**launch_arguments, "headless": False} ) page = await browser_context.new_page() await page.goto(server.PREFIX + "/beforeunload.html") diff --git a/tests/async/test_launcher.py b/tests/async/test_launcher.py index d29b20989..1b974725b 100644 --- a/tests/async/test_launcher.py +++ b/tests/async/test_launcher.py @@ -112,7 +112,7 @@ async def test_browser_close_should_be_callable_twice( @pytest.mark.only_browser("chromium") async def test_browser_launch_should_return_background_pages( browser_type: BrowserType, - tmpdir: Path, + tmp_path: Path, browser_channel: Optional[str], assetdir: Path, launch_arguments: Dict, @@ -122,7 +122,7 @@ async def test_browser_launch_should_return_background_pages( extension_path = str(assetdir / "simple-extension") context = await browser_type.launch_persistent_context( - str(tmpdir), + str(tmp_path), **{ **launch_arguments, "headless": False, diff --git a/tests/async/test_page_base_url.py b/tests/async/test_page_base_url.py index ab917b248..3f0599e01 100644 --- a/tests/async/test_page_base_url.py +++ b/tests/async/test_page_base_url.py @@ -38,10 +38,10 @@ async def test_should_construct_a_new_url_when_a_base_url_in_browser_new_page_is async def test_should_construct_a_new_url_when_a_base_url_in_browser_new_persistent_context_is_passed( - browser_type: BrowserType, tmpdir: Path, server: Server, launch_arguments: Dict + browser_type: BrowserType, tmp_path: Path, server: Server, launch_arguments: Dict ) -> None: context = await browser_type.launch_persistent_context( - tmpdir, **launch_arguments, base_url=server.PREFIX + tmp_path, **launch_arguments, base_url=server.PREFIX ) page = await context.new_page() assert (must(await page.goto("/empty.html"))).url == server.EMPTY_PAGE diff --git a/tests/async/test_pdf.py b/tests/async/test_pdf.py index 7e916dc11..93d1fcf8a 100644 --- a/tests/async/test_pdf.py +++ b/tests/async/test_pdf.py @@ -23,8 +23,8 @@ pytestmark = pytest.mark.only_browser("chromium") -async def test_should_be_able_to_save_pdf_file(page: Page, tmpdir: Path) -> None: - output_file = tmpdir / "foo.png" +async def test_should_be_able_to_save_pdf_file(page: Page, tmp_path: Path) -> None: + output_file = tmp_path / "foo.png" await page.pdf(path=str(output_file)) assert os.path.getsize(output_file) > 0 @@ -35,11 +35,11 @@ async def test_should_be_able_capture_pdf_without_path(page: Page) -> None: async def test_should_be_able_to_generate_outline( - page: Page, server: Server, tmpdir: Path + page: Page, server: Server, tmp_path: Path ) -> None: await page.goto(server.PREFIX + "/headings.html") - output_file_no_outline = tmpdir / "outputNoOutline.pdf" - output_file_outline = tmpdir / "outputOutline.pdf" + output_file_no_outline = tmp_path / "outputNoOutline.pdf" + output_file_outline = tmp_path / "outputOutline.pdf" await page.pdf(path=output_file_no_outline) await page.pdf(path=output_file_outline, tagged=True, outline=True) assert os.path.getsize(output_file_outline) > os.path.getsize( diff --git a/tests/async/test_tracing.py b/tests/async/test_tracing.py index bb39f96f4..6b0c557f2 100644 --- a/tests/async/test_tracing.py +++ b/tests/async/test_tracing.py @@ -87,7 +87,7 @@ async def test_should_collect_sources( async def test_should_collect_trace_with_resources_but_no_js( - context: BrowserContext, page: Page, server: Server, tmpdir: Path + context: BrowserContext, page: Page, server: Server, tmp_path: Path ) -> None: await context.tracing.start(screenshots=True, snapshots=True) await page.goto(server.PREFIX + "/frames/frame.html") @@ -105,7 +105,7 @@ async def test_should_collect_trace_with_resources_but_no_js( server.PREFIX + "/one-style.html" ) # should not produce a route.continue_ entry since we continue all routes if no match. await page.close() - trace_file_path = tmpdir / "trace.zip" + trace_file_path = tmp_path / "trace.zip" await context.tracing.stop(path=trace_file_path) (_, events) = parse_trace(trace_file_path) @@ -147,7 +147,7 @@ async def test_should_collect_trace_with_resources_but_no_js( async def test_should_correctly_determine_sync_apiname( - context: BrowserContext, page: Page, server: Server, tmpdir: Path + context: BrowserContext, page: Page, server: Server, tmp_path: Path ) -> None: await context.tracing.start(screenshots=True, snapshots=True) @@ -162,7 +162,7 @@ async def _handle_response(response: Response) -> None: await page.goto(server.PREFIX + "/grid.html") await received_response await page.close() - trace_file_path = tmpdir / "trace.zip" + trace_file_path = tmp_path / "trace.zip" await context.tracing.stop(path=trace_file_path) (_, events) = parse_trace(trace_file_path) @@ -174,19 +174,19 @@ async def _handle_response(response: Response) -> None: async def test_should_collect_two_traces( - context: BrowserContext, page: Page, server: Server, tmpdir: Path + context: BrowserContext, page: Page, server: Server, tmp_path: Path ) -> None: await context.tracing.start(screenshots=True, snapshots=True) await page.goto(server.EMPTY_PAGE) await page.set_content("") await page.click('"Click"') - tracing1_path = tmpdir / "trace1.zip" + tracing1_path = tmp_path / "trace1.zip" await context.tracing.stop(path=tracing1_path) await context.tracing.start(screenshots=True, snapshots=True) await page.dblclick('"Click"') await page.close() - tracing2_path = tmpdir / "trace2.zip" + tracing2_path = tmp_path / "trace2.zip" await context.tracing.stop(path=tracing2_path) (_, events) = parse_trace(tracing1_path) @@ -209,7 +209,7 @@ async def test_should_not_throw_when_stopping_without_start_but_not_exporting( async def test_should_work_with_playwright_context_managers( - context: BrowserContext, page: Page, server: Server, tmpdir: Path + context: BrowserContext, page: Page, server: Server, tmp_path: Path ) -> None: await context.tracing.start(screenshots=True, snapshots=True) await page.goto(server.EMPTY_PAGE) @@ -221,7 +221,7 @@ async def test_should_work_with_playwright_context_managers( async with page.expect_popup(): await page.evaluate("window._popup = window.open(document.location.href)") - trace_file_path = tmpdir / "trace.zip" + trace_file_path = tmp_path / "trace.zip" await context.tracing.stop(path=trace_file_path) (_, events) = parse_trace(trace_file_path) @@ -238,7 +238,7 @@ async def test_should_work_with_playwright_context_managers( async def test_should_display_wait_for_load_state_even_if_did_not_wait_for_it( - context: BrowserContext, page: Page, server: Server, tmpdir: Path + context: BrowserContext, page: Page, server: Server, tmp_path: Path ) -> None: await context.tracing.start(screenshots=True, snapshots=True) @@ -246,7 +246,7 @@ async def test_should_display_wait_for_load_state_even_if_did_not_wait_for_it( await page.wait_for_load_state("load") await page.wait_for_load_state("load") - trace_file_path = tmpdir / "trace.zip" + trace_file_path = tmp_path / "trace.zip" await context.tracing.stop(path=trace_file_path) (_, events) = parse_trace(trace_file_path) @@ -260,23 +260,23 @@ async def test_should_display_wait_for_load_state_even_if_did_not_wait_for_it( async def test_should_respect_traces_dir_and_name( browser_type: BrowserType, server: Server, - tmpdir: Path, + tmp_path: Path, launch_arguments: Dict, ) -> None: - traces_dir = tmpdir / "traces" + traces_dir = tmp_path / "traces" browser = await browser_type.launch(traces_dir=traces_dir, **launch_arguments) context = await browser.new_context() page = await context.new_page() await context.tracing.start(name="name1", snapshots=True) await page.goto(server.PREFIX + "/one-style.html") - await context.tracing.stop_chunk(path=tmpdir / "trace1.zip") + await context.tracing.stop_chunk(path=tmp_path / "trace1.zip") assert (traces_dir / "name1.trace").exists() assert (traces_dir / "name1.network").exists() await context.tracing.start_chunk(name="name2") await page.goto(server.PREFIX + "/har.html") - await context.tracing.stop(path=tmpdir / "trace2.zip") + await context.tracing.stop(path=tmp_path / "trace2.zip") assert (traces_dir / "name2.trace").exists() assert (traces_dir / "name2.network").exists() @@ -290,7 +290,7 @@ def resource_names(resources: Dict[str, bytes]) -> List[str]: ] ) - (resources, events) = parse_trace(tmpdir / "trace1.zip") + (resources, events) = parse_trace(tmp_path / "trace1.zip") assert get_trace_actions(events) == ["Page.goto"] assert resource_names(resources) == [ "resources/XXX.css", @@ -300,7 +300,7 @@ def resource_names(resources: Dict[str, bytes]) -> List[str]: "trace.trace", ] - (resources, events) = parse_trace(tmpdir / "trace2.zip") + (resources, events) = parse_trace(tmp_path / "trace2.zip") assert get_trace_actions(events) == ["Page.goto"] assert resource_names(resources) == [ "resources/XXX.css", diff --git a/tests/async/test_video.py b/tests/async/test_video.py index b0ab4c529..08d757794 100644 --- a/tests/async/test_video.py +++ b/tests/async/test_video.py @@ -21,37 +21,37 @@ async def test_should_expose_video_path( - browser: Browser, tmpdir: Path, server: Server + browser: Browser, tmp_path: Path, server: Server ) -> None: - page = await browser.new_page(record_video_dir=tmpdir) + page = await browser.new_page(record_video_dir=tmp_path) await page.goto(server.PREFIX + "/grid.html") assert page.video path = await page.video.path() - assert str(tmpdir) in str(path) + assert str(tmp_path) in str(path) await page.context.close() async def test_short_video_should_throw( - browser: Browser, tmpdir: Path, server: Server + browser: Browser, tmp_path: Path, server: Server ) -> None: - page = await browser.new_page(record_video_dir=tmpdir) + page = await browser.new_page(record_video_dir=tmp_path) await page.goto(server.PREFIX + "/grid.html") assert page.video path = await page.video.path() - assert str(tmpdir) in str(path) + assert str(tmp_path) in str(path) await page.wait_for_timeout(1000) await page.context.close() assert os.path.exists(path) async def test_short_video_should_throw_persistent_context( - browser_type: BrowserType, tmpdir: Path, launch_arguments: Dict, server: Server + browser_type: BrowserType, tmp_path: Path, launch_arguments: Dict, server: Server ) -> None: context = await browser_type.launch_persistent_context( - str(tmpdir), + str(tmp_path), **launch_arguments, viewport={"width": 320, "height": 240}, - record_video_dir=str(tmpdir) + "1", + record_video_dir=str(tmp_path) + "1", ) page = context.pages[0] await page.goto(server.PREFIX + "/grid.html") @@ -60,16 +60,16 @@ async def test_short_video_should_throw_persistent_context( assert page.video path = await page.video.path() - assert str(tmpdir) in str(path) + assert str(tmp_path) in str(path) async def test_should_not_error_if_page_not_closed_before_save_as( - browser: Browser, tmpdir: Path, server: Server + browser: Browser, tmp_path: Path, server: Server ) -> None: - page = await browser.new_page(record_video_dir=tmpdir) + page = await browser.new_page(record_video_dir=tmp_path) await page.goto(server.PREFIX + "/grid.html") await page.wait_for_timeout(1000) # make sure video has some data - out_path = tmpdir / "some-video.webm" + out_path = tmp_path / "some-video.webm" assert page.video saved = page.video.save_as(out_path) await page.close() @@ -79,7 +79,7 @@ async def test_should_not_error_if_page_not_closed_before_save_as( async def test_should_be_None_if_not_recording( - browser: Browser, tmpdir: Path, server: Server + browser: Browser, tmp_path: Path, server: Server ) -> None: page = await browser.new_page() assert page.video is None diff --git a/tests/sync/test_browsercontext_storage_state.py b/tests/sync/test_browsercontext_storage_state.py index f7db067d4..6850de8a1 100644 --- a/tests/sync/test_browsercontext_storage_state.py +++ b/tests/sync/test_browsercontext_storage_state.py @@ -93,7 +93,7 @@ def test_should_set_local_storage(browser: Browser) -> None: def test_should_round_trip_through_the_file( - browser: Browser, context: BrowserContext, tmpdir: Path + browser: Browser, context: BrowserContext, tmp_path: Path ) -> None: page1 = context.new_page() page1.route( @@ -109,7 +109,7 @@ def test_should_round_trip_through_the_file( }""" ) - path = tmpdir / "storage-state.json" + path = tmp_path / "storage-state.json" state = context.storage_state(path=path) with open(path, "r") as f: written = json.load(f) diff --git a/tests/sync/test_fetch_global.py b/tests/sync/test_fetch_global.py index 9efc6e93b..7305834a9 100644 --- a/tests/sync/test_fetch_global.py +++ b/tests/sync/test_fetch_global.py @@ -236,7 +236,7 @@ def test_should_return_empty_body(playwright: Playwright, server: Server) -> Non def test_storage_state_should_round_trip_through_file( - playwright: Playwright, tmpdir: Path + playwright: Playwright, tmp_path: Path ) -> None: expected: StorageState = { "cookies": [ @@ -254,7 +254,7 @@ def test_storage_state_should_round_trip_through_file( "origins": [], } request = playwright.request.new_context(storage_state=expected) - path = tmpdir / "storage-state.json" + path = tmp_path / "storage-state.json" actual = request.storage_state(path=path) assert actual == expected diff --git a/tests/sync/test_har.py b/tests/sync/test_har.py index 0644d3856..990b1d382 100644 --- a/tests/sync/test_har.py +++ b/tests/sync/test_har.py @@ -25,8 +25,8 @@ from tests.server import Server -def test_should_work(browser: Browser, server: Server, tmpdir: Path) -> None: - path = os.path.join(tmpdir, "log.har") +def test_should_work(browser: Browser, server: Server, tmp_path: Path) -> None: + path = os.path.join(tmp_path, "log.har") context = browser.new_context(record_har_path=path) page = context.new_page() page.goto(server.EMPTY_PAGE) @@ -36,8 +36,8 @@ def test_should_work(browser: Browser, server: Server, tmpdir: Path) -> None: assert "log" in data -def test_should_omit_content(browser: Browser, server: Server, tmpdir: Path) -> None: - path = os.path.join(tmpdir, "log.har") +def test_should_omit_content(browser: Browser, server: Server, tmp_path: Path) -> None: + path = os.path.join(tmp_path, "log.har") context = browser.new_context(record_har_path=path, record_har_content="omit") page = context.new_page() page.goto(server.PREFIX + "/har.html") @@ -53,9 +53,9 @@ def test_should_omit_content(browser: Browser, server: Server, tmpdir: Path) -> def test_should_omit_content_legacy( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = browser.new_context(record_har_path=path, record_har_omit_content=True) page = context.new_page() page.goto(server.PREFIX + "/har.html") @@ -70,8 +70,10 @@ def test_should_omit_content_legacy( assert "encoding" not in content1 -def test_should_attach_content(browser: Browser, server: Server, tmpdir: Path) -> None: - path = os.path.join(tmpdir, "log.har.zip") +def test_should_attach_content( + browser: Browser, server: Server, tmp_path: Path +) -> None: + path = os.path.join(tmp_path, "log.har.zip") context = browser.new_context( record_har_path=path, record_har_content="attach", @@ -127,8 +129,10 @@ def test_should_attach_content(browser: Browser, server: Server, tmpdir: Path) - assert len(f.read()) == entries[2]["response"]["content"]["size"] -def test_should_include_content(browser: Browser, server: Server, tmpdir: Path) -> None: - path = os.path.join(tmpdir, "log.har") +def test_should_include_content( + browser: Browser, server: Server, tmp_path: Path +) -> None: + path = os.path.join(tmp_path, "log.har") context = browser.new_context(record_har_path=path) page = context.new_page() page.goto(server.PREFIX + "/har.html") @@ -144,9 +148,9 @@ def test_should_include_content(browser: Browser, server: Server, tmpdir: Path) def test_should_default_to_full_mode( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = browser.new_context( record_har_path=path, ) @@ -161,9 +165,9 @@ def test_should_default_to_full_mode( def test_should_support_minimal_mode( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - path = os.path.join(tmpdir, "log.har") + path = os.path.join(tmp_path, "log.har") context = browser.new_context( record_har_path=path, record_har_mode="minimal", @@ -178,8 +182,8 @@ def test_should_support_minimal_mode( assert log["entries"][0]["request"]["bodySize"] == -1 -def test_should_filter_by_glob(browser: Browser, server: Server, tmpdir: str) -> None: - path = os.path.join(tmpdir, "log.har") +def test_should_filter_by_glob(browser: Browser, server: Server, tmp_path: str) -> None: + path = os.path.join(tmp_path, "log.har") context = browser.new_context( base_url=server.PREFIX, record_har_path=path, @@ -197,8 +201,10 @@ def test_should_filter_by_glob(browser: Browser, server: Server, tmpdir: str) -> assert log["entries"][0]["request"]["url"].endswith("one-style.css") -def test_should_filter_by_regexp(browser: Browser, server: Server, tmpdir: str) -> None: - path = os.path.join(tmpdir, "log.har") +def test_should_filter_by_regexp( + browser: Browser, server: Server, tmp_path: str +) -> None: + path = os.path.join(tmp_path, "log.har") context = browser.new_context( base_url=server.PREFIX, record_har_path=path, @@ -270,9 +276,9 @@ def test_by_default_should_abort_requests_not_found_in_har( def test_fallback_continue_should_continue_requests_on_bad_har( - context: BrowserContext, server: Server, tmpdir: Path + context: BrowserContext, server: Server, tmp_path: Path ) -> None: - path_to_invalid_har = tmpdir / "invalid.har" + path_to_invalid_har = tmp_path / "invalid.har" with path_to_invalid_har.open("w") as f: json.dump({"log": {}}, f) context.route_from_har(har=path_to_invalid_har, not_found="fallback") @@ -423,9 +429,9 @@ def test_should_fulfill_from_har_with_content_in_a_file( def test_should_round_trip_har_zip( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context_1 = browser.new_context(record_har_mode="minimal", record_har_path=har_path) page_1 = context_1.new_page() page_1.goto(server.PREFIX + "/one-style.html") @@ -440,7 +446,7 @@ def test_should_round_trip_har_zip( def test_should_round_trip_har_with_post_data( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: server.set_route( "/echo", lambda req: (req.write(cast(Any, req).post_body), req.finish()) @@ -451,7 +457,7 @@ def test_should_round_trip_har_with_post_data( return response.text(); }; """ - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context_1 = browser.new_context(record_har_mode="minimal", record_har_path=har_path) page_1 = context_1.new_page() page_1.goto(server.EMPTY_PAGE) @@ -473,7 +479,7 @@ def test_should_round_trip_har_with_post_data( def test_should_disambiguate_by_header( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: server.set_route( "/echo", @@ -493,7 +499,7 @@ def test_should_disambiguate_by_header( return response.text(); }; """ - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context_1 = browser.new_context(record_har_mode="minimal", record_har_path=har_path) page_1 = context_1.new_page() page_1.goto(server.EMPTY_PAGE) @@ -514,9 +520,9 @@ def test_should_disambiguate_by_header( def test_should_produce_extracted_zip( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.har" + har_path = tmp_path / "har.har" context = browser.new_context( record_har_mode="minimal", record_har_path=har_path, record_har_content="attach" ) @@ -539,9 +545,9 @@ def test_should_produce_extracted_zip( def test_should_update_har_zip_for_context( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context = browser.new_context() context.route_from_har(har_path, update=True) page_1 = context.new_page() @@ -559,9 +565,9 @@ def test_should_update_har_zip_for_context( def test_should_update_har_zip_for_page( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context = browser.new_context() page_1 = context.new_page() page_1.route_from_har(har_path, update=True) @@ -579,9 +585,9 @@ def test_should_update_har_zip_for_page( def test_should_update_har_zip_for_page_with_different_options( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.zip" + har_path = tmp_path / "har.zip" context1 = browser.new_context() page1 = context1.new_page() page1.route_from_har( @@ -600,9 +606,9 @@ def test_should_update_har_zip_for_page_with_different_options( def test_should_update_extracted_har_zip_for_page( - browser: Browser, server: Server, tmpdir: Path + browser: Browser, server: Server, tmp_path: Path ) -> None: - har_path = tmpdir / "har.har" + har_path = tmp_path / "har.har" context = browser.new_context() page_1 = context.new_page() page_1.route_from_har(har_path, update=True) diff --git a/tests/sync/test_launcher.py b/tests/sync/test_launcher.py index 8577fd200..52deeb827 100644 --- a/tests/sync/test_launcher.py +++ b/tests/sync/test_launcher.py @@ -93,7 +93,7 @@ def test_browser_close_should_be_callable_twice( @pytest.mark.only_browser("chromium") def test_browser_launch_should_return_background_pages( browser_type: BrowserType, - tmpdir: Path, + tmp_path: Path, browser_channel: Optional[str], assetdir: Path, launch_arguments: Dict, @@ -103,7 +103,7 @@ def test_browser_launch_should_return_background_pages( extension_path = str(assetdir / "simple-extension") context = browser_type.launch_persistent_context( - str(tmpdir), + str(tmp_path), **{ **launch_arguments, "headless": False, diff --git a/tests/sync/test_pdf.py b/tests/sync/test_pdf.py index 684f27268..552d0f6bf 100644 --- a/tests/sync/test_pdf.py +++ b/tests/sync/test_pdf.py @@ -21,8 +21,8 @@ @pytest.mark.only_browser("chromium") -def test_should_be_able_to_save_pdf_file(page: Page, tmpdir: Path) -> None: - output_file = tmpdir / "foo.png" +def test_should_be_able_to_save_pdf_file(page: Page, tmp_path: Path) -> None: + output_file = tmp_path / "foo.png" page.pdf(path=str(output_file)) assert os.path.getsize(output_file) > 0 diff --git a/tests/sync/test_tracing.py b/tests/sync/test_tracing.py index 9308d5d0a..cf08ac0c6 100644 --- a/tests/sync/test_tracing.py +++ b/tests/sync/test_tracing.py @@ -80,7 +80,7 @@ def test_should_collect_sources( def test_should_collect_trace_with_resources_but_no_js( - context: BrowserContext, page: Page, server: Server, tmpdir: Path + context: BrowserContext, page: Page, server: Server, tmp_path: Path ) -> None: context.tracing.start(screenshots=True, snapshots=True) page.goto(server.PREFIX + "/frames/frame.html") @@ -98,7 +98,7 @@ def test_should_collect_trace_with_resources_but_no_js( server.PREFIX + "/one-style.html" ) # should not produce a route.continue_ entry since we continue all routes if no match. page.close() - trace_file_path = tmpdir / "trace.zip" + trace_file_path = tmp_path / "trace.zip" context.tracing.stop(path=trace_file_path) (_, events) = parse_trace(trace_file_path) @@ -140,7 +140,7 @@ def test_should_collect_trace_with_resources_but_no_js( def test_should_correctly_determine_sync_apiname( - context: BrowserContext, page: Page, server: Server, tmpdir: Path + context: BrowserContext, page: Page, server: Server, tmp_path: Path ) -> None: context.tracing.start(screenshots=True, snapshots=True) received_response = threading.Event() @@ -155,7 +155,7 @@ def _handle_response(response: Response) -> None: received_response.wait() page.close() - trace_file_path = tmpdir / "trace.zip" + trace_file_path = tmp_path / "trace.zip" context.tracing.stop(path=trace_file_path) (_, events) = parse_trace(trace_file_path) @@ -167,19 +167,19 @@ def _handle_response(response: Response) -> None: def test_should_collect_two_traces( - context: BrowserContext, page: Page, server: Server, tmpdir: Path + context: BrowserContext, page: Page, server: Server, tmp_path: Path ) -> None: context.tracing.start(screenshots=True, snapshots=True) page.goto(server.EMPTY_PAGE) page.set_content("") page.click('"Click"') - tracing1_path = tmpdir / "trace1.zip" + tracing1_path = tmp_path / "trace1.zip" context.tracing.stop(path=tracing1_path) context.tracing.start(screenshots=True, snapshots=True) page.dblclick('"Click"') page.close() - tracing2_path = tmpdir / "trace2.zip" + tracing2_path = tmp_path / "trace2.zip" context.tracing.stop(path=tracing2_path) (_, events) = parse_trace(tracing1_path) @@ -202,7 +202,7 @@ def test_should_not_throw_when_stopping_without_start_but_not_exporting( def test_should_work_with_playwright_context_managers( - context: BrowserContext, page: Page, server: Server, tmpdir: Path + context: BrowserContext, page: Page, server: Server, tmp_path: Path ) -> None: context.tracing.start(screenshots=True, snapshots=True) page.goto(server.EMPTY_PAGE) @@ -214,7 +214,7 @@ def test_should_work_with_playwright_context_managers( with page.expect_popup(): page.evaluate("window._popup = window.open(document.location.href)") - trace_file_path = tmpdir / "trace.zip" + trace_file_path = tmp_path / "trace.zip" context.tracing.stop(path=trace_file_path) (_, events) = parse_trace(trace_file_path) @@ -231,7 +231,7 @@ def test_should_work_with_playwright_context_managers( def test_should_display_wait_for_load_state_even_if_did_not_wait_for_it( - context: BrowserContext, page: Page, server: Server, tmpdir: Path + context: BrowserContext, page: Page, server: Server, tmp_path: Path ) -> None: context.tracing.start(screenshots=True, snapshots=True) @@ -239,7 +239,7 @@ def test_should_display_wait_for_load_state_even_if_did_not_wait_for_it( page.wait_for_load_state("load") page.wait_for_load_state("load") - trace_file_path = tmpdir / "trace.zip" + trace_file_path = tmp_path / "trace.zip" context.tracing.stop(path=trace_file_path) (_, events) = parse_trace(trace_file_path) @@ -253,23 +253,23 @@ def test_should_display_wait_for_load_state_even_if_did_not_wait_for_it( def test_should_respect_traces_dir_and_name( browser_type: BrowserType, server: Server, - tmpdir: Path, + tmp_path: Path, launch_arguments: Any, ) -> None: - traces_dir = tmpdir / "traces" + traces_dir = tmp_path / "traces" browser = browser_type.launch(traces_dir=traces_dir, **launch_arguments) context = browser.new_context() page = context.new_page() context.tracing.start(name="name1", snapshots=True) page.goto(server.PREFIX + "/one-style.html") - context.tracing.stop_chunk(path=tmpdir / "trace1.zip") + context.tracing.stop_chunk(path=tmp_path / "trace1.zip") assert (traces_dir / "name1.trace").exists() assert (traces_dir / "name1.network").exists() context.tracing.start_chunk(name="name2") page.goto(server.PREFIX + "/har.html") - context.tracing.stop(path=tmpdir / "trace2.zip") + context.tracing.stop(path=tmp_path / "trace2.zip") assert (traces_dir / "name2.trace").exists() assert (traces_dir / "name2.network").exists() @@ -283,7 +283,7 @@ def resource_names(resources: Dict[str, bytes]) -> List[str]: ] ) - (resources, events) = parse_trace(tmpdir / "trace1.zip") + (resources, events) = parse_trace(tmp_path / "trace1.zip") assert get_trace_actions(events) == ["Page.goto"] assert resource_names(resources) == [ "resources/XXX.css", @@ -293,7 +293,7 @@ def resource_names(resources: Dict[str, bytes]) -> List[str]: "trace.trace", ] - (resources, events) = parse_trace(tmpdir / "trace2.zip") + (resources, events) = parse_trace(tmp_path / "trace2.zip") assert get_trace_actions(events) == ["Page.goto"] assert resource_names(resources) == [ "resources/XXX.css", diff --git a/tests/sync/test_video.py b/tests/sync/test_video.py index ec45c1fad..3ae1daa21 100644 --- a/tests/sync/test_video.py +++ b/tests/sync/test_video.py @@ -23,85 +23,85 @@ def test_should_expose_video_path( - browser: Browser, tmpdir: Path, server: Server + browser: Browser, tmp_path: Path, server: Server ) -> None: page = browser.new_page( - record_video_dir=tmpdir, record_video_size={"width": 100, "height": 200} + record_video_dir=tmp_path, record_video_size={"width": 100, "height": 200} ) page.goto(server.PREFIX + "/grid.html") video = page.video assert video path = video.path() assert repr(page.video) == f"