From 1a4bdf70310980344c8c22650857e1944103629c Mon Sep 17 00:00:00 2001 From: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com> Date: Wed, 24 Mar 2021 10:47:51 +0530 Subject: [PATCH 001/140] chore: roll to Playwright v1.10.0 (#591) Co-authored-by: Andrey Lushnikov --- playwright/_impl/_browser_type.py | 5 ++- playwright/_impl/_helper.py | 11 +++++ playwright/async_api/_generated.py | 69 +++++++++++++++++------------- playwright/sync_api/_generated.py | 69 +++++++++++++++++------------- setup.py | 2 +- 5 files changed, 93 insertions(+), 63 deletions(-) diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index 358d43a07..69ec77df5 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -25,6 +25,7 @@ from playwright._impl._browser_context import BrowserContext from playwright._impl._connection import ChannelOwner, from_channel from playwright._impl._helper import ( + BrowserChannel, ColorScheme, Env, locals_to_params, @@ -52,7 +53,7 @@ def executable_path(self) -> str: async def launch( self, executablePath: Union[str, Path] = None, - channel: str = None, + channel: BrowserChannel = None, args: List[str] = None, ignoreDefaultArgs: Union[bool, List[str]] = None, handleSIGINT: bool = None, @@ -80,7 +81,7 @@ async def launch( async def launch_persistent_context( self, userDataDir: Union[str, Path], - channel: str = None, + channel: BrowserChannel = None, executablePath: Union[str, Path] = None, args: List[str] = None, ignoreDefaultArgs: Union[bool, List[str]] = None, diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index e68fb36de..7f961df19 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -52,6 +52,17 @@ KeyboardModifier = Literal["Alt", "Control", "Meta", "Shift"] MouseButton = Literal["left", "middle", "right"] +BrowserChannel = Literal[ + "chrome", + "chrome-beta", + "chrome-canary", + "chrome-dev", + "msedge", + "msedge-beta", + "msedge-canary", + "msedge-dev", +] + class ErrorPayload(TypedDict, total=False): message: str diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 0c4ae2846..dbed04d65 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -92,8 +92,6 @@ def resource_type(self) -> str: following: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttrack`, `xhr`, `fetch`, `eventsource`, `websocket`, `manifest`, `other`. - > NOTE: The resource types are available as constants in [ResourceTypes]. - Returns ------- str @@ -8641,11 +8639,14 @@ async def new_context( option to work. If all contexts override the proxy, global proxy will be never used and can be any string, for example `launch({ proxy: { server: 'per-context' } })`. record_har_path : Union[pathlib.Path, str, NoneType] - Path on the filesystem to write the HAR file to. + Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the + filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` for the HAR to + be saved. record_har_omit_content : Union[bool, NoneType] Optional setting to control whether to omit request content from the HAR. Defaults to `false`. record_video_dir : Union[pathlib.Path, str, NoneType] - Path to the directory to put videos into. + Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure + to call `browser_context.close()` for videos to be saved. record_video_size : Union[{width: int, height: int}, NoneType] Dimensions of the recorded videos. If not specified the size will be equal to `viewport` scaled down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of each page will @@ -8778,11 +8779,14 @@ async def new_page( option to work. If all contexts override the proxy, global proxy will be never used and can be any string, for example `launch({ proxy: { server: 'per-context' } })`. record_har_path : Union[pathlib.Path, str, NoneType] - Path on the filesystem to write the HAR file to. + Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the + filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` for the HAR to + be saved. record_har_omit_content : Union[bool, NoneType] Optional setting to control whether to omit request content from the HAR. Defaults to `false`. record_video_dir : Union[pathlib.Path, str, NoneType] - Path to the directory to put videos into. + Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure + to call `browser_context.close()` for videos to be saved. record_video_size : Union[{width: int, height: int}, NoneType] Dimensions of the recorded videos. If not specified the size will be equal to `viewport` scaled down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of each page will @@ -8882,7 +8886,16 @@ async def launch( self, *, executable_path: typing.Union[str, pathlib.Path] = None, - channel: str = None, + channel: Literal[ + "chrome", + "chrome-beta", + "chrome-canary", + "chrome-dev", + "msedge", + "msedge-beta", + "msedge-canary", + "msedge-dev", + ] = None, args: typing.List[str] = None, ignore_default_args: typing.Union[bool, typing.List[str]] = None, handle_sigint: bool = None, @@ -8933,16 +8946,8 @@ async def launch( Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium, Firefox or WebKit, use at your own risk. - channel : Union[str, NoneType] - Chromium distribution channel, one of - - chrome - - chrome-beta - - chrome-dev - - chrome-canary - - msedge - - msedge-beta - - msedge-dev - - msedge-canary + channel : Union["chrome", "chrome-beta", "chrome-canary", "chrome-dev", "msedge", "msedge-beta", "msedge-canary", "msedge-dev", NoneType] + Browser distribution channel. args : Union[List[str], NoneType] Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). @@ -9014,7 +9019,16 @@ async def launch_persistent_context( self, user_data_dir: typing.Union[str, pathlib.Path], *, - channel: str = None, + channel: Literal[ + "chrome", + "chrome-beta", + "chrome-canary", + "chrome-dev", + "msedge", + "msedge-beta", + "msedge-canary", + "msedge-dev", + ] = None, executable_path: typing.Union[str, pathlib.Path] = None, args: typing.List[str] = None, ignore_default_args: typing.Union[bool, typing.List[str]] = None, @@ -9066,16 +9080,8 @@ async def launch_persistent_context( [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`. - channel : Union[str, NoneType] - Chromium distribution channel, one of - - chrome - - chrome-beta - - chrome-dev - - chrome-canary - - msedge - - msedge-beta - - msedge-dev - - msedge-canary + channel : Union["chrome", "chrome-beta", "chrome-canary", "chrome-dev", "msedge", "msedge-beta", "msedge-canary", "msedge-dev", NoneType] + Browser distribution channel. executable_path : Union[pathlib.Path, str, NoneType] Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to the current working directory. **BEWARE**: Playwright is only guaranteed to work with the bundled @@ -9157,11 +9163,14 @@ async def launch_persistent_context( chromium_sandbox : Union[bool, NoneType] Enable Chromium sandboxing. Defaults to `true`. record_har_path : Union[pathlib.Path, str, NoneType] - Path on the filesystem to write the HAR file to. + Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the + filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` for the HAR to + be saved. record_har_omit_content : Union[bool, NoneType] Optional setting to control whether to omit request content from the HAR. Defaults to `false`. record_video_dir : Union[pathlib.Path, str, NoneType] - Path to the directory to put videos into. + Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure + to call `browser_context.close()` for videos to be saved. record_video_size : Union[{width: int, height: int}, NoneType] Dimensions of the recorded videos. If not specified the size will be equal to `viewport` scaled down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of each page will diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index a6521137a..da05b60c5 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -92,8 +92,6 @@ def resource_type(self) -> str: following: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttrack`, `xhr`, `fetch`, `eventsource`, `websocket`, `manifest`, `other`. - > NOTE: The resource types are available as constants in [ResourceTypes]. - Returns ------- str @@ -8587,11 +8585,14 @@ def new_context( option to work. If all contexts override the proxy, global proxy will be never used and can be any string, for example `launch({ proxy: { server: 'per-context' } })`. record_har_path : Union[pathlib.Path, str, NoneType] - Path on the filesystem to write the HAR file to. + Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the + filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` for the HAR to + be saved. record_har_omit_content : Union[bool, NoneType] Optional setting to control whether to omit request content from the HAR. Defaults to `false`. record_video_dir : Union[pathlib.Path, str, NoneType] - Path to the directory to put videos into. + Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure + to call `browser_context.close()` for videos to be saved. record_video_size : Union[{width: int, height: int}, NoneType] Dimensions of the recorded videos. If not specified the size will be equal to `viewport` scaled down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of each page will @@ -8724,11 +8725,14 @@ def new_page( option to work. If all contexts override the proxy, global proxy will be never used and can be any string, for example `launch({ proxy: { server: 'per-context' } })`. record_har_path : Union[pathlib.Path, str, NoneType] - Path on the filesystem to write the HAR file to. + Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the + filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` for the HAR to + be saved. record_har_omit_content : Union[bool, NoneType] Optional setting to control whether to omit request content from the HAR. Defaults to `false`. record_video_dir : Union[pathlib.Path, str, NoneType] - Path to the directory to put videos into. + Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure + to call `browser_context.close()` for videos to be saved. record_video_size : Union[{width: int, height: int}, NoneType] Dimensions of the recorded videos. If not specified the size will be equal to `viewport` scaled down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of each page will @@ -8828,7 +8832,16 @@ def launch( self, *, executable_path: typing.Union[str, pathlib.Path] = None, - channel: str = None, + channel: Literal[ + "chrome", + "chrome-beta", + "chrome-canary", + "chrome-dev", + "msedge", + "msedge-beta", + "msedge-canary", + "msedge-dev", + ] = None, args: typing.List[str] = None, ignore_default_args: typing.Union[bool, typing.List[str]] = None, handle_sigint: bool = None, @@ -8879,16 +8892,8 @@ def launch( Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium, Firefox or WebKit, use at your own risk. - channel : Union[str, NoneType] - Chromium distribution channel, one of - - chrome - - chrome-beta - - chrome-dev - - chrome-canary - - msedge - - msedge-beta - - msedge-dev - - msedge-canary + channel : Union["chrome", "chrome-beta", "chrome-canary", "chrome-dev", "msedge", "msedge-beta", "msedge-canary", "msedge-dev", NoneType] + Browser distribution channel. args : Union[List[str], NoneType] Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). @@ -8960,7 +8965,16 @@ def launch_persistent_context( self, user_data_dir: typing.Union[str, pathlib.Path], *, - channel: str = None, + channel: Literal[ + "chrome", + "chrome-beta", + "chrome-canary", + "chrome-dev", + "msedge", + "msedge-beta", + "msedge-canary", + "msedge-dev", + ] = None, executable_path: typing.Union[str, pathlib.Path] = None, args: typing.List[str] = None, ignore_default_args: typing.Union[bool, typing.List[str]] = None, @@ -9012,16 +9026,8 @@ def launch_persistent_context( [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`. - channel : Union[str, NoneType] - Chromium distribution channel, one of - - chrome - - chrome-beta - - chrome-dev - - chrome-canary - - msedge - - msedge-beta - - msedge-dev - - msedge-canary + channel : Union["chrome", "chrome-beta", "chrome-canary", "chrome-dev", "msedge", "msedge-beta", "msedge-canary", "msedge-dev", NoneType] + Browser distribution channel. executable_path : Union[pathlib.Path, str, NoneType] Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to the current working directory. **BEWARE**: Playwright is only guaranteed to work with the bundled @@ -9103,11 +9109,14 @@ def launch_persistent_context( chromium_sandbox : Union[bool, NoneType] Enable Chromium sandboxing. Defaults to `true`. record_har_path : Union[pathlib.Path, str, NoneType] - Path on the filesystem to write the HAR file to. + Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the + filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` for the HAR to + be saved. record_har_omit_content : Union[bool, NoneType] Optional setting to control whether to omit request content from the HAR. Defaults to `false`. record_video_dir : Union[pathlib.Path, str, NoneType] - Path to the directory to put videos into. + Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure + to call `browser_context.close()` for videos to be saved. record_video_size : Union[{width: int, height: int}, NoneType] Dimensions of the recorded videos. If not specified the size will be equal to `viewport` scaled down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of each page will diff --git a/setup.py b/setup.py index ac3464c6a..b644a46be 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ from auditwheel.wheeltools import InWheel from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.10.0-next-1616094973000" +driver_version = "1.10.0-next-1616530863000" def extractall(zip: zipfile.ZipFile, path: str) -> None: From 140ad1beda99fd1c005034fb479ce734c0339115 Mon Sep 17 00:00:00 2001 From: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com> Date: Sat, 27 Mar 2021 19:51:31 +0530 Subject: [PATCH 002/140] fix: remove pre-release docs from README (#600) --- README.md | 36 ------------------------------------ playwright/__init__.py | 2 -- 2 files changed, 38 deletions(-) diff --git a/README.md b/README.md index b7588f005..5d2815c45 100644 --- a/README.md +++ b/README.md @@ -288,39 +288,3 @@ asyncio.run(main()) ## Documentation Check out our [new documentation site](https://playwright.dev/python/docs/intro)! - -## Is Playwright ready? - -Yes, Playwright for Python is ready! The latest version of Playwright for -Python is 1.8.0a. We are ready to drop the Alpha bit once we hear from you. -Once it is gone, we will become semver compatible and the API will be -frozen in its present form for years. We will still be adding features with -every release, but we promise to not break it anymore! - -## Migration from the pre-release versions - -The API has changed since the last 0.170.0 version: - -- Snake case notation for methods and arguments: - - ```py - # old - browser.newPage() - ``` - - ```py - # new - browser.new_page() - ``` - -- Import has changed to include sync vs async mode explicitly: - ```py - # old - from playwright import sync_playwright - ``` - ```py - # new - from playwright.sync_api import sync_playwright - ``` - -That's about it! Our new [doc site](https://playwright.dev/python/docs/intro) uses proper notation and examples for the new API. diff --git a/playwright/__init__.py b/playwright/__init__.py index a3ee486ad..16593a9f2 100644 --- a/playwright/__init__.py +++ b/playwright/__init__.py @@ -16,6 +16,4 @@ Python package `playwright` is a Python library to automate Chromium, Firefox and WebKit with a single API. Playwright is built to enable cross-browser web automation that is ever-green, capable, reliable and fast. -For more information you'll find the documentation for the sync API [here](sync_api.html) -and for the async API [here](async_api.html). """ From ea66054dc769efcf4d8f87b6b442f24b2736b21c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 1 Apr 2021 04:51:55 +0800 Subject: [PATCH 003/140] feat(roll): toll to the ToT 1617207797000 (#606) --- playwright/_impl/_browser.py | 2 + playwright/_impl/_browser_type.py | 1 + playwright/_impl/_frame.py | 15 ++++ playwright/_impl/_page.py | 8 ++ playwright/async_api/_generated.py | 134 +++++++++++++++++++++++++---- playwright/sync_api/_generated.py | 134 +++++++++++++++++++++++++---- setup.py | 2 +- tests/async/test_navigation.py | 8 +- tests/async/test_video.py | 4 + tests/async/test_wait_for_url.py | 122 ++++++++++++++++++++++++++ 10 files changed, 393 insertions(+), 37 deletions(-) create mode 100644 tests/async/test_wait_for_url.py diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index fe84432f3..4f6ebe639 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -69,6 +69,7 @@ def is_connected(self) -> bool: async def new_context( self, viewport: ViewportSize = None, + screen: ViewportSize = None, noViewport: bool = None, ignoreHTTPSErrors: bool = None, javaScriptEnabled: bool = None, @@ -107,6 +108,7 @@ async def new_context( async def new_page( self, viewport: ViewportSize = None, + screen: ViewportSize = None, noViewport: bool = None, ignoreHTTPSErrors: bool = None, javaScriptEnabled: bool = None, diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index 69ec77df5..1070b57ef 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -96,6 +96,7 @@ async def launch_persistent_context( downloadsPath: Union[str, Path] = None, slowMo: float = None, viewport: ViewportSize = None, + screen: ViewportSize = None, noViewport: bool = None, ignoreHTTPSErrors: bool = None, javaScriptEnabled: bool = None, diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 04b4ac37a..5698a90f4 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -181,6 +181,21 @@ async def continuation() -> Optional[Response]: return EventContextManagerImpl(asyncio.create_task(continuation())) + async def wait_for_url( + self, + url: URLMatch, + wait_until: DocumentLoadState = None, + timeout: float = None, + ) -> None: + matcher = URLMatcher(url) + if matcher.matches(self.url): + await self.wait_for_load_state(state=wait_until, timeout=timeout) + return + async with self.expect_navigation( + url=url, wait_until=wait_until, timeout=timeout + ): + pass + async def wait_for_load_state( self, state: DocumentLoadState = None, timeout: float = None ) -> None: diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 39f9568f4..1ba1573d6 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -477,6 +477,14 @@ async def wait_for_load_state( ) -> None: return await self._main_frame.wait_for_load_state(**locals_to_params(locals())) + async def wait_for_url( + self, + url: URLMatch, + wait_until: DocumentLoadState = None, + timeout: float = None, + ) -> None: + return await self._main_frame.wait_for_url(**locals_to_params(locals())) + async def wait_for_event( self, event: str, predicate: Callable = None, timeout: float = None ) -> Any: diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index dbed04d65..db311a7ae 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -819,7 +819,7 @@ async def press(self, key: str, *, delay: float = None) -> NoneType: If `key` is a single character, it is case-sensitive, so the values `a` and `A` will generate different respective texts. - Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When speficied with the + Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed. ```py @@ -1390,8 +1390,8 @@ async def dispatch_event( ) -> NoneType: """ElementHandle.dispatch_event - The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the elment, `click` - is dispatched. This is equivalend to calling + The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the element, + `click` is dispatched. This is equivalent to calling [element.click()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click). ```py @@ -1948,7 +1948,7 @@ async def press( If `key` is a single character, it is case-sensitive, so the values `a` and `A` will generate different respective texts. - Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When speficied with the + Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed. Parameters @@ -2709,6 +2709,47 @@ def expect_navigation( ).future ) + async def wait_for_url( + self, + url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]], + *, + wait_until: Literal["domcontentloaded", "load", "networkidle"] = None, + timeout: float = None + ) -> NoneType: + """Frame.wait_for_url + + Waits for the frame to navigate to the given URL. + + ```py + await frame.click(\"a.delayed-navigation\") # clicking the link will indirectly cause a navigation + await frame.wait_for_url(\"**/target.html\") + ``` + + Parameters + ---------- + url : Union[Callable[[str], bool], Pattern, str] + A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + wait_until : Union["domcontentloaded", "load", "networkidle", NoneType] + When to consider operation succeeded, defaults to `load`. Events can be either: + - `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired. + - `'load'` - consider operation to be finished when the `load` event is fired. + - `'networkidle'` - consider operation to be finished when there are no network connections for at least `500` ms. + timeout : Union[float, NoneType] + Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be + changed by using the `browser_context.set_default_navigation_timeout()`, + `browser_context.set_default_timeout()`, `page.set_default_navigation_timeout()` or + `page.set_default_timeout()` methods. + """ + + return mapping.from_maybe_impl( + await self._async( + "frame.wait_for_url", + self._impl_obj.wait_for_url( + url=self._wrap_handler(url), wait_until=wait_until, timeout=timeout + ), + ) + ) + async def wait_for_load_state( self, state: Literal["domcontentloaded", "load", "networkidle"] = None, @@ -3169,8 +3210,8 @@ async def dispatch_event( ) -> NoneType: """Frame.dispatch_event - The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the elment, `click` - is dispatched. This is equivalend to calling + The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the element, + `click` is dispatched. This is equivalent to calling [element.click()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click). ```py @@ -4089,7 +4130,7 @@ async def press( If `key` is a single character, it is case-sensitive, so the values `a` and `A` will generate different respective texts. - Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When speficied with the + Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed. Parameters @@ -4894,6 +4935,7 @@ def set_default_navigation_timeout(self, timeout: float) -> NoneType: - `page.reload()` - `page.set_content()` - `page.expect_navigation()` + - `page.wait_for_url()` > NOTE: `page.set_default_navigation_timeout()` takes priority over `page.set_default_timeout()`, `browser_context.set_default_timeout()` and `browser_context.set_default_navigation_timeout()`. @@ -5210,8 +5252,8 @@ async def dispatch_event( ) -> NoneType: """Page.dispatch_event - The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the elment, `click` - is dispatched. This is equivalend to calling + The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the element, + `click` is dispatched. This is equivalent to calling [element.click()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click). ```py @@ -5904,6 +5946,49 @@ async def wait_for_load_state( ) ) + async def wait_for_url( + self, + url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]], + *, + wait_until: Literal["domcontentloaded", "load", "networkidle"] = None, + timeout: float = None + ) -> NoneType: + """Page.wait_for_url + + Waits for the main frame to navigate to the given URL. + + ```py + await page.click(\"a.delayed-navigation\") # clicking the link will indirectly cause a navigation + await page.wait_for_url(\"**/target.html\") + ``` + + Shortcut for main frame's `frame.wait_for_url()`. + + Parameters + ---------- + url : Union[Callable[[str], bool], Pattern, str] + A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + wait_until : Union["domcontentloaded", "load", "networkidle", NoneType] + When to consider operation succeeded, defaults to `load`. Events can be either: + - `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired. + - `'load'` - consider operation to be finished when the `load` event is fired. + - `'networkidle'` - consider operation to be finished when there are no network connections for at least `500` ms. + timeout : Union[float, NoneType] + Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be + changed by using the `browser_context.set_default_navigation_timeout()`, + `browser_context.set_default_timeout()`, `page.set_default_navigation_timeout()` or + `page.set_default_timeout()` methods. + """ + + return mapping.from_maybe_impl( + await self._async( + "page.wait_for_url", + self._impl_obj.wait_for_url( + url=self._wrap_handler(url), wait_until=wait_until, timeout=timeout + ), + ) + ) + async def wait_for_event( self, event: str, predicate: typing.Callable = None, *, timeout: float = None ) -> typing.Any: @@ -6163,7 +6248,7 @@ async def route( > NOTE: The handler will only be called for the first url if the response is a redirect. - An example of a naïve handler that aborts all image requests: + An example of a naive handler that aborts all image requests: ```py page = await browser.new_page() @@ -6184,6 +6269,8 @@ async def route( Page routes take precedence over browser context routes (set up with `browser_context.route()`) when request matches both handlers. + To remove a route with its handler you can use `page.unroute()`. + > NOTE: Enabling routing disables http cache. Parameters @@ -6247,9 +6334,6 @@ async def screenshot( Returns the buffer with the captured screenshot. - > NOTE: Screenshots take at least 1/6 second on Chromium OS X and Chromium Windows. See https://crbug.com/741689 for - discussion. - Parameters ---------- timeout : Union[float, NoneType] @@ -7001,7 +7085,7 @@ async def press( If `key` is a single character, it is case-sensitive, so the values `a` and `A` will generate different respective texts. - Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When speficied with the + Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed. ```py @@ -8190,7 +8274,7 @@ async def route( Routing provides the capability to modify network requests that are made by any page in the browser context. Once route is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. - An example of a naïve handler that aborts all image requests: + An example of a naive handler that aborts all image requests: ```py context = await browser.new_context() @@ -8214,6 +8298,8 @@ async def route( Page routes (set up with `page.route()`) take precedence over browser context routes when request matches both handlers. + To remove a route with its handler you can use `browser_context.unroute()`. + > NOTE: Enabling routing disables http cache. Parameters @@ -8553,6 +8639,7 @@ async def new_context( self, *, viewport: ViewportSize = None, + screen: ViewportSize = None, no_viewport: bool = None, ignore_https_errors: bool = None, java_script_enabled: bool = None, @@ -8595,6 +8682,9 @@ async def new_context( ---------- viewport : Union[{width: int, height: int}, NoneType] Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `no_viewport` disables the fixed viewport. + screen : Union[{width: int, height: int}, NoneType] + Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport` + is set. no_viewport : Union[bool, NoneType] Does not enforce fixed viewport, allows resizing window in the headed mode. ignore_https_errors : Union[bool, NoneType] @@ -8666,6 +8756,7 @@ async def new_context( "browser.new_context", self._impl_obj.new_context( viewport=viewport, + screen=screen, noViewport=no_viewport, ignoreHTTPSErrors=ignore_https_errors, javaScriptEnabled=java_script_enabled, @@ -8698,6 +8789,7 @@ async def new_page( self, *, viewport: ViewportSize = None, + screen: ViewportSize = None, no_viewport: bool = None, ignore_https_errors: bool = None, java_script_enabled: bool = None, @@ -8735,6 +8827,9 @@ async def new_page( ---------- viewport : Union[{width: int, height: int}, NoneType] Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `no_viewport` disables the fixed viewport. + screen : Union[{width: int, height: int}, NoneType] + Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport` + is set. no_viewport : Union[bool, NoneType] Does not enforce fixed viewport, allows resizing window in the headed mode. ignore_https_errors : Union[bool, NoneType] @@ -8806,6 +8901,7 @@ async def new_page( "browser.new_page", self._impl_obj.new_page( viewport=viewport, + screen=screen, noViewport=no_viewport, ignoreHTTPSErrors=ignore_https_errors, javaScriptEnabled=java_script_enabled, @@ -8947,7 +9043,8 @@ async def launch( resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium, Firefox or WebKit, use at your own risk. channel : Union["chrome", "chrome-beta", "chrome-canary", "chrome-dev", "msedge", "msedge-beta", "msedge-canary", "msedge-dev", NoneType] - Browser distribution channel. + Browser distribution channel. Read more about using + [Google Chrome and Microsoft Edge](./browsers#google-chrome--microsoft-edge). args : Union[List[str], NoneType] Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). @@ -9043,6 +9140,7 @@ async def launch_persistent_context( downloads_path: typing.Union[str, pathlib.Path] = None, slow_mo: float = None, viewport: ViewportSize = None, + screen: ViewportSize = None, no_viewport: bool = None, ignore_https_errors: bool = None, java_script_enabled: bool = None, @@ -9121,6 +9219,9 @@ async def launch_persistent_context( Defaults to 0. viewport : Union[{width: int, height: int}, NoneType] Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `no_viewport` disables the fixed viewport. + screen : Union[{width: int, height: int}, NoneType] + Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport` + is set. no_viewport : Union[bool, NoneType] Does not enforce fixed viewport, allows resizing window in the headed mode. ignore_https_errors : Union[bool, NoneType] @@ -9201,6 +9302,7 @@ async def launch_persistent_context( downloadsPath=downloads_path, slowMo=slow_mo, viewport=viewport, + screen=screen, noViewport=no_viewport, ignoreHTTPSErrors=ignore_https_errors, javaScriptEnabled=java_script_enabled, diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index da05b60c5..dcf6e0375 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -815,7 +815,7 @@ def press(self, key: str, *, delay: float = None) -> NoneType: If `key` is a single character, it is case-sensitive, so the values `a` and `A` will generate different respective texts. - Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When speficied with the + Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed. ```py @@ -1368,8 +1368,8 @@ def is_visible(self) -> bool: def dispatch_event(self, type: str, event_init: typing.Dict = None) -> NoneType: """ElementHandle.dispatch_event - The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the elment, `click` - is dispatched. This is equivalend to calling + The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the element, + `click` is dispatched. This is equivalent to calling [element.click()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click). ```py @@ -1937,7 +1937,7 @@ def press( If `key` is a single character, it is case-sensitive, so the values `a` and `A` will generate different respective texts. - Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When speficied with the + Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed. Parameters @@ -2696,6 +2696,47 @@ def expect_navigation( ).future, ) + def wait_for_url( + self, + url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]], + *, + wait_until: Literal["domcontentloaded", "load", "networkidle"] = None, + timeout: float = None + ) -> NoneType: + """Frame.wait_for_url + + Waits for the frame to navigate to the given URL. + + ```py + frame.click(\"a.delayed-navigation\") # clicking the link will indirectly cause a navigation + frame.wait_for_url(\"**/target.html\") + ``` + + Parameters + ---------- + url : Union[Callable[[str], bool], Pattern, str] + A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + wait_until : Union["domcontentloaded", "load", "networkidle", NoneType] + When to consider operation succeeded, defaults to `load`. Events can be either: + - `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired. + - `'load'` - consider operation to be finished when the `load` event is fired. + - `'networkidle'` - consider operation to be finished when there are no network connections for at least `500` ms. + timeout : Union[float, NoneType] + Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be + changed by using the `browser_context.set_default_navigation_timeout()`, + `browser_context.set_default_timeout()`, `page.set_default_navigation_timeout()` or + `page.set_default_timeout()` methods. + """ + + return mapping.from_maybe_impl( + self._sync( + "frame.wait_for_url", + self._impl_obj.wait_for_url( + url=self._wrap_handler(url), wait_until=wait_until, timeout=timeout + ), + ) + ) + def wait_for_load_state( self, state: Literal["domcontentloaded", "load", "networkidle"] = None, @@ -3151,8 +3192,8 @@ def dispatch_event( ) -> NoneType: """Frame.dispatch_event - The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the elment, `click` - is dispatched. This is equivalend to calling + The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the element, + `click` is dispatched. This is equivalent to calling [element.click()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click). ```py @@ -4071,7 +4112,7 @@ def press( If `key` is a single character, it is case-sensitive, so the values `a` and `A` will generate different respective texts. - Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When speficied with the + Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed. Parameters @@ -4867,6 +4908,7 @@ def set_default_navigation_timeout(self, timeout: float) -> NoneType: - `page.reload()` - `page.set_content()` - `page.expect_navigation()` + - `page.wait_for_url()` > NOTE: `page.set_default_navigation_timeout()` takes priority over `page.set_default_timeout()`, `browser_context.set_default_timeout()` and `browser_context.set_default_navigation_timeout()`. @@ -5180,8 +5222,8 @@ def dispatch_event( ) -> NoneType: """Page.dispatch_event - The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the elment, `click` - is dispatched. This is equivalend to calling + The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the element, + `click` is dispatched. This is equivalent to calling [element.click()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click). ```py @@ -5866,6 +5908,49 @@ def wait_for_load_state( ) ) + def wait_for_url( + self, + url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]], + *, + wait_until: Literal["domcontentloaded", "load", "networkidle"] = None, + timeout: float = None + ) -> NoneType: + """Page.wait_for_url + + Waits for the main frame to navigate to the given URL. + + ```py + page.click(\"a.delayed-navigation\") # clicking the link will indirectly cause a navigation + page.wait_for_url(\"**/target.html\") + ``` + + Shortcut for main frame's `frame.wait_for_url()`. + + Parameters + ---------- + url : Union[Callable[[str], bool], Pattern, str] + A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + wait_until : Union["domcontentloaded", "load", "networkidle", NoneType] + When to consider operation succeeded, defaults to `load`. Events can be either: + - `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired. + - `'load'` - consider operation to be finished when the `load` event is fired. + - `'networkidle'` - consider operation to be finished when there are no network connections for at least `500` ms. + timeout : Union[float, NoneType] + Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be + changed by using the `browser_context.set_default_navigation_timeout()`, + `browser_context.set_default_timeout()`, `page.set_default_navigation_timeout()` or + `page.set_default_timeout()` methods. + """ + + return mapping.from_maybe_impl( + self._sync( + "page.wait_for_url", + self._impl_obj.wait_for_url( + url=self._wrap_handler(url), wait_until=wait_until, timeout=timeout + ), + ) + ) + def wait_for_event( self, event: str, predicate: typing.Callable = None, *, timeout: float = None ) -> typing.Any: @@ -6124,7 +6209,7 @@ def route( > NOTE: The handler will only be called for the first url if the response is a redirect. - An example of a naïve handler that aborts all image requests: + An example of a naive handler that aborts all image requests: ```py page = browser.new_page() @@ -6145,6 +6230,8 @@ def route( Page routes take precedence over browser context routes (set up with `browser_context.route()`) when request matches both handlers. + To remove a route with its handler you can use `page.unroute()`. + > NOTE: Enabling routing disables http cache. Parameters @@ -6208,9 +6295,6 @@ def screenshot( Returns the buffer with the captured screenshot. - > NOTE: Screenshots take at least 1/6 second on Chromium OS X and Chromium Windows. See https://crbug.com/741689 for - discussion. - Parameters ---------- timeout : Union[float, NoneType] @@ -6960,7 +7044,7 @@ def press( If `key` is a single character, it is case-sensitive, so the values `a` and `A` will generate different respective texts. - Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When speficied with the + Shortcuts such as `key: \"Control+o\"` or `key: \"Control+Shift+T\"` are supported as well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed. ```py @@ -8135,7 +8219,7 @@ def route( Routing provides the capability to modify network requests that are made by any page in the browser context. Once route is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. - An example of a naïve handler that aborts all image requests: + An example of a naive handler that aborts all image requests: ```py context = browser.new_context() @@ -8160,6 +8244,8 @@ def route( Page routes (set up with `page.route()`) take precedence over browser context routes when request matches both handlers. + To remove a route with its handler you can use `browser_context.unroute()`. + > NOTE: Enabling routing disables http cache. Parameters @@ -8499,6 +8585,7 @@ def new_context( self, *, viewport: ViewportSize = None, + screen: ViewportSize = None, no_viewport: bool = None, ignore_https_errors: bool = None, java_script_enabled: bool = None, @@ -8541,6 +8628,9 @@ def new_context( ---------- viewport : Union[{width: int, height: int}, NoneType] Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `no_viewport` disables the fixed viewport. + screen : Union[{width: int, height: int}, NoneType] + Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport` + is set. no_viewport : Union[bool, NoneType] Does not enforce fixed viewport, allows resizing window in the headed mode. ignore_https_errors : Union[bool, NoneType] @@ -8612,6 +8702,7 @@ def new_context( "browser.new_context", self._impl_obj.new_context( viewport=viewport, + screen=screen, noViewport=no_viewport, ignoreHTTPSErrors=ignore_https_errors, javaScriptEnabled=java_script_enabled, @@ -8644,6 +8735,7 @@ def new_page( self, *, viewport: ViewportSize = None, + screen: ViewportSize = None, no_viewport: bool = None, ignore_https_errors: bool = None, java_script_enabled: bool = None, @@ -8681,6 +8773,9 @@ def new_page( ---------- viewport : Union[{width: int, height: int}, NoneType] Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `no_viewport` disables the fixed viewport. + screen : Union[{width: int, height: int}, NoneType] + Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport` + is set. no_viewport : Union[bool, NoneType] Does not enforce fixed viewport, allows resizing window in the headed mode. ignore_https_errors : Union[bool, NoneType] @@ -8752,6 +8847,7 @@ def new_page( "browser.new_page", self._impl_obj.new_page( viewport=viewport, + screen=screen, noViewport=no_viewport, ignoreHTTPSErrors=ignore_https_errors, javaScriptEnabled=java_script_enabled, @@ -8893,7 +8989,8 @@ def launch( resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium, Firefox or WebKit, use at your own risk. channel : Union["chrome", "chrome-beta", "chrome-canary", "chrome-dev", "msedge", "msedge-beta", "msedge-canary", "msedge-dev", NoneType] - Browser distribution channel. + Browser distribution channel. Read more about using + [Google Chrome and Microsoft Edge](./browsers#google-chrome--microsoft-edge). args : Union[List[str], NoneType] Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). @@ -8989,6 +9086,7 @@ def launch_persistent_context( downloads_path: typing.Union[str, pathlib.Path] = None, slow_mo: float = None, viewport: ViewportSize = None, + screen: ViewportSize = None, no_viewport: bool = None, ignore_https_errors: bool = None, java_script_enabled: bool = None, @@ -9067,6 +9165,9 @@ def launch_persistent_context( Defaults to 0. viewport : Union[{width: int, height: int}, NoneType] Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `no_viewport` disables the fixed viewport. + screen : Union[{width: int, height: int}, NoneType] + Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport` + is set. no_viewport : Union[bool, NoneType] Does not enforce fixed viewport, allows resizing window in the headed mode. ignore_https_errors : Union[bool, NoneType] @@ -9147,6 +9248,7 @@ def launch_persistent_context( downloadsPath=downloads_path, slowMo=slow_mo, viewport=viewport, + screen=screen, noViewport=no_viewport, ignoreHTTPSErrors=ignore_https_errors, javaScriptEnabled=java_script_enabled, diff --git a/setup.py b/setup.py index b644a46be..a29749ac8 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ from auditwheel.wheeltools import InWheel from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.10.0-next-1616530863000" +driver_version = "1.11.0-next-1617207797000" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/async/test_navigation.py b/tests/async/test_navigation.py index e77bf77ff..d685e602f 100644 --- a/tests/async/test_navigation.py +++ b/tests/async/test_navigation.py @@ -440,9 +440,9 @@ async def test_wait_for_nav_should_work(page, server): async def test_wait_for_nav_should_respect_timeout(page, server): with pytest.raises(Error) as exc_info: - async with page.expect_navigation(url="**/frame.html", timeout=5000): + async with page.expect_navigation(url="**/frame.html", timeout=2500): await page.goto(server.EMPTY_PAGE) - assert "Timeout 5000ms exceeded" in exc_info.value.message + assert "Timeout 2500ms exceeded" in exc_info.value.message async def test_wait_for_nav_should_work_with_both_domcontentloaded_and_load( @@ -866,9 +866,9 @@ async def test_frame_goto_should_continue_after_client_redirect(page, server): url = server.PREFIX + "/frames/child-redirect.html" with pytest.raises(Error) as exc_info: - await page.goto(url, timeout=5000, wait_until="networkidle") + await page.goto(url, timeout=2500, wait_until="networkidle") - assert "Timeout 5000ms exceeded." in exc_info.value.message + assert "Timeout 2500ms exceeded." in exc_info.value.message assert ( f'navigating to "{url}", waiting until "networkidle"' in exc_info.value.message ) diff --git a/tests/async/test_video.py b/tests/async/test_video.py index bb6210da7..40e8ff0d7 100644 --- a/tests/async/test_video.py +++ b/tests/async/test_video.py @@ -14,6 +14,8 @@ import os +import pytest + async def test_should_expose_video_path(browser, tmpdir, server): page = await browser.new_page(record_video_dir=tmpdir) @@ -23,6 +25,7 @@ async def test_should_expose_video_path(browser, tmpdir, server): await page.context.close() +@pytest.mark.skip("Upstream is changing, will fix with next roll") async def test_short_video_should_exist(browser, tmpdir, server): page = await browser.new_page(record_video_dir=tmpdir) await page.goto(server.PREFIX + "/grid.html") @@ -32,6 +35,7 @@ async def test_short_video_should_exist(browser, tmpdir, server): assert os.path.exists(path) +@pytest.mark.skip("Upstream is changing, will fix with next roll") async def test_short_video_should_exist_persistent_context( browser_type, tmpdir, launch_arguments ): diff --git a/tests/async/test_wait_for_url.py b/tests/async/test_wait_for_url.py new file mode 100644 index 000000000..4c4882170 --- /dev/null +++ b/tests/async/test_wait_for_url.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +import pytest + +from playwright.async_api import Error, Page + + +async def test_wait_for_url_should_work(page: Page, server): + await page.goto(server.EMPTY_PAGE) + await page.evaluate( + "url => window.location.href = url", server.PREFIX + "/grid.html" + ) + await page.wait_for_url("**/grid.html") + assert "grid.html" in page.url + + +async def test_wait_for_url_should_respect_timeout(page: Page, server): + await page.goto(server.EMPTY_PAGE) + with pytest.raises(Error) as exc_info: + await page.wait_for_url("**/frame.html", timeout=2500) + assert "Timeout 2500ms exceeded" in exc_info.value.message + + +async def test_wait_for_url_should_work_with_both_domcontentloaded_and_load( + page: Page, server +): + await page.goto(server.EMPTY_PAGE) + await page.wait_for_url("**/*", wait_until="domcontentloaded") + await page.wait_for_url("**/*", wait_until="load") + + +async def test_wait_for_url_should_work_with_clicking_on_anchor_links( + page: Page, server +): + await page.goto(server.EMPTY_PAGE) + await page.set_content('foobar') + await page.click("a") + await page.wait_for_url("**/*#foobar") + assert page.url == server.EMPTY_PAGE + "#foobar" + + +async def test_wait_for_url_should_work_with_history_push_state(page: Page, server): + await page.goto(server.EMPTY_PAGE) + await page.set_content( + """ + SPA + + """ + ) + await page.click("a") + await page.wait_for_url("**/wow.html") + assert page.url == server.PREFIX + "/wow.html" + + +async def test_wait_for_url_should_work_with_history_replace_state(page: Page, server): + await page.goto(server.EMPTY_PAGE) + await page.set_content( + """ + SPA + + """ + ) + await page.click("a") + await page.wait_for_url("**/replaced.html") + assert page.url == server.PREFIX + "/replaced.html" + + +async def test_wait_for_url_should_work_with_dom_history_back_forward( + page: Page, server +): + await page.goto(server.EMPTY_PAGE) + await page.set_content( + """ + back + forward + + """ + ) + + assert page.url == server.PREFIX + "/second.html" + + await page.click("a#back"), + await page.wait_for_url("**/first.html") + assert page.url == server.PREFIX + "/first.html" + + await page.click("a#forward"), + await page.wait_for_url("**/second.html") + assert page.url == server.PREFIX + "/second.html" + + +async def test_wait_for_url_should_work_with_url_match_for_same_document_navigations( + page: Page, server +): + await page.goto(server.EMPTY_PAGE) + await page.evaluate("history.pushState({}, '', '/first.html')") + await page.evaluate("history.pushState({}, '', '/second.html')") + await page.evaluate("history.pushState({}, '', '/third.html')") + await page.wait_for_url(re.compile(r"third\.html")) + assert "/third.html" in page.url From 0c70108aa225a6df82cd886da7a250f754d99ffe Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 3 Apr 2021 08:47:43 +0800 Subject: [PATCH 004/140] feat(roll): roll to 1617212285000 aka video (#609) --- README.md | 2 +- playwright/_impl/_artifact.py | 48 +++++++++++++++++++++++++ playwright/_impl/_browser.py | 1 + playwright/_impl/_browser_context.py | 1 + playwright/_impl/_download.py | 31 +++++++++------- playwright/_impl/_element_handle.py | 8 ++++- playwright/_impl/_helper.py | 8 +++++ playwright/_impl/_object_factory.py | 9 +++-- playwright/_impl/_page.py | 34 +++++++++--------- playwright/_impl/_stream.py | 34 ++++++++++++++++++ playwright/_impl/_video.py | 53 ++++++++++++++++++---------- playwright/async_api/_generated.py | 30 ++++++++++++++-- playwright/sync_api/_generated.py | 30 ++++++++++++++-- setup.py | 2 +- tests/async/test_download.py | 2 +- tests/async/test_interception.py | 4 +++ tests/async/test_video.py | 14 ++++---- tests/sync/test_video.py | 4 +++ 18 files changed, 250 insertions(+), 65 deletions(-) create mode 100644 playwright/_impl/_artifact.py create mode 100644 playwright/_impl/_stream.py diff --git a/README.md b/README.md index 5d2815c45..e09a4318e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 90.0.4430.0 | ✅ | ✅ | ✅ | +| Chromium 91.0.4455.0 | ✅ | ✅ | ✅ | | WebKit 14.2 | ✅ | ✅ | ✅ | | Firefox 87.0b10 | ✅ | ✅ | ✅ | diff --git a/playwright/_impl/_artifact.py b/playwright/_impl/_artifact.py new file mode 100644 index 000000000..9c3afc3b5 --- /dev/null +++ b/playwright/_impl/_artifact.py @@ -0,0 +1,48 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pathlib +from pathlib import Path +from typing import Dict, Optional, Union, cast + +from playwright._impl._connection import ChannelOwner, from_channel +from playwright._impl._helper import Error, make_dirs_for_file, patch_error_message +from playwright._impl._stream import Stream + + +class Artifact(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._is_remote = False + self.absolute_path = initializer["absolutePath"] + + async def path_after_finished(self) -> Optional[pathlib.Path]: + if self._is_remote: + raise Error( + "Path is not available when using browser_type.connect(). Use save_as() to save a local copy." + ) + return pathlib.Path(await self._channel.send("pathAfterFinished")) + + async def save_as(self, path: Union[str, Path]) -> None: + stream = cast(Stream, from_channel(await self._channel.send("saveAsStream"))) + make_dirs_for_file(path) + await stream.save_as(path) + + async def failure(self) -> Optional[str]: + return patch_error_message(await self._channel.send("failure")) + + async def delete(self) -> None: + await self._channel.send("delete") diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index 4f6ebe639..d5d512063 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -47,6 +47,7 @@ def __init__( self._browser_type = parent self._is_connected = True self._is_closed_or_closing = False + self._is_remote = False self._contexts: List[BrowserContext] = [] self._channel.on("close", lambda _: self._on_close()) diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 3cade6e15..b2c232327 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -79,6 +79,7 @@ def __repr__(self) -> str: return f"" def _on_page(self, page: Page) -> None: + print("ON PAGE ARRIVED") page._set_browser_context(self) self._pages.append(page) self.emit(BrowserContext.Events.Page, page) diff --git a/playwright/_impl/_download.py b/playwright/_impl/_download.py index 0400f005a..11cdd3e7e 100644 --- a/playwright/_impl/_download.py +++ b/playwright/_impl/_download.py @@ -14,38 +14,43 @@ import pathlib from pathlib import Path -from typing import Dict, Optional, Union +from typing import TYPE_CHECKING, Optional, Union -from playwright._impl._connection import ChannelOwner -from playwright._impl._helper import patch_error_message +from playwright._impl._artifact import Artifact +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._page import Page -class Download(ChannelOwner): + +class Download: def __init__( - self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + self, page: "Page", url: str, suggested_filename: str, artifact: Artifact ) -> None: - super().__init__(parent, type, guid, initializer) + self._loop = page._loop + self._dispatcher_fiber = page._dispatcher_fiber + self._url = url + self._suggested_filename = suggested_filename + self._artifact = artifact def __repr__(self) -> str: return f"" @property def url(self) -> str: - return self._initializer["url"] + return self._url @property def suggested_filename(self) -> str: - return self._initializer["suggestedFilename"] + return self._suggested_filename async def delete(self) -> None: - await self._channel.send("delete") + await self._artifact.delete() async def failure(self) -> Optional[str]: - return patch_error_message(await self._channel.send("failure")) + return await self._artifact.failure() async def path(self) -> Optional[pathlib.Path]: - return pathlib.Path(await self._channel.send("path")) + return await self._artifact.path_after_finished() async def save_as(self, path: Union[str, Path]) -> None: - path = str(Path(path)) - return await self._channel.send("saveAs", dict(path=path)) + await self._artifact.save_as(path) diff --git a/playwright/_impl/_element_handle.py b/playwright/_impl/_element_handle.py index af0861f44..84fed008b 100644 --- a/playwright/_impl/_element_handle.py +++ b/playwright/_impl/_element_handle.py @@ -20,7 +20,12 @@ from playwright._impl._api_structures import FilePayload, FloatRect, Position from playwright._impl._connection import ChannelOwner, from_nullable_channel from playwright._impl._file_chooser import normalize_file_payloads -from playwright._impl._helper import KeyboardModifier, MouseButton, locals_to_params +from playwright._impl._helper import ( + KeyboardModifier, + MouseButton, + locals_to_params, + make_dirs_for_file, +) from playwright._impl._js_handle import ( JSHandle, Serializable, @@ -221,6 +226,7 @@ async def screenshot( encoded_binary = await self._channel.send("screenshot", params) decoded_binary = base64.b64decode(encoded_binary) if path: + make_dirs_for_file(path) with open(path, "wb") as fd: fd.write(decoded_binary) return decoded_binary diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 7f961df19..01be264ed 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -14,10 +14,12 @@ import fnmatch import math +import os import re import sys import time import traceback +from pathlib import Path from types import TracebackType from typing import ( TYPE_CHECKING, @@ -232,3 +234,9 @@ def not_installed_error(message: str) -> Exception: def to_snake_case(name: str) -> str: return to_snake_case_regex.sub(r"_\1", name).lower() + + +def make_dirs_for_file(path: Union[Path, str]) -> None: + if not os.path.isabs(path): + path = Path.cwd() / path + os.makedirs(os.path.dirname(path), exist_ok=True) diff --git a/playwright/_impl/_object_factory.py b/playwright/_impl/_object_factory.py index aa9c0d988..1679c5332 100644 --- a/playwright/_impl/_object_factory.py +++ b/playwright/_impl/_object_factory.py @@ -14,6 +14,7 @@ from typing import Any, Dict, cast +from playwright._impl._artifact import Artifact from playwright._impl._browser import Browser from playwright._impl._browser_context import BrowserContext from playwright._impl._browser_type import BrowserType @@ -22,7 +23,6 @@ from playwright._impl._connection import ChannelOwner from playwright._impl._console_message import ConsoleMessage from playwright._impl._dialog import Dialog -from playwright._impl._download import Download from playwright._impl._element_handle import ElementHandle from playwright._impl._frame import Frame from playwright._impl._js_handle import JSHandle @@ -30,6 +30,7 @@ from playwright._impl._page import BindingCall, Page, Worker from playwright._impl._playwright import Playwright from playwright._impl._selectors import Selectors +from playwright._impl._stream import Stream class DummyObject(ChannelOwner): @@ -42,6 +43,8 @@ def __init__( def create_remote_object( parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> Any: + if type == "Artifact": + return Artifact(parent, type, guid, initializer) if type == "BindingCall": return BindingCall(parent, type, guid, initializer) if type == "Browser": @@ -63,8 +66,6 @@ def create_remote_object( return ConsoleMessage(parent, type, guid, initializer) if type == "Dialog": return Dialog(parent, type, guid, initializer) - if type == "Download": - return Download(parent, type, guid, initializer) if type == "ElementHandle": return ElementHandle(parent, type, guid, initializer) if type == "Frame": @@ -81,6 +82,8 @@ def create_remote_object( return Response(parent, type, guid, initializer) if type == "Route": return Route(parent, type, guid, initializer) + if type == "Stream": + return Stream(parent, type, guid, initializer) if type == "WebSocket": return WebSocket(parent, type, guid, initializer) if type == "Worker": diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 1ba1573d6..5e1b56270 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -54,6 +54,7 @@ URLMatchResponse, is_safe_close_error, locals_to_params, + make_dirs_for_file, parse_error, serialize_error, ) @@ -142,12 +143,7 @@ def __init__( self._channel.on( "domcontentloaded", lambda _: self.emit(Page.Events.DOMContentLoaded) ) - self._channel.on( - "download", - lambda params: self.emit( - Page.Events.Download, from_channel(params["download"]) - ), - ) + self._channel.on("download", lambda params: self._on_download(params)) self._channel.on( "fileChooser", lambda params: self.emit( @@ -208,12 +204,7 @@ def __init__( from_channel(params["route"]), from_channel(params["request"]) ), ) - self._channel.on( - "video", - lambda params: cast(Video, self.video)._set_relative_path( - params["relativePath"] - ), - ) + self._channel.on("video", lambda params: self._on_video(params)) self._channel.on( "webSocket", lambda params: self.emit( @@ -294,6 +285,18 @@ def _on_dialog(self, params: Any) -> None: else: asyncio.create_task(dialog.dismiss()) + def _on_download(self, params: Any) -> None: + url = params["url"] + suggested_filename = params["suggestedFilename"] + artifact = from_channel(params["artifact"]) + self.emit( + Page.Events.Download, Download(self, url, suggested_filename, artifact) + ) + + def _on_video(self, params: Any) -> None: + artifact = from_channel(params["artifact"]) + cast(Video, self.video)._artifact_ready(artifact) + def _add_event_handler(self, event: str, k: Any, v: Any) -> None: if event == Page.Events.FileChooser and len(self.listeners(event)) == 0: self._channel.send_no_reply( @@ -575,6 +578,7 @@ async def screenshot( encoded_binary = await self._channel.send("screenshot", params) decoded_binary = base64.b64decode(encoded_binary) if path: + make_dirs_for_file(path) with open(path, "wb") as fd: fd.write(decoded_binary) return decoded_binary @@ -765,6 +769,7 @@ async def pdf( encoded_binary = await self._channel.send("pdf", params) decoded_binary = base64.b64decode(encoded_binary) if path: + make_dirs_for_file(path) with open(path, "wb") as fd: fd.write(decoded_binary) return decoded_binary @@ -773,13 +778,10 @@ async def pdf( def video( self, ) -> Optional[Video]: - context_options = self._browser_context._options - if "recordVideo" not in context_options: + if "recordVideo" not in self._browser_context._options: return None if not self._video: self._video = Video(self) - if "videoRelativePath" in self._initializer: - self._video._set_relative_path(self._initializer["videoRelativePath"]) return self._video def expect_event( diff --git a/playwright/_impl/_stream.py b/playwright/_impl/_stream.py new file mode 100644 index 000000000..a346bf7e5 --- /dev/null +++ b/playwright/_impl/_stream.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +from pathlib import Path +from typing import Dict + +from playwright._impl._connection import ChannelOwner + + +class Stream(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + + async def save_as(self, path: Path) -> None: + with open(path, mode="wb") as file: + while True: + binary = await self._channel.send("read") + if not binary: + break + file.write(base64.b64decode(binary)) diff --git a/playwright/_impl/_video.py b/playwright/_impl/_video.py index da3c72a3a..171852506 100644 --- a/playwright/_impl/_video.py +++ b/playwright/_impl/_video.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import pathlib -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Union + +from playwright._impl._artifact import Artifact +from playwright._impl._helper import Error if TYPE_CHECKING: # pragma: no cover from playwright._impl._page import Page @@ -25,22 +27,37 @@ def __init__(self, page: "Page") -> None: self._loop = page._loop self._dispatcher_fiber = page._dispatcher_fiber self._page = page - self._path_future = page._loop.create_future() - - async def path(self) -> pathlib.Path: - return await self._path_future - - def _set_relative_path(self, relative_path: str) -> None: - self._path_future.set_result( - pathlib.Path( - os.path.join( - cast( - str, self._page._browser_context._options["recordVideo"]["dir"] - ), - relative_path, - ) - ) - ) + self._artifact_future = page._loop.create_future() + if page.is_closed(): + self._page_closed() + else: + page.on("close", lambda: self._page_closed()) def __repr__(self) -> str: return f"