diff --git a/README.md b/README.md index 9a5529b13..b203c6dab 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 133.0.6943.16 | ✅ | ✅ | ✅ | -| WebKit 18.2 | ✅ | ✅ | ✅ | -| Firefox 134.0 | ✅ | ✅ | ✅ | +| Chromium 134.0.6998.35 | ✅ | ✅ | ✅ | +| WebKit 18.4 | ✅ | ✅ | ✅ | +| Firefox 135.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index c5a9022a3..aa56d8244 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -32,6 +32,7 @@ from playwright._impl._errors import is_target_closed_error from playwright._impl._helper import ( ColorScheme, + Contrast, ForcedColors, HarContentPolicy, HarMode, @@ -107,6 +108,7 @@ async def new_context( colorScheme: ColorScheme = None, reducedMotion: ReducedMotion = None, forcedColors: ForcedColors = None, + contrast: Contrast = None, acceptDownloads: bool = None, defaultBrowserType: str = None, proxy: ProxySettings = None, @@ -152,6 +154,7 @@ async def new_page( hasTouch: bool = None, colorScheme: ColorScheme = None, forcedColors: ForcedColors = None, + contrast: Contrast = None, reducedMotion: ReducedMotion = None, acceptDownloads: bool = None, defaultBrowserType: str = None, @@ -254,6 +257,8 @@ async def prepare_browser_context_params(params: Dict) -> None: params["reducedMotion"] = "no-override" if params.get("forcedColors", None) == "null": params["forcedColors"] = "no-override" + if params.get("contrast", None) == "null": + params["contrast"] = "no-override" if "acceptDownloads" in params: params["acceptDownloads"] = "accept" if params["acceptDownloads"] else "deny" diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index e5a9b14fd..22da4375d 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -599,8 +599,12 @@ async def _inner_close() -> None: await self._channel.send("close", {"reason": reason}) await self._closed_future - async def storage_state(self, path: Union[str, Path] = None) -> StorageState: - result = await self._channel.send_return_as_dict("storageState") + async def storage_state( + self, path: Union[str, Path] = None, indexedDB: bool = None + ) -> StorageState: + result = await self._channel.send_return_as_dict( + "storageState", {"indexedDB": indexedDB} + ) if path: await async_writefile(path, json.dumps(result)) return result diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index 1c9303c7f..ec8c988d5 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -35,6 +35,7 @@ from playwright._impl._errors import Error from playwright._impl._helper import ( ColorScheme, + Contrast, Env, ForcedColors, HarContentPolicy, @@ -134,6 +135,7 @@ async def launch_persistent_context( colorScheme: ColorScheme = None, reducedMotion: ReducedMotion = None, forcedColors: ForcedColors = None, + contrast: Contrast = None, acceptDownloads: bool = None, tracesDir: Union[pathlib.Path, str] = None, chromiumSandbox: bool = None, @@ -150,7 +152,7 @@ async def launch_persistent_context( recordHarContent: HarContentPolicy = None, clientCertificates: List[ClientCertificate] = None, ) -> BrowserContext: - userDataDir = str(Path(userDataDir)) if userDataDir else "" + userDataDir = self._user_data_dir(userDataDir) params = locals_to_params(locals()) await prepare_browser_context_params(params) normalize_launch_params(params) @@ -161,6 +163,13 @@ async def launch_persistent_context( self._did_create_context(context, params, params) return context + def _user_data_dir(self, userDataDir: Optional[Union[str, Path]]) -> str: + if not userDataDir: + return "" + if not Path(userDataDir).is_absolute(): + return str(Path(userDataDir).resolve()) + return str(Path(userDataDir)) + async def connect_over_cdp( self, endpointURL: str, diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index 93144ac55..b53e4e629 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -73,6 +73,7 @@ async def new_context( timeout: float = None, storageState: Union[StorageState, str, Path] = None, clientCertificates: List[ClientCertificate] = None, + failOnStatusCode: bool = None, ) -> "APIRequestContext": params = locals_to_params(locals()) if "storageState" in params: @@ -422,9 +423,13 @@ async def _inner_fetch( return APIResponse(self, response) async def storage_state( - self, path: Union[pathlib.Path, str] = None + self, + path: Union[pathlib.Path, str] = None, + indexedDB: bool = None, ) -> StorageState: - result = await self._channel.send_return_as_dict("storageState") + result = await self._channel.send_return_as_dict( + "storageState", {"indexedDB": indexedDB} + ) if path: await async_writefile(path, json.dumps(result)) return result @@ -475,11 +480,14 @@ def headers_array(self) -> network.HeadersArray: async def body(self) -> bytes: try: - result = await self._request._channel.send_return_as_dict( - "fetchResponseBody", - { - "fetchUid": self._fetch_uid, - }, + result = await self._request._connection.wrap_api_call( + lambda: self._request._channel.send_return_as_dict( + "fetchResponseBody", + { + "fetchUid": self._fetch_uid, + }, + ), + True, ) if result is None: raise Error("Response has been disposed") diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 538d5533a..2f7ab57b0 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -62,6 +62,7 @@ ColorScheme = Literal["dark", "light", "no-preference", "null"] ForcedColors = Literal["active", "none", "null"] +Contrast = Literal["more", "no-preference", "null"] ReducedMotion = Literal["no-preference", "null", "reduce"] DocumentLoadState = Literal["commit", "domcontentloaded", "load", "networkidle"] KeyboardModifier = Literal["Alt", "Control", "ControlOrMeta", "Meta", "Shift"] diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 1ad18f999..37b1f9441 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -70,6 +70,7 @@ def __init__( has_not_text: Union[str, Pattern[str]] = None, has: "Locator" = None, has_not: "Locator" = None, + visible: bool = None, ) -> None: self._frame = frame self._selector = selector @@ -95,6 +96,9 @@ def __init__( raise Error('Inner "has_not" locator must belong to the same frame.') self._selector += " >> internal:has-not=" + json.dumps(locator._selector) + if visible is not None: + self._selector += f" >> visible={bool_to_js_bool(visible)}" + def __repr__(self) -> str: return f"" @@ -338,6 +342,7 @@ def filter( hasNotText: Union[str, Pattern[str]] = None, has: "Locator" = None, hasNot: "Locator" = None, + visible: bool = None, ) -> "Locator": return Locator( self._frame, @@ -346,6 +351,7 @@ def filter( has_not_text=hasNotText, has=has, has_not=hasNot, + visible=visible, ) def or_(self, locator: "Locator") -> "Locator": diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 62fec2a3f..6327cce70 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -60,6 +60,7 @@ from playwright._impl._har_router import HarRouter from playwright._impl._helper import ( ColorScheme, + Contrast, DocumentLoadState, ForcedColors, HarMode, @@ -608,6 +609,7 @@ async def emulate_media( colorScheme: ColorScheme = None, reducedMotion: ReducedMotion = None, forcedColors: ForcedColors = None, + contrast: Contrast = None, ) -> None: params = locals_to_params(locals()) if "media" in params: @@ -624,6 +626,10 @@ async def emulate_media( params["forcedColors"] = ( "no-override" if params["forcedColors"] == "null" else forcedColors ) + if "contrast" in params: + params["contrast"] = ( + "no-override" if params["contrast"] == "null" else contrast + ) await self._channel.send("emulateMedia", params) async def set_viewport_size(self, viewportSize: ViewportSize) -> None: diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 7b92fbafb..d2f93dbb6 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -2821,7 +2821,9 @@ async def screenshot( Defaults to `"device"`. mask : Union[Sequence[Locator], None] Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink - box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. + box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. The mask is also applied to + invisible elements, see [Matching only visible elements](../locators.md#matching-only-visible-elements) to disable + that. mask_color : Union[str, None] Specify the color of the overlay box for masked elements, in [CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`. @@ -9277,6 +9279,7 @@ async def emulate_media( Literal["no-preference", "null", "reduce"] ] = None, forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, + contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, ) -> None: """Page.emulate_media @@ -9325,6 +9328,7 @@ async def emulate_media( Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing `null` disables reduced motion emulation. forced_colors : Union["active", "none", "null", None] + contrast : Union["more", "no-preference", "null", None] """ return mapping.from_maybe_impl( @@ -9333,6 +9337,7 @@ async def emulate_media( colorScheme=color_scheme, reducedMotion=reduced_motion, forcedColors=forced_colors, + contrast=contrast, ) ) @@ -9709,7 +9714,9 @@ async def screenshot( Defaults to `"device"`. mask : Union[Sequence[Locator], None] Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink - box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. + box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. The mask is also applied to + invisible elements, see [Matching only visible elements](../locators.md#matching-only-visible-elements) to disable + that. mask_color : Union[str, None] Specify the color of the overlay box for masked elements, in [CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`. @@ -13437,24 +13444,37 @@ async def close(self, *, reason: typing.Optional[str] = None) -> None: return mapping.from_maybe_impl(await self._impl_obj.close(reason=reason)) async def storage_state( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, + *, + path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + indexed_db: typing.Optional[bool] = None, ) -> StorageState: """BrowserContext.storage_state - Returns storage state for this browser context, contains current cookies and local storage snapshot. + Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB + snapshot. Parameters ---------- path : Union[pathlib.Path, str, None] The file path to save the storage state to. If `path` is a relative path, then it is resolved relative to current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. + indexed_db : Union[bool, None] + Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage + 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}]}]} """ - return mapping.from_impl(await self._impl_obj.storage_state(path=path)) + return mapping.from_impl( + await self._impl_obj.storage_state(path=path, indexedDB=indexed_db) + ) async def wait_for_event( self, @@ -13727,6 +13747,7 @@ async def new_context( Literal["no-preference", "null", "reduce"] ] = None, forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, + contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, accept_downloads: typing.Optional[bool] = None, default_browser_type: typing.Optional[str] = None, proxy: typing.Optional[ProxySettings] = None, @@ -13832,6 +13853,10 @@ async def new_context( Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'none'`. + contrast : Union["more", "no-preference", "null", None] + Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See + `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to + `'no-preference'`. accept_downloads : Union[bool, None] Whether to automatically download all the attachments. Defaults to `true` where all the downloads are accepted. proxy : Union[{server: str, bypass: Union[str, None], username: Union[str, None], password: Union[str, None]}, None] @@ -13923,6 +13948,7 @@ async def new_context( colorScheme=color_scheme, reducedMotion=reduced_motion, forcedColors=forced_colors, + contrast=contrast, acceptDownloads=accept_downloads, defaultBrowserType=default_browser_type, proxy=proxy, @@ -13965,6 +13991,7 @@ async def new_page( Literal["dark", "light", "no-preference", "null"] ] = None, forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, + contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, reduced_motion: typing.Optional[ Literal["no-preference", "null", "reduce"] ] = None, @@ -14053,6 +14080,10 @@ async def new_page( Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'none'`. + contrast : Union["more", "no-preference", "null", None] + Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See + `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to + `'no-preference'`. reduced_motion : Union["no-preference", "null", "reduce", None] Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to @@ -14147,6 +14178,7 @@ async def new_page( hasTouch=has_touch, colorScheme=color_scheme, forcedColors=forced_colors, + contrast=contrast, reducedMotion=reduced_motion, acceptDownloads=accept_downloads, defaultBrowserType=default_browser_type, @@ -14480,6 +14512,7 @@ async def launch_persistent_context( Literal["no-preference", "null", "reduce"] ] = None, forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, + contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, accept_downloads: typing.Optional[bool] = None, traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, chromium_sandbox: typing.Optional[bool] = None, @@ -14622,6 +14655,10 @@ async def launch_persistent_context( Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'none'`. + contrast : Union["more", "no-preference", "null", None] + Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See + `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to + `'no-preference'`. accept_downloads : Union[bool, None] Whether to automatically download all the attachments. Defaults to `true` where all the downloads are accepted. traces_dir : Union[pathlib.Path, str, None] @@ -14728,6 +14765,7 @@ async def launch_persistent_context( colorScheme=color_scheme, reducedMotion=reduced_motion, forcedColors=forced_colors, + contrast=contrast, acceptDownloads=accept_downloads, tracesDir=traces_dir, chromiumSandbox=chromium_sandbox, @@ -14762,6 +14800,10 @@ async def connect_over_cdp( **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. + **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via + `browser_type.connect()`. If you are experiencing issues or attempting to use advanced functionality, you + probably want to use `browser_type.connect()`. + **Usage** ```py @@ -14809,14 +14851,15 @@ async def connect( ) -> "Browser": """BrowserType.connect - This method attaches Playwright to an existing browser instance. When connecting to another browser launched via - `BrowserType.launchServer` in Node.js, the major and minor version needs to match the client version (1.2.3 → is - compatible with 1.2.x). + This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js. + + **NOTE** The major and minor version of the Playwright instance that connects needs to match the version of + Playwright that launches the browser (1.2.3 → is compatible with 1.2.x). Parameters ---------- ws_endpoint : str - A browser websocket endpoint to connect to. + A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. timeout : Union[float, None] Maximum time in milliseconds to wait for the connection to be established. Defaults to `0` (no timeout). slow_mo : Union[float, None] @@ -15579,11 +15622,6 @@ async def evaluate( **Usage** - ```py - tweets = page.locator(\".tweet .retweets\") - assert await tweets.evaluate(\"node => node.innerText\") == \"10 retweets\" - ``` - Parameters ---------- expression : str @@ -16397,6 +16435,7 @@ def filter( has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, + visible: typing.Optional[bool] = None, ) -> "Locator": """Locator.filter @@ -16438,6 +16477,8 @@ def filter( outer one. For example, `article` that does not have `div` matches `
Playwright
`. Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s. + visible : Union[bool, None] + Only matches visible or invisible elements. Returns ------- @@ -16450,6 +16491,7 @@ def filter( hasNotText=has_not_text, has=has._impl_obj if has else None, hasNot=has_not._impl_obj if has_not else None, + visible=visible, ) ) @@ -17141,7 +17183,9 @@ async def screenshot( Defaults to `"device"`. mask : Union[Sequence[Locator], None] Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink - box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. + box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. The mask is also applied to + invisible elements, see [Matching only visible elements](../locators.md#matching-only-visible-elements) to disable + that. mask_color : Union[str, None] Specify the color of the overlay box for masked elements, in [CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`. @@ -17283,9 +17327,9 @@ async def select_option( ```html ``` @@ -17446,7 +17490,8 @@ async def tap( ) -> None: """Locator.tap - Perform a tap gesture on the element matching the locator. + Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually + dispatching touch events, see the [emulating legacy touch events](https://playwright.dev/python/docs/touch-events) page. **Details** @@ -18607,7 +18652,10 @@ async def fetch( ) async def storage_state( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, + *, + path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + indexed_db: typing.Optional[bool] = None, ) -> StorageState: """APIRequestContext.storage_state @@ -18619,13 +18667,17 @@ async def storage_state( path : Union[pathlib.Path, str, None] The file path to save the storage state to. If `path` is a relative path, then it is resolved relative to current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. + indexed_db : Union[bool, None] + Set to `true` to include IndexedDB in the storage state snapshot. 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}]}]} """ - return mapping.from_impl(await self._impl_obj.storage_state(path=path)) + return mapping.from_impl( + await self._impl_obj.storage_state(path=path, indexedDB=indexed_db) + ) mapping.register(APIRequestContextImpl, APIRequestContext) @@ -18647,6 +18699,7 @@ async def new_context( typing.Union[StorageState, str, pathlib.Path] ] = None, client_certificates: typing.Optional[typing.List[ClientCertificate]] = None, + fail_on_status_code: typing.Optional[bool] = None, ) -> "APIRequestContext": """APIRequest.new_context @@ -18695,6 +18748,9 @@ async def new_context( **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. + 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. Returns ------- @@ -18712,6 +18768,7 @@ async def new_context( timeout=timeout, storageState=storage_state, clientCertificates=client_certificates, + failOnStatusCode=fail_on_status_code, ) ) @@ -18810,7 +18867,7 @@ async def to_have_url( Time to retry the assertion for in milliseconds. Defaults to `5000`. ignore_case : Union[bool, None] Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular - expression flag if specified. + expression parameter if specified. A provided predicate ignores this flag. """ __tracebackhide__ = True diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 04a0f10fc..619319910 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -2855,7 +2855,9 @@ def screenshot( Defaults to `"device"`. mask : Union[Sequence[Locator], None] Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink - box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. + box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. The mask is also applied to + invisible elements, see [Matching only visible elements](../locators.md#matching-only-visible-elements) to disable + that. mask_color : Union[str, None] Specify the color of the overlay box for masked elements, in [CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`. @@ -9318,6 +9320,7 @@ def emulate_media( Literal["no-preference", "null", "reduce"] ] = None, forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, + contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, ) -> None: """Page.emulate_media @@ -9366,6 +9369,7 @@ def emulate_media( Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing `null` disables reduced motion emulation. forced_colors : Union["active", "none", "null", None] + contrast : Union["more", "no-preference", "null", None] """ return mapping.from_maybe_impl( @@ -9375,6 +9379,7 @@ def emulate_media( colorScheme=color_scheme, reducedMotion=reduced_motion, forcedColors=forced_colors, + contrast=contrast, ) ) ) @@ -9760,7 +9765,9 @@ def screenshot( Defaults to `"device"`. mask : Union[Sequence[Locator], None] Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink - box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. + box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. The mask is also applied to + invisible elements, see [Matching only visible elements](../locators.md#matching-only-visible-elements) to disable + that. mask_color : Union[str, None] Specify the color of the overlay box for masked elements, in [CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`. @@ -13474,24 +13481,37 @@ def close(self, *, reason: typing.Optional[str] = None) -> None: return mapping.from_maybe_impl(self._sync(self._impl_obj.close(reason=reason))) def storage_state( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, + *, + path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + indexed_db: typing.Optional[bool] = None, ) -> StorageState: """BrowserContext.storage_state - Returns storage state for this browser context, contains current cookies and local storage snapshot. + Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB + snapshot. Parameters ---------- path : Union[pathlib.Path, str, None] The file path to save the storage state to. If `path` is a relative path, then it is resolved relative to current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. + indexed_db : Union[bool, None] + Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage + 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}]}]} """ - return mapping.from_impl(self._sync(self._impl_obj.storage_state(path=path))) + return mapping.from_impl( + self._sync(self._impl_obj.storage_state(path=path, indexedDB=indexed_db)) + ) def wait_for_event( self, @@ -13764,6 +13784,7 @@ def new_context( Literal["no-preference", "null", "reduce"] ] = None, forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, + contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, accept_downloads: typing.Optional[bool] = None, default_browser_type: typing.Optional[str] = None, proxy: typing.Optional[ProxySettings] = None, @@ -13869,6 +13890,10 @@ def new_context( Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'none'`. + contrast : Union["more", "no-preference", "null", None] + Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See + `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to + `'no-preference'`. accept_downloads : Union[bool, None] Whether to automatically download all the attachments. Defaults to `true` where all the downloads are accepted. proxy : Union[{server: str, bypass: Union[str, None], username: Union[str, None], password: Union[str, None]}, None] @@ -13961,6 +13986,7 @@ def new_context( colorScheme=color_scheme, reducedMotion=reduced_motion, forcedColors=forced_colors, + contrast=contrast, acceptDownloads=accept_downloads, defaultBrowserType=default_browser_type, proxy=proxy, @@ -14004,6 +14030,7 @@ def new_page( Literal["dark", "light", "no-preference", "null"] ] = None, forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, + contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, reduced_motion: typing.Optional[ Literal["no-preference", "null", "reduce"] ] = None, @@ -14092,6 +14119,10 @@ def new_page( Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'none'`. + contrast : Union["more", "no-preference", "null", None] + Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See + `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to + `'no-preference'`. reduced_motion : Union["no-preference", "null", "reduce", None] Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to @@ -14187,6 +14218,7 @@ def new_page( hasTouch=has_touch, colorScheme=color_scheme, forcedColors=forced_colors, + contrast=contrast, reducedMotion=reduced_motion, acceptDownloads=accept_downloads, defaultBrowserType=default_browser_type, @@ -14525,6 +14557,7 @@ def launch_persistent_context( Literal["no-preference", "null", "reduce"] ] = None, forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, + contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, accept_downloads: typing.Optional[bool] = None, traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, chromium_sandbox: typing.Optional[bool] = None, @@ -14667,6 +14700,10 @@ def launch_persistent_context( Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'none'`. + contrast : Union["more", "no-preference", "null", None] + Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See + `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to + `'no-preference'`. accept_downloads : Union[bool, None] Whether to automatically download all the attachments. Defaults to `true` where all the downloads are accepted. traces_dir : Union[pathlib.Path, str, None] @@ -14774,6 +14811,7 @@ def launch_persistent_context( colorScheme=color_scheme, reducedMotion=reduced_motion, forcedColors=forced_colors, + contrast=contrast, acceptDownloads=accept_downloads, tracesDir=traces_dir, chromiumSandbox=chromium_sandbox, @@ -14809,6 +14847,10 @@ def connect_over_cdp( **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. + **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via + `browser_type.connect()`. If you are experiencing issues or attempting to use advanced functionality, you + probably want to use `browser_type.connect()`. + **Usage** ```py @@ -14858,14 +14900,15 @@ def connect( ) -> "Browser": """BrowserType.connect - This method attaches Playwright to an existing browser instance. When connecting to another browser launched via - `BrowserType.launchServer` in Node.js, the major and minor version needs to match the client version (1.2.3 → is - compatible with 1.2.x). + This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js. + + **NOTE** The major and minor version of the Playwright instance that connects needs to match the version of + Playwright that launches the browser (1.2.3 → is compatible with 1.2.x). Parameters ---------- ws_endpoint : str - A browser websocket endpoint to connect to. + A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. timeout : Union[float, None] Maximum time in milliseconds to wait for the connection to be established. Defaults to `0` (no timeout). slow_mo : Union[float, None] @@ -15637,11 +15680,6 @@ def evaluate( **Usage** - ```py - tweets = page.locator(\".tweet .retweets\") - assert tweets.evaluate(\"node => node.innerText\") == \"10 retweets\" - ``` - Parameters ---------- expression : str @@ -16467,6 +16505,7 @@ def filter( has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, + visible: typing.Optional[bool] = None, ) -> "Locator": """Locator.filter @@ -16507,6 +16546,8 @@ def filter( outer one. For example, `article` that does not have `div` matches `
Playwright
`. Note that outer and inner locators must belong to the same frame. Inner locator must not contain `FrameLocator`s. + visible : Union[bool, None] + Only matches visible or invisible elements. Returns ------- @@ -16519,6 +16560,7 @@ def filter( hasNotText=has_not_text, has=has._impl_obj if has else None, hasNot=has_not._impl_obj if has_not else None, + visible=visible, ) ) @@ -17230,7 +17272,9 @@ def screenshot( Defaults to `"device"`. mask : Union[Sequence[Locator], None] Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink - box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. + box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box. The mask is also applied to + invisible elements, see [Matching only visible elements](../locators.md#matching-only-visible-elements) to disable + that. mask_color : Union[str, None] Specify the color of the overlay box for masked elements, in [CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`. @@ -17374,9 +17418,9 @@ def select_option( ```html ``` @@ -17543,7 +17587,8 @@ def tap( ) -> None: """Locator.tap - Perform a tap gesture on the element matching the locator. + Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually + dispatching touch events, see the [emulating legacy touch events](https://playwright.dev/python/docs/touch-events) page. **Details** @@ -18734,7 +18779,10 @@ def fetch( ) def storage_state( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, + *, + path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + indexed_db: typing.Optional[bool] = None, ) -> StorageState: """APIRequestContext.storage_state @@ -18746,13 +18794,17 @@ def storage_state( path : Union[pathlib.Path, str, None] The file path to save the storage state to. If `path` is a relative path, then it is resolved relative to current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. + indexed_db : Union[bool, None] + Set to `true` to include IndexedDB in the storage state snapshot. 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}]}]} """ - return mapping.from_impl(self._sync(self._impl_obj.storage_state(path=path))) + return mapping.from_impl( + self._sync(self._impl_obj.storage_state(path=path, indexedDB=indexed_db)) + ) mapping.register(APIRequestContextImpl, APIRequestContext) @@ -18774,6 +18826,7 @@ def new_context( typing.Union[StorageState, str, pathlib.Path] ] = None, client_certificates: typing.Optional[typing.List[ClientCertificate]] = None, + fail_on_status_code: typing.Optional[bool] = None, ) -> "APIRequestContext": """APIRequest.new_context @@ -18822,6 +18875,9 @@ def new_context( **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. + 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. Returns ------- @@ -18840,6 +18896,7 @@ def new_context( timeout=timeout, storageState=storage_state, clientCertificates=client_certificates, + failOnStatusCode=fail_on_status_code, ) ) ) @@ -18943,7 +19000,7 @@ def to_have_url( Time to retry the assertion for in milliseconds. Defaults to `5000`. ignore_case : Union[bool, None] Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular - expression flag if specified. + expression parameter if specified. A provided predicate ignores this flag. """ __tracebackhide__ = True diff --git a/scripts/documentation_provider.py b/scripts/documentation_provider.py index 608c4319d..6ea931fac 100644 --- a/scripts/documentation_provider.py +++ b/scripts/documentation_provider.py @@ -489,7 +489,7 @@ def inner_serialize_doc_type(self, type: Any, direction: str) -> str: return "int" if type_name.lower() == "string": return "str" - if type_name == "any" or type_name == "Serializable": + if type_name == "any" or type_name == "unknown" or type_name == "Serializable": return "Any" if type_name == "Object": return "Dict" diff --git a/setup.py b/setup.py index 6168e595e..5403790f2 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import zipfile from typing import Dict -driver_version = "1.50.1-beta-1738589118000" +driver_version = "1.51.0-beta-1741261385000" base_wheel_bundles = [ { diff --git a/tests/async/test_browsercontext.py b/tests/async/test_browsercontext.py index b89ebd7f2..37c812f57 100644 --- a/tests/async/test_browsercontext.py +++ b/tests/async/test_browsercontext.py @@ -825,7 +825,6 @@ async def test_strict_selectors_on_context(browser: Browser, server: Server) -> await context.close() -@pytest.mark.skip_browser("webkit") # https://bugs.webkit.org/show_bug.cgi?id=225281 async def test_should_support_forced_colors(browser: Browser) -> None: context = await browser.new_context(forced_colors="active") page = await context.new_page() diff --git a/tests/async/test_browsercontext_storage_state.py b/tests/async/test_browsercontext_storage_state.py index f11aa8281..a7e853391 100644 --- a/tests/async/test_browsercontext_storage_state.py +++ b/tests/async/test_browsercontext_storage_state.py @@ -16,7 +16,7 @@ import json from pathlib import Path -from playwright.async_api import Browser, BrowserContext, Page +from playwright.async_api import Browser, BrowserContext, Page, StorageState from tests.server import Server @@ -44,16 +44,30 @@ async def test_should_capture_local_storage(context: BrowserContext) -> None: async def test_should_set_local_storage(browser: Browser) -> None: - context = await browser.new_context( - storage_state={ - "origins": [ + storage_state: StorageState = { + "origins": [ + { + "origin": "https://www.example.com", + "localStorage": [{"name": "name1", "value": "value1"}], + } + ] + } + # We intentionally hide the indexed_db part in our API for now + storage_state["origins"][0]["indexedDB"] = [ # type: ignore + { + "name": "db", + "version": 42, + "stores": [ { - "origin": "https://www.example.com", - "localStorage": [{"name": "name1", "value": "value1"}], + "name": "store", + "autoIncrement": False, + "records": [{"key": "bar", "value": "foo"}], + "indexes": [], } - ] + ], } - ) + ] + context = await browser.new_context(storage_state=storage_state) page = await context.new_page() await page.route( @@ -62,6 +76,23 @@ async def test_should_set_local_storage(browser: Browser) -> None: await page.goto("https://www.example.com") local_storage = await page.evaluate("window.localStorage") assert local_storage == {"name1": "value1"} + + indexed_db = await page.evaluate( + """async () => { + return new Promise((resolve, reject) => { + const openRequest = indexedDB.open('db', 42); + openRequest.addEventListener('success', () => { + const db = openRequest.result; + const transaction = db.transaction('store', 'readonly'); + const getRequest = transaction.objectStore('store').get('bar'); + getRequest.addEventListener('success', () => resolve(getRequest.result)); + getRequest.addEventListener('error', () => reject(getRequest.error)); + }); + openRequest.addEventListener('error', () => reject(openRequest.error)); + }); + }""" + ) + assert indexed_db == "foo" await context.close() @@ -112,3 +143,48 @@ async def test_should_serialiser_storage_state_with_lone_surrogates( storage_state = await context.storage_state() # 65533 is the Unicode replacement character assert storage_state["origins"][0]["localStorage"][0]["value"] == chr(65533) + + +async def test_should_serialise_indexed_db(page: Page, server: Server) -> None: + await page.goto(server.EMPTY_PAGE) + await page.evaluate( + """async () => { + await new Promise((resolve, reject) => { + const openRequest = indexedDB.open('db', 42); + openRequest.onupgradeneeded = () => { + openRequest.result.createObjectStore('store'); + }; + openRequest.onsuccess = () => { + const request = openRequest.result.transaction('store', 'readwrite') + .objectStore('store') + .put('foo', 'bar'); + request.addEventListener('success', resolve); + request.addEventListener('error', reject); + }; + }); + }""" + ) + assert await page.context.storage_state() == {"cookies": [], "origins": []} + assert await page.context.storage_state(indexed_db=True) == { + "cookies": [], + "origins": [ + { + "origin": f"http://localhost:{server.PORT}", + "localStorage": [], + "indexedDB": [ + { + "name": "db", + "version": 42, + "stores": [ + { + "name": "store", + "autoIncrement": False, + "records": [{"key": "bar", "value": "foo"}], + "indexes": [], + } + ], + } + ], + } + ], + } diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py index ff3b32489..23466a0af 100644 --- a/tests/async/test_defaultbrowsercontext.py +++ b/tests/async/test_defaultbrowsercontext.py @@ -303,6 +303,16 @@ async def test_should_support_timezone_id_option( ) +async def test_should_support_contrast_option( + launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]", +) -> None: + (page, _) = await launch_persistent(contrast="more") + assert await page.evaluate('() => matchMedia("(prefers-contrast: more)").matches') + assert not await page.evaluate( + '() => matchMedia("(prefers-contrast: no-preference)").matches' + ) + + async def test_should_support_locale_option( launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]", ) -> None: diff --git a/tests/async/test_fetch_global.py b/tests/async/test_fetch_global.py index 838e56c7d..d37697322 100644 --- a/tests/async/test_fetch_global.py +++ b/tests/async/test_fetch_global.py @@ -486,3 +486,41 @@ def _handle_request(req: TestServerRequest) -> None: assert await response.text() == "Hello!" assert request_count == 4 await request.dispose() + + +async def test_should_throw_when_fail_on_status_code_is_true( + playwright: Playwright, server: Server +) -> None: + server.set_route( + "/empty.html", + lambda req: ( + req.setResponseCode(404), + req.setHeader("Content-Length", "10"), + req.setHeader("Content-Type", "text/plain"), + req.write(b"Not found."), + req.finish(), + ), + ) + request = await playwright.request.new_context(fail_on_status_code=True) + with pytest.raises(Error, match="404 Not Found"): + await request.fetch(server.EMPTY_PAGE) + await request.dispose() + + +async def test_should_not_throw_when_fail_on_status_code_is_false( + playwright: Playwright, server: Server +) -> None: + server.set_route( + "/empty.html", + lambda req: ( + req.setResponseCode(404), + req.setHeader("Content-Length", "10"), + req.setHeader("Content-Type", "text/plain"), + req.write(b"Not found."), + req.finish(), + ), + ) + request = await playwright.request.new_context(fail_on_status_code=False) + response = await request.fetch(server.EMPTY_PAGE) + assert response.status == 404 + await request.dispose() diff --git a/tests/async/test_locators.py b/tests/async/test_locators.py index aceb39991..a5891f558 100644 --- a/tests/async/test_locators.py +++ b/tests/async/test_locators.py @@ -532,6 +532,28 @@ async def test_should_combine_visible_with_other_selectors(page: Page) -> None: ) +async def test_should_support_filter_visible(page: Page) -> None: + await page.set_content( + """
+ +
visible data1
+ +
visible data2
+ +
visible data3
+
+ """ + ) + locator = page.locator(".item").filter(visible=True).nth(1) + await expect(locator).to_have_text("visible data2") + await expect( + page.locator(".item").filter(visible=True).get_by_text("data3") + ).to_have_text("visible data3") + await expect( + page.locator(".item").filter(visible=False).get_by_text("data1") + ).to_have_text("Hidden data1") + + async def test_locator_count_should_work_with_deleted_map_in_main_world( page: Page, ) -> None: diff --git a/tests/async/test_page.py b/tests/async/test_page.py index 376df8376..962a11e59 100644 --- a/tests/async/test_page.py +++ b/tests/async/test_page.py @@ -1350,7 +1350,6 @@ async def test_should_set_bodysize_to_0(page: Page, server: Server) -> None: assert sizes["requestHeadersSize"] >= 200 -@pytest.mark.skip_browser("webkit") # https://bugs.webkit.org/show_bug.cgi?id=225281 async def test_should_emulate_forced_colors(page: Page) -> None: assert await page.evaluate("matchMedia('(forced-colors: none)').matches") await page.emulate_media(forced_colors="none") @@ -1361,6 +1360,26 @@ async def test_should_emulate_forced_colors(page: Page) -> None: assert not await page.evaluate("matchMedia('(forced-colors: none)').matches") +async def test_should_emulate_contrast(page: Page) -> None: + assert await page.evaluate( + "matchMedia('(prefers-contrast: no-preference)').matches" + ) + await page.emulate_media(contrast="no-preference") + assert await page.evaluate( + "matchMedia('(prefers-contrast: no-preference)').matches" + ) + assert not await page.evaluate("matchMedia('(prefers-contrast: more)').matches") + await page.emulate_media(contrast="more") + assert not await page.evaluate( + "matchMedia('(prefers-contrast: no-preference)').matches" + ) + assert await page.evaluate("matchMedia('(prefers-contrast: more)').matches") + await page.emulate_media(contrast="null") + assert await page.evaluate( + "matchMedia('(prefers-contrast: no-preference)').matches" + ) + + async def test_should_not_throw_when_continuing_while_page_is_closing( page: Page, server: Server ) -> None: diff --git a/tests/sync/test_browsercontext_storage_state.py b/tests/sync/test_browsercontext_storage_state.py index c785b1479..f7db067d4 100644 --- a/tests/sync/test_browsercontext_storage_state.py +++ b/tests/sync/test_browsercontext_storage_state.py @@ -15,7 +15,8 @@ import json from pathlib import Path -from playwright.sync_api import Browser, BrowserContext +from playwright.sync_api import Browser, BrowserContext, Page, StorageState +from tests.server import Server def test_should_capture_local_storage(context: BrowserContext) -> None: @@ -41,22 +42,53 @@ def test_should_capture_local_storage(context: BrowserContext) -> None: def test_should_set_local_storage(browser: Browser) -> None: - context = browser.new_context( - storage_state={ - "origins": [ + storage_state: StorageState = { + "origins": [ + { + "origin": "https://www.example.com", + "localStorage": [{"name": "name1", "value": "value1"}], + } + ] + } + # We intentionally hide the indexed_db part in our API for now + storage_state["origins"][0]["indexedDB"] = [ # type: ignore + { + "name": "db", + "version": 42, + "stores": [ { - "origin": "https://www.example.com", - "localStorage": [{"name": "name1", "value": "value1"}], + "name": "store", + "autoIncrement": False, + "records": [{"key": "bar", "value": "foo"}], + "indexes": [], } - ] + ], } - ) + ] + context = browser.new_context(storage_state=storage_state) page = context.new_page() page.route("**/*", lambda route: route.fulfill(body="")) page.goto("https://www.example.com") local_storage = page.evaluate("window.localStorage") assert local_storage == {"name1": "value1"} + + indexed_db = page.evaluate( + """async () => { + return new Promise((resolve, reject) => { + const openRequest = indexedDB.open('db', 42); + openRequest.addEventListener('success', () => { + const db = openRequest.result; + const transaction = db.transaction('store', 'readonly'); + const getRequest = transaction.objectStore('store').get('bar'); + getRequest.addEventListener('success', () => resolve(getRequest.result)); + getRequest.addEventListener('error', () => reject(getRequest.error)); + }); + openRequest.addEventListener('error', () => reject(openRequest.error)); + }); + }""" + ) + assert indexed_db == "foo" context.close() @@ -95,3 +127,48 @@ def test_should_round_trip_through_the_file( cookie = page2.evaluate("document.cookie") assert cookie == "username=John Doe" context2.close() + + +def test_should_serialise_indexed_db(page: Page, server: Server) -> None: + page.goto(server.EMPTY_PAGE) + page.evaluate( + """async () => { + await new Promise((resolve, reject) => { + const openRequest = indexedDB.open('db', 42); + openRequest.onupgradeneeded = () => { + openRequest.result.createObjectStore('store'); + }; + openRequest.onsuccess = () => { + const request = openRequest.result.transaction('store', 'readwrite') + .objectStore('store') + .put('foo', 'bar'); + request.addEventListener('success', resolve); + request.addEventListener('error', reject); + }; + }); + }""" + ) + assert page.context.storage_state() == {"cookies": [], "origins": []} + assert page.context.storage_state(indexed_db=True) == { + "cookies": [], + "origins": [ + { + "origin": f"http://localhost:{server.PORT}", + "localStorage": [], + "indexedDB": [ + { + "name": "db", + "version": 42, + "stores": [ + { + "name": "store", + "autoIncrement": False, + "records": [{"key": "bar", "value": "foo"}], + "indexes": [], + } + ], + } + ], + } + ], + } diff --git a/tests/sync/test_fetch_global.py b/tests/sync/test_fetch_global.py index 5c25d4059..b7420253b 100644 --- a/tests/sync/test_fetch_global.py +++ b/tests/sync/test_fetch_global.py @@ -323,3 +323,41 @@ def test_should_serialize_null_values_in_json( assert response.status == 200 assert response.text() == '{"foo": null}' request.dispose() + + +def test_should_throw_when_fail_on_status_code_is_true( + playwright: Playwright, server: Server +) -> None: + server.set_route( + "/empty.html", + lambda req: ( + req.setResponseCode(404), + req.setHeader("Content-Length", "10"), + req.setHeader("Content-Type", "text/plain"), + req.write(b"Not found."), + req.finish(), + ), + ) + request = playwright.request.new_context(fail_on_status_code=True) + with pytest.raises(Error, match="404 Not Found"): + request.fetch(server.EMPTY_PAGE) + request.dispose() + + +def test_should_not_throw_when_fail_on_status_code_is_false( + playwright: Playwright, server: Server +) -> None: + server.set_route( + "/empty.html", + lambda req: ( + req.setResponseCode(404), + req.setHeader("Content-Length", "10"), + req.setHeader("Content-Type", "text/plain"), + req.write(b"Not found."), + req.finish(), + ), + ) + request = playwright.request.new_context(fail_on_status_code=False) + response = request.fetch(server.EMPTY_PAGE) + assert response.status == 404 + request.dispose() diff --git a/tests/sync/test_locators.py b/tests/sync/test_locators.py index f373abdaa..31d7b174b 100644 --- a/tests/sync/test_locators.py +++ b/tests/sync/test_locators.py @@ -493,6 +493,28 @@ def test_should_combine_visible_with_other_selectors(page: Page) -> None: ) +def test_should_support_filter_visible(page: Page) -> None: + page.set_content( + """
+ +
visible data1
+ +
visible data2
+ +
visible data3
+
+ """ + ) + locator = page.locator(".item").filter(visible=True).nth(1) + expect(locator).to_have_text("visible data2") + expect( + page.locator(".item").filter(visible=True).get_by_text("data3") + ).to_have_text("visible data3") + expect( + page.locator(".item").filter(visible=False).get_by_text("data1") + ).to_have_text("Hidden data1") + + def test_locator_count_should_work_with_deleted_map_in_main_world(page: Page) -> None: page.evaluate("Map = 1") page.locator("#searchResultTableDiv .x-grid3-row").count()