From 1f020469c8c0b1681c968843c234555ac1090769 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 7 Jul 2021 12:36:33 +0200 Subject: [PATCH 001/646] chore: sync with upstream GitHub issue templates (#793) --- .github/ISSUE_TEMPLATE/bug.md | 30 ---------- .github/ISSUE_TEMPLATE/bug.yml | 69 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 4 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 18 ++++++ .github/ISSUE_TEMPLATE/question.yml | 11 ++++ playwright/__main__.py | 15 ++--- 6 files changed, 106 insertions(+), 41 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index 30f051884..000000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Bug Report -about: Something doesn't work like it should? Tell us! -title: "[BUG]" -labels: '' -assignees: '' - ---- - -**Context:** - -- Playwright Version: [what Playwright version do you use?] -- Operating System: [e.g. Windows, Linux or Mac] -- Python Version: [e.g. 3.8, 3.9] - - - - -**Code Snippet** - -Help us help you! Put down a short code snippet that illustrates your bug and -that we can run and debug locally. For example: - -```python -... -``` - -**Describe the bug** - -Add any other details about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 000000000..c107fb603 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,69 @@ +name: Bug Report +description: Something doesn't work like it should? Tell us! +title: "[Bug]: " +labels: [] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: version + attributes: + label: Playwright version + description: Which version of of Playwright are you using? + placeholder: ex. 1.12.0 + validations: + required: true + - type: dropdown + id: operating-system + attributes: + label: Operating system + multiple: true + description: What operating system are you running Playwright on? + options: + - Windows + - MacOS + - Linux + - type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + multiple: true + options: + - Chromium + - Firefox + - WebKit + - type: textarea + id: other-information + attributes: + label: Other information + description: ex. Python version, Linux distribution etc. + - type: textarea + id: what-happened + attributes: + label: What happened? / Describe the bug + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + validations: + required: true + - type: textarea + id: reproducible + attributes: + label: Code snippet to reproduce your bug + description: Help us help you! Put down a short code snippet that illustrates your bug and that we can run and debug locally. This will be automatically formatted into code, so no need for backticks. + render: shell + placeholder: | + from playwright.sync_api import sync_playwright + + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + # ... + browser.close() + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output like [Playwright debug logs](https://playwright.dev/docs/debug#verbose-api-logs). This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..7b92d8d4c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: Join our Slack community + url: https://aka.ms/playwright-slack + about: Ask questions and discuss with other community members diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..8a755a424 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,18 @@ +name: Feature request +description: Request new features to be added +title: "[Feature]: " +labels: [] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! + - type: textarea + id: what-happened + attributes: + label: Feature request + description: | + Let us know what functionality you'd like to see in Playwright and what is your use case. + Do you think others might benefit from this as well? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 000000000..c805b9812 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,11 @@ +name: I have a question +description: Feel free to ask us your questions! +title: "[Question]: " +labels: [] +body: + - type: textarea + id: question + attributes: + label: Your question + validations: + required: true diff --git a/playwright/__main__.py b/playwright/__main__.py index 6046134eb..1c0a01d10 100644 --- a/playwright/__main__.py +++ b/playwright/__main__.py @@ -13,24 +13,17 @@ # limitations under the License. import os -import platform import subprocess import sys from playwright._impl._driver import compute_driver_executable -from playwright._repo_version import version def main() -> None: - if "envinfo" in sys.argv: - print(f"- Playwright Version: {version}") - print(f"- Operating System: {platform.platform()}") - print(f"- Python Version: {sys.version}") - else: - driver_executable = compute_driver_executable() - env = os.environ.copy() - env["PW_CLI_TARGET_LANG"] = "python" - subprocess.run([str(driver_executable), *sys.argv[1:]], env=env) + driver_executable = compute_driver_executable() + env = os.environ.copy() + env["PW_CLI_TARGET_LANG"] = "python" + subprocess.run([str(driver_executable), *sys.argv[1:]], env=env) if __name__ == "__main__": From 42e1283edb9fbb7dd3e49cce65d2d611740c43a9 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 12 Jul 2021 11:52:03 +0200 Subject: [PATCH 002/646] fix(cli): forward CLI exit code over to the parent (#801) --- playwright/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playwright/__main__.py b/playwright/__main__.py index 1c0a01d10..cbc9cd4ba 100644 --- a/playwright/__main__.py +++ b/playwright/__main__.py @@ -23,7 +23,8 @@ def main() -> None: driver_executable = compute_driver_executable() env = os.environ.copy() env["PW_CLI_TARGET_LANG"] = "python" - subprocess.run([str(driver_executable), *sys.argv[1:]], env=env) + completed_process = subprocess.run([str(driver_executable), *sys.argv[1:]], env=env) + sys.exit(completed_process.returncode) if __name__ == "__main__": From 85f317716c15a992b9122f1db574b64b077b4d17 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 12 Jul 2021 12:45:41 +0200 Subject: [PATCH 003/646] feat(roll): roll Playwright to 1.13.0-next-1625812834000 (#799) --- playwright/_impl/_artifact.py | 3 + playwright/_impl/_browser.py | 2 + playwright/_impl/_browser_context.py | 4 +- playwright/_impl/_browser_type.py | 1 + playwright/_impl/_download.py | 3 + playwright/_impl/_frame.py | 12 ++- playwright/_impl/_helper.py | 5 +- playwright/_impl/_page.py | 32 ++++++-- playwright/async_api/_generated.py | 101 ++++++++++++++++-------- playwright/sync_api/_generated.py | 109 ++++++++++++++++---------- setup.cfg | 2 +- setup.py | 2 +- tests/assets/error.html | 3 +- tests/async/test_download.py | 19 +++++ tests/async/test_page.py | 36 +++++++-- tests/async/test_page_base_url.py | 110 +++++++++++++++++++++++++++ tests/server.py | 2 +- 17 files changed, 349 insertions(+), 97 deletions(-) create mode 100644 tests/async/test_page_base_url.py diff --git a/playwright/_impl/_artifact.py b/playwright/_impl/_artifact.py index 9c3afc3b5..270783b74 100644 --- a/playwright/_impl/_artifact.py +++ b/playwright/_impl/_artifact.py @@ -46,3 +46,6 @@ async def failure(self) -> Optional[str]: async def delete(self) -> None: await self._channel.send("delete") + + async def cancel(self) -> None: + await self._channel.send("cancel") diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index 331b446d5..5fa0755ed 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -105,6 +105,7 @@ async def new_context( recordVideoDir: Union[Path, str] = None, recordVideoSize: ViewportSize = None, storageState: Union[StorageState, str, Path] = None, + baseURL: str = None, ) -> BrowserContext: params = locals_to_params(locals()) await normalize_context_params(self._connection._is_sync, params) @@ -145,6 +146,7 @@ async def new_page( recordVideoDir: Union[Path, str] = None, recordVideoSize: ViewportSize = None, storageState: Union[StorageState, str, Path] = None, + baseURL: str = None, ) -> Page: params = locals_to_params(locals()) context = await self.new_context(**params) diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index a68ccf232..293500350 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -240,7 +240,9 @@ async def expose_function(self, name: str, callback: Callable) -> None: await self.expose_binding(name, lambda source, *args: callback(*args)) async def route(self, url: URLMatch, handler: RouteHandler) -> None: - self._routes.append(RouteHandlerEntry(URLMatcher(url), handler)) + self._routes.append( + RouteHandlerEntry(URLMatcher(self._options.get("baseURL"), url), handler) + ) if len(self._routes) == 1: await self._channel.send( "setNetworkInterceptionEnabled", dict(enabled=True) diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index 07e0ad724..dff830f76 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -132,6 +132,7 @@ async def launch_persistent_context( recordHarOmitContent: bool = None, recordVideoDir: Union[Path, str] = None, recordVideoSize: ViewportSize = None, + baseURL: str = None, ) -> BrowserContext: userDataDir = str(Path(userDataDir)) params = locals_to_params(locals()) diff --git a/playwright/_impl/_download.py b/playwright/_impl/_download.py index ad8ad9024..1b93850ba 100644 --- a/playwright/_impl/_download.py +++ b/playwright/_impl/_download.py @@ -59,3 +59,6 @@ async def path(self) -> Optional[pathlib.Path]: async def save_as(self, path: Union[str, Path]) -> None: await self._artifact.save_as(path) + + async def cancel(self) -> None: + return await self._artifact.cancel() diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 34825b03c..1a45e2790 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -153,7 +153,11 @@ def expect_navigation( timeout = self._page._timeout_settings.navigation_timeout() deadline = monotonic_time() + timeout wait_helper = self._setup_navigation_wait_helper("expect_navigation", timeout) - matcher = URLMatcher(url) if url else None + matcher = ( + URLMatcher(self._page._browser_context._options.get("baseURL"), url) + if url + else None + ) def predicate(event: Any) -> bool: # Any failed navigation results in a rejection. @@ -188,7 +192,7 @@ async def wait_for_url( wait_until: DocumentLoadState = None, timeout: float = None, ) -> None: - matcher = URLMatcher(url) + matcher = URLMatcher(self._page._browser_context._options.get("baseURL"), url) if matcher.matches(self.url): await self.wait_for_load_state(state=wait_until, timeout=timeout) return @@ -274,10 +278,10 @@ async def is_editable(self, selector: str, timeout: float = None) -> bool: async def is_enabled(self, selector: str, timeout: float = None) -> bool: return await self._channel.send("isEnabled", locals_to_params(locals())) - async def is_hidden(self, selector: str, timeout: float = None) -> bool: + async def is_hidden(self, selector: str) -> bool: return await self._channel.send("isHidden", locals_to_params(locals())) - async def is_visible(self, selector: str, timeout: float = None) -> bool: + async def is_visible(self, selector: str) -> bool: return await self._channel.send("isVisible", locals_to_params(locals())) async def dispatch_event( diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 0485e0411..4b764af99 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -32,6 +32,7 @@ Union, cast, ) +from urllib.parse import urljoin from playwright._impl._api_types import Error, TimeoutError @@ -105,10 +106,12 @@ class FrameNavigatedEvent(TypedDict): class URLMatcher: - def __init__(self, match: URLMatch) -> None: + def __init__(self, base_url: Union[str, None], match: URLMatch) -> None: self._callback: Optional[Callable[[str], bool]] = None self._regex_obj: Optional[Pattern] = None if isinstance(match, str): + if base_url and not match.startswith("*"): + match = urljoin(base_url, match) regex = fnmatch.translate(match) self._regex_obj = re.compile(regex) elif isinstance(match, Pattern): diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 10c8c4237..766b4b0fa 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -287,7 +287,11 @@ def main_frame(self) -> Frame: return self._main_frame def frame(self, name: str = None, url: URLMatch = None) -> Optional[Frame]: - matcher = URLMatcher(url) if url else None + matcher = ( + URLMatcher(self._browser_context._options.get("baseURL"), url) + if url + else None + ) for frame in self._frames: if name and frame.name == name: return frame @@ -335,10 +339,10 @@ async def is_editable(self, selector: str, timeout: float = None) -> bool: async def is_enabled(self, selector: str, timeout: float = None) -> bool: return await self._main_frame.is_enabled(**locals_to_params(locals())) - async def is_hidden(self, selector: str, timeout: float = None) -> bool: + async def is_hidden(self, selector: str) -> bool: return await self._main_frame.is_hidden(**locals_to_params(locals())) - async def is_visible(self, selector: str, timeout: float = None) -> bool: + async def is_visible(self, selector: str) -> bool: return await self._main_frame.is_visible(**locals_to_params(locals())) async def dispatch_event( @@ -506,7 +510,11 @@ async def add_init_script( await self._channel.send("addInitScript", dict(source=script)) async def route(self, url: URLMatch, handler: RouteHandler) -> None: - self._routes.append(RouteHandlerEntry(URLMatcher(url), handler)) + self._routes.append( + RouteHandlerEntry( + URLMatcher(self._browser_context._options.get("baseURL"), url), handler + ) + ) if len(self._routes) == 1: await self._channel.send( "setNetworkInterceptionEnabled", dict(enabled=True) @@ -822,7 +830,13 @@ def expect_request( url_or_predicate: URLMatchRequest, timeout: float = None, ) -> EventContextManagerImpl[Request]: - matcher = None if callable(url_or_predicate) else URLMatcher(url_or_predicate) + matcher = ( + None + if callable(url_or_predicate) + else URLMatcher( + self._browser_context._options.get("baseURL"), url_or_predicate + ) + ) predicate = url_or_predicate if callable(url_or_predicate) else None def my_predicate(request: Request) -> bool: @@ -850,7 +864,13 @@ def expect_response( url_or_predicate: URLMatchResponse, timeout: float = None, ) -> EventContextManagerImpl[Response]: - matcher = None if callable(url_or_predicate) else URLMatcher(url_or_predicate) + matcher = ( + None + if callable(url_or_predicate) + else URLMatcher( + self._browser_context._options.get("baseURL"), url_or_predicate + ) + ) predicate = url_or_predicate if callable(url_or_predicate) else None def my_predicate(response: Response) -> bool: diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index b3b9729d2..225a1a56f 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -3283,7 +3283,7 @@ async def is_enabled(self, selector: str, *, timeout: float = None) -> bool: ) ) - async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: + async def is_hidden(self, selector: str) -> bool: """Frame.is_hidden Returns whether the element is hidden, the opposite of [visible](./actionability.md#visible). `selector` that does not @@ -3294,9 +3294,6 @@ async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. - timeout : Union[float, NoneType] - Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -3305,12 +3302,11 @@ async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: return mapping.from_maybe_impl( await self._async( - "frame.is_hidden", - self._impl_obj.is_hidden(selector=selector, timeout=timeout), + "frame.is_hidden", self._impl_obj.is_hidden(selector=selector) ) ) - async def is_visible(self, selector: str, *, timeout: float = None) -> bool: + async def is_visible(self, selector: str) -> bool: """Frame.is_visible Returns whether the element is [visible](./actionability.md#visible). `selector` that does not match any elements is @@ -3321,9 +3317,6 @@ async def is_visible(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. - timeout : Union[float, NoneType] - Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -3332,8 +3325,7 @@ async def is_visible(self, selector: str, *, timeout: float = None) -> bool: return mapping.from_maybe_impl( await self._async( - "frame.is_visible", - self._impl_obj.is_visible(selector=selector, timeout=timeout), + "frame.is_visible", self._impl_obj.is_visible(selector=selector) ) ) @@ -4931,6 +4923,9 @@ async def path(self) -> typing.Optional[pathlib.Path]: Returns path to the downloaded file in case of successful download. The method will wait for the download to finish if necessary. The method throws when connected remotely. + Note that the download's file name is a random GUID, use `download.suggested_filename()` to get suggested file + name. + Returns ------- Union[pathlib.Path, NoneType] @@ -4956,6 +4951,17 @@ async def save_as(self, path: typing.Union[str, pathlib.Path]) -> NoneType: await self._async("download.save_as", self._impl_obj.save_as(path=path)) ) + async def cancel(self) -> NoneType: + """Download.cancel + + Cancels a download. Will not fail if the download is already finished or canceled. Upon successful cancellations, + `download.failure()` would resolve to `'canceled'`. + """ + + return mapping.from_maybe_impl( + await self._async("download.cancel", self._impl_obj.cancel()) + ) + mapping.register(DownloadImpl, Download) @@ -5449,7 +5455,7 @@ async def is_enabled(self, selector: str, *, timeout: float = None) -> bool: ) ) - async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: + async def is_hidden(self, selector: str) -> bool: """Page.is_hidden Returns whether the element is hidden, the opposite of [visible](./actionability.md#visible). `selector` that does not @@ -5460,9 +5466,6 @@ async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. - timeout : Union[float, NoneType] - Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -5471,12 +5474,11 @@ async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: return mapping.from_maybe_impl( await self._async( - "page.is_hidden", - self._impl_obj.is_hidden(selector=selector, timeout=timeout), + "page.is_hidden", self._impl_obj.is_hidden(selector=selector) ) ) - async def is_visible(self, selector: str, *, timeout: float = None) -> bool: + async def is_visible(self, selector: str) -> bool: """Page.is_visible Returns whether the element is [visible](./actionability.md#visible). `selector` that does not match any elements is @@ -5487,9 +5489,6 @@ async def is_visible(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. - timeout : Union[float, NoneType] - Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -5498,8 +5497,7 @@ async def is_visible(self, selector: str, *, timeout: float = None) -> bool: return mapping.from_maybe_impl( await self._async( - "page.is_visible", - self._impl_obj.is_visible(selector=selector, timeout=timeout), + "page.is_visible", self._impl_obj.is_visible(selector=selector) ) ) @@ -6091,7 +6089,9 @@ async def goto( Parameters ---------- url : str - URL to navigate page to. The url should include scheme, e.g. `https://`. + URL to navigate page to. The url should include scheme, e.g. `https://`. When a `baseURL` via the context options was + provided and the passed URL is a path, it gets merged via the + [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. 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()`, @@ -6558,7 +6558,9 @@ def handle_route(route): Parameters ---------- url : Union[Callable[[str], bool], Pattern, str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. + A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context + options was provided and the passed URL is a path, it gets merged via the + [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. """ @@ -8070,7 +8072,7 @@ def expect_request( ) -> AsyncEventContextManager["Request"]: """Page.expect_request - Waits for the matching request and returns it. See [waiting for event](./events.md#waiting-for-event) for more details + Waits for the matching request and returns it. See [waiting for event](./events.md#waiting-for-event) for more details about events. ```py @@ -8087,7 +8089,9 @@ def expect_request( Parameters ---------- url_or_predicate : Union[Callable[[Request], bool], Pattern, str] - Request URL string, regex or predicate receiving `Request` object. + Request URL string, regex or predicate receiving `Request` object. When a `baseURL` via the context options was provided + and the passed URL is a path, it gets merged via the + [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. timeout : Union[float, NoneType] Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be changed by using the `page.set_default_timeout()` method. @@ -8162,7 +8166,9 @@ def expect_response( Parameters ---------- url_or_predicate : Union[Callable[[Response], bool], Pattern, str] - Request URL string, regex or predicate receiving `Response` object. + Request URL string, regex or predicate receiving `Response` object. When a `baseURL` via the context options was + provided and the passed URL is a path, it gets merged via the + [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. timeout : Union[float, NoneType] Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. @@ -8782,7 +8788,9 @@ def handle_route(route): Parameters ---------- url : Union[Callable[[str], bool], Pattern, str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. + A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context + options was provided and the passed URL is a path, it gets merged via the + [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. """ @@ -9111,7 +9119,8 @@ async def new_context( record_har_omit_content: bool = None, record_video_dir: typing.Union[str, pathlib.Path] = None, record_video_size: ViewportSize = None, - storage_state: typing.Union[StorageState, str, pathlib.Path] = None + storage_state: typing.Union[StorageState, str, pathlib.Path] = None, + base_url: str = None ) -> "BrowserContext": """Browser.new_context @@ -9198,6 +9207,13 @@ async def new_context( Populates context with given storage state. This option can be used to initialize context with logged-in information obtained via `browser_context.storage_state()`. Either a path to the file with saved storage, or an object with the following fields: + base_url : Union[str, NoneType] + When using `page.goto()`, `page.route()`, `page.wait_for_url()`, `page.expect_request()`, + or `page.expect_response()` it takes the base URL in consideration by using the + [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. + Examples: + - baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` + - baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` Returns ------- @@ -9235,6 +9251,7 @@ async def new_context( recordVideoDir=record_video_dir, recordVideoSize=record_video_size, storageState=storage_state, + baseURL=base_url, ), ) ) @@ -9268,7 +9285,8 @@ async def new_page( record_har_omit_content: bool = None, record_video_dir: typing.Union[str, pathlib.Path] = None, record_video_size: ViewportSize = None, - storage_state: typing.Union[StorageState, str, pathlib.Path] = None + storage_state: typing.Union[StorageState, str, pathlib.Path] = None, + base_url: str = None ) -> "Page": """Browser.new_page @@ -9350,6 +9368,13 @@ async def new_page( Populates context with given storage state. This option can be used to initialize context with logged-in information obtained via `browser_context.storage_state()`. Either a path to the file with saved storage, or an object with the following fields: + base_url : Union[str, NoneType] + When using `page.goto()`, `page.route()`, `page.wait_for_url()`, `page.expect_request()`, + or `page.expect_response()` it takes the base URL in consideration by using the + [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. + Examples: + - baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` + - baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` Returns ------- @@ -9387,6 +9412,7 @@ async def new_page( recordVideoDir=record_video_dir, recordVideoSize=record_video_size, storageState=storage_state, + baseURL=base_url, ), ) ) @@ -9692,7 +9718,8 @@ async def launch_persistent_context( record_har_path: typing.Union[str, pathlib.Path] = None, record_har_omit_content: bool = None, record_video_dir: typing.Union[str, pathlib.Path] = None, - record_video_size: ViewportSize = None + record_video_size: ViewportSize = None, + base_url: str = None ) -> "BrowserContext": """BrowserType.launch_persistent_context @@ -9812,6 +9839,13 @@ async def launch_persistent_context( 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 be scaled down if necessary to fit the specified size. + base_url : Union[str, NoneType] + When using `page.goto()`, `page.route()`, `page.wait_for_url()`, `page.expect_request()`, + or `page.expect_response()` it takes the base URL in consideration by using the + [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. + Examples: + - baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` + - baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` Returns ------- @@ -9863,6 +9897,7 @@ async def launch_persistent_context( recordHarOmitContent=record_har_omit_content, recordVideoDir=record_video_dir, recordVideoSize=record_video_size, + baseURL=base_url, ), ) ) diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 3d36bebc6..3a444150e 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -3261,7 +3261,7 @@ def is_enabled(self, selector: str, *, timeout: float = None) -> bool: ) ) - def is_hidden(self, selector: str, *, timeout: float = None) -> bool: + def is_hidden(self, selector: str) -> bool: """Frame.is_hidden Returns whether the element is hidden, the opposite of [visible](./actionability.md#visible). `selector` that does not @@ -3272,9 +3272,6 @@ def is_hidden(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. - timeout : Union[float, NoneType] - Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -3282,13 +3279,10 @@ def is_hidden(self, selector: str, *, timeout: float = None) -> bool: """ return mapping.from_maybe_impl( - self._sync( - "frame.is_hidden", - self._impl_obj.is_hidden(selector=selector, timeout=timeout), - ) + self._sync("frame.is_hidden", self._impl_obj.is_hidden(selector=selector)) ) - def is_visible(self, selector: str, *, timeout: float = None) -> bool: + def is_visible(self, selector: str) -> bool: """Frame.is_visible Returns whether the element is [visible](./actionability.md#visible). `selector` that does not match any elements is @@ -3299,9 +3293,6 @@ def is_visible(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. - timeout : Union[float, NoneType] - Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -3309,10 +3300,7 @@ def is_visible(self, selector: str, *, timeout: float = None) -> bool: """ return mapping.from_maybe_impl( - self._sync( - "frame.is_visible", - self._impl_obj.is_visible(selector=selector, timeout=timeout), - ) + self._sync("frame.is_visible", self._impl_obj.is_visible(selector=selector)) ) def dispatch_event( @@ -4902,6 +4890,9 @@ def path(self) -> typing.Optional[pathlib.Path]: Returns path to the downloaded file in case of successful download. The method will wait for the download to finish if necessary. The method throws when connected remotely. + Note that the download's file name is a random GUID, use `download.suggested_filename()` to get suggested file + name. + Returns ------- Union[pathlib.Path, NoneType] @@ -4927,6 +4918,17 @@ def save_as(self, path: typing.Union[str, pathlib.Path]) -> NoneType: self._sync("download.save_as", self._impl_obj.save_as(path=path)) ) + def cancel(self) -> NoneType: + """Download.cancel + + Cancels a download. Will not fail if the download is already finished or canceled. Upon successful cancellations, + `download.failure()` would resolve to `'canceled'`. + """ + + return mapping.from_maybe_impl( + self._sync("download.cancel", self._impl_obj.cancel()) + ) + mapping.register(DownloadImpl, Download) @@ -5415,7 +5417,7 @@ def is_enabled(self, selector: str, *, timeout: float = None) -> bool: ) ) - def is_hidden(self, selector: str, *, timeout: float = None) -> bool: + def is_hidden(self, selector: str) -> bool: """Page.is_hidden Returns whether the element is hidden, the opposite of [visible](./actionability.md#visible). `selector` that does not @@ -5426,9 +5428,6 @@ def is_hidden(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. - timeout : Union[float, NoneType] - Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -5436,13 +5435,10 @@ def is_hidden(self, selector: str, *, timeout: float = None) -> bool: """ return mapping.from_maybe_impl( - self._sync( - "page.is_hidden", - self._impl_obj.is_hidden(selector=selector, timeout=timeout), - ) + self._sync("page.is_hidden", self._impl_obj.is_hidden(selector=selector)) ) - def is_visible(self, selector: str, *, timeout: float = None) -> bool: + def is_visible(self, selector: str) -> bool: """Page.is_visible Returns whether the element is [visible](./actionability.md#visible). `selector` that does not match any elements is @@ -5453,9 +5449,6 @@ def is_visible(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. - timeout : Union[float, NoneType] - Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -5463,10 +5456,7 @@ def is_visible(self, selector: str, *, timeout: float = None) -> bool: """ return mapping.from_maybe_impl( - self._sync( - "page.is_visible", - self._impl_obj.is_visible(selector=selector, timeout=timeout), - ) + self._sync("page.is_visible", self._impl_obj.is_visible(selector=selector)) ) def dispatch_event( @@ -6049,7 +6039,9 @@ def goto( Parameters ---------- url : str - URL to navigate page to. The url should include scheme, e.g. `https://`. + URL to navigate page to. The url should include scheme, e.g. `https://`. When a `baseURL` via the context options was + provided and the passed URL is a path, it gets merged via the + [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. 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()`, @@ -6515,7 +6507,9 @@ def handle_route(route): Parameters ---------- url : Union[Callable[[str], bool], Pattern, str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. + A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context + options was provided and the passed URL is a path, it gets merged via the + [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. """ @@ -8020,7 +8014,7 @@ def expect_request( ) -> EventContextManager["Request"]: """Page.expect_request - Waits for the matching request and returns it. See [waiting for event](./events.md#waiting-for-event) for more details + Waits for the matching request and returns it. See [waiting for event](./events.md#waiting-for-event) for more details about events. ```py @@ -8037,7 +8031,9 @@ def expect_request( Parameters ---------- url_or_predicate : Union[Callable[[Request], bool], Pattern, str] - Request URL string, regex or predicate receiving `Request` object. + Request URL string, regex or predicate receiving `Request` object. When a `baseURL` via the context options was provided + and the passed URL is a path, it gets merged via the + [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. timeout : Union[float, NoneType] Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be changed by using the `page.set_default_timeout()` method. @@ -8112,7 +8108,9 @@ def expect_response( Parameters ---------- url_or_predicate : Union[Callable[[Response], bool], Pattern, str] - Request URL string, regex or predicate receiving `Response` object. + Request URL string, regex or predicate receiving `Response` object. When a `baseURL` via the context options was + provided and the passed URL is a path, it gets merged via the + [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. timeout : Union[float, NoneType] Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. @@ -8723,7 +8721,9 @@ def handle_route(route): Parameters ---------- url : Union[Callable[[str], bool], Pattern, str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. + A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context + options was provided and the passed URL is a path, it gets merged via the + [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. """ @@ -9052,7 +9052,8 @@ def new_context( record_har_omit_content: bool = None, record_video_dir: typing.Union[str, pathlib.Path] = None, record_video_size: ViewportSize = None, - storage_state: typing.Union[StorageState, str, pathlib.Path] = None + storage_state: typing.Union[StorageState, str, pathlib.Path] = None, + base_url: str = None ) -> "BrowserContext": """Browser.new_context @@ -9139,6 +9140,13 @@ def new_context( Populates context with given storage state. This option can be used to initialize context with logged-in information obtained via `browser_context.storage_state()`. Either a path to the file with saved storage, or an object with the following fields: + base_url : Union[str, NoneType] + When using `page.goto()`, `page.route()`, `page.wait_for_url()`, `page.expect_request()`, + or `page.expect_response()` it takes the base URL in consideration by using the + [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. + Examples: + - baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` + - baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` Returns ------- @@ -9176,6 +9184,7 @@ def new_context( recordVideoDir=record_video_dir, recordVideoSize=record_video_size, storageState=storage_state, + baseURL=base_url, ), ) ) @@ -9209,7 +9218,8 @@ def new_page( record_har_omit_content: bool = None, record_video_dir: typing.Union[str, pathlib.Path] = None, record_video_size: ViewportSize = None, - storage_state: typing.Union[StorageState, str, pathlib.Path] = None + storage_state: typing.Union[StorageState, str, pathlib.Path] = None, + base_url: str = None ) -> "Page": """Browser.new_page @@ -9291,6 +9301,13 @@ def new_page( Populates context with given storage state. This option can be used to initialize context with logged-in information obtained via `browser_context.storage_state()`. Either a path to the file with saved storage, or an object with the following fields: + base_url : Union[str, NoneType] + When using `page.goto()`, `page.route()`, `page.wait_for_url()`, `page.expect_request()`, + or `page.expect_response()` it takes the base URL in consideration by using the + [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. + Examples: + - baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` + - baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` Returns ------- @@ -9328,6 +9345,7 @@ def new_page( recordVideoDir=record_video_dir, recordVideoSize=record_video_size, storageState=storage_state, + baseURL=base_url, ), ) ) @@ -9633,7 +9651,8 @@ def launch_persistent_context( record_har_path: typing.Union[str, pathlib.Path] = None, record_har_omit_content: bool = None, record_video_dir: typing.Union[str, pathlib.Path] = None, - record_video_size: ViewportSize = None + record_video_size: ViewportSize = None, + base_url: str = None ) -> "BrowserContext": """BrowserType.launch_persistent_context @@ -9753,6 +9772,13 @@ def launch_persistent_context( 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 be scaled down if necessary to fit the specified size. + base_url : Union[str, NoneType] + When using `page.goto()`, `page.route()`, `page.wait_for_url()`, `page.expect_request()`, + or `page.expect_response()` it takes the base URL in consideration by using the + [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. + Examples: + - baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` + - baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` Returns ------- @@ -9804,6 +9830,7 @@ def launch_persistent_context( recordHarOmitContent=record_har_omit_content, recordVideoDir=record_video_dir, recordVideoSize=record_video_size, + baseURL=base_url, ), ) ) diff --git a/setup.cfg b/setup.cfg index 61d090f79..47e20a6fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -addopts = -Wall -rsx -vv +addopts = -Wall -rsx -vv -s markers = skip_browser only_browser diff --git a/setup.py b/setup.py index 9f8266b99..2cc574bfa 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ InWheel = None from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.13.0-next-1625158334000" +driver_version = "1.13.0-next-1625812834000" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/assets/error.html b/tests/assets/error.html index 26978c466..9532d14cf 100644 --- a/tests/assets/error.html +++ b/tests/assets/error.html @@ -11,8 +11,7 @@ } function c() { - window.e = new Error('Fancy error!'); - throw window.e; + throw new Error('Fancy error!'); } //# sourceURL=myscript.js diff --git a/tests/async/test_download.py b/tests/async/test_download.py index d157aa254..6278d1e30 100644 --- a/tests/async/test_download.py +++ b/tests/async/test_download.py @@ -349,3 +349,22 @@ async def test_should_delete_downloads_on_browser_gone(browser_factory, server): assert os.path.exists(path1) is False assert os.path.exists(path2) is False assert os.path.exists(os.path.join(path1, "..")) is False + + +async def test_download_cancel_should_work(browser, server): + def handle_download(request): + request.setHeader("Content-Type", "application/octet-stream") + request.setHeader("Content-Disposition", "attachment") + # Chromium requires a large enough payload to trigger the download event soon enough + request.write(b"a" * 4096) + request.write(b"foo") + + server.set_route("/downloadWithDelay", handle_download) + page = await browser.new_page(accept_downloads=True) + await page.set_content(f'download') + async with page.expect_download() as download_info: + await page.click("a") + download = await download_info.value + await download.cancel() + assert await download.failure() == "canceled" + await page.close() diff --git a/tests/async/test_page.py b/tests/async/test_page.py index e9256f69f..bd58dab3c 100644 --- a/tests/async/test_page.py +++ b/tests/async/test_page.py @@ -392,17 +392,41 @@ def logme(t): assert result == 17 -async def test_page_error_should_fire(page, server, is_webkit): +async def test_page_error_should_fire(page, server, browser_name): + url = server.PREFIX + "/error.html" async with page.expect_event("pageerror") as error_info: - await page.goto(server.PREFIX + "/error.html") + await page.goto(url) error = await error_info.value assert error.name == "Error" assert error.message == "Fancy error!" - stack = await page.evaluate("window.e.stack") # Note that WebKit reports the stack of the 'throw' statement instead of the Error constructor call. - if is_webkit: - stack = stack.replace("14:25", "15:19") - assert error.stack == stack + if browser_name == "chromium": + assert ( + error.stack + == """Error: Fancy error! + at c (myscript.js:14:11) + at b (myscript.js:10:5) + at a (myscript.js:6:5) + at myscript.js:3:1""" + ) + if browser_name == "firefox": + assert ( + error.stack + == """Error: Fancy error! + at c (myscript.js:14:11) + at b (myscript.js:10:5) + at a (myscript.js:6:5) + at (myscript.js:3:1)""" + ) + if browser_name == "webkit": + assert ( + error.stack + == f"""Error: Fancy error! + at c ({url}:14:36) + at b ({url}:10:6) + at a ({url}:6:6) + at global code ({url}:3:2)""" + ) async def test_page_error_should_handle_odd_values(page): diff --git a/tests/async/test_page_base_url.py b/tests/async/test_page_base_url.py new file mode 100644 index 000000000..11d0349b2 --- /dev/null +++ b/tests/async/test_page_base_url.py @@ -0,0 +1,110 @@ +# 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. + +from playwright.async_api import Browser, BrowserType +from tests.server import Server + + +async def test_should_construct_a_new_url_when_a_base_url_in_browser_new_context_is_passed( + browser: Browser, server: Server +): + context = await browser.new_context(base_url=server.PREFIX) + page = await context.new_page() + assert (await page.goto("/empty.html")).url == server.EMPTY_PAGE + await context.close() + + +async def test_should_construct_a_new_url_when_a_base_url_in_browser_new_page_is_passed( + browser: Browser, server: Server +): + page = await browser.new_page(base_url=server.PREFIX) + assert (await page.goto("/empty.html")).url == server.EMPTY_PAGE + await page.close() + + +async def test_should_construct_a_new_url_when_a_base_url_in_browser_new_persistent_context_is_passed( + browser_type: BrowserType, tmpdir, server: Server, launch_arguments +): + context = await browser_type.launch_persistent_context( + tmpdir, **launch_arguments, base_url=server.PREFIX + ) + page = await context.new_page() + assert (await page.goto("/empty.html")).url == server.EMPTY_PAGE + await context.close() + + +async def test_should_construct_correctly_when_a_baseurl_without_a_trailing_slash_is_passed( + browser: Browser, server: Server +): + page = await browser.new_page(base_url=server.PREFIX + "/url-construction") + assert (await page.goto("mypage.html")).url == server.PREFIX + "/mypage.html" + assert (await page.goto("./mypage.html")).url == server.PREFIX + "/mypage.html" + assert (await page.goto("/mypage.html")).url == server.PREFIX + "/mypage.html" + await page.close() + + +async def test_should_construct_correctly_when_a_baseurl_with_a_trailing_slash_is_passed( + browser: Browser, server: Server +): + page = await browser.new_page(base_url=server.PREFIX + "/url-construction/") + assert ( + await page.goto("mypage.html") + ).url == server.PREFIX + "/url-construction/mypage.html" + assert ( + await page.goto("./mypage.html") + ).url == server.PREFIX + "/url-construction/mypage.html" + assert (await page.goto("/mypage.html")).url == server.PREFIX + "/mypage.html" + assert (await page.goto(".")).url == server.PREFIX + "/url-construction/" + assert (await page.goto("/")).url == server.PREFIX + "/" + await page.close() + + +async def test_should_not_construct_a_new_url_when_valid_urls_are_passed( + browser: Browser, server: Server +): + page = await browser.new_page(base_url="http://microsoft.com") + assert (await page.goto(server.EMPTY_PAGE)).url == server.EMPTY_PAGE + + await page.goto("data:text/html,Hello world") + assert page.url == "data:text/html,Hello world" + + await page.goto("about:blank") + assert page.url == "about:blank" + + await page.close() + + +async def test_should_be_able_to_match_a_url_relative_to_its_given_url_with_urlmatcher( + browser: Browser, server: Server +): + page = await browser.new_page(base_url=server.PREFIX + "/foobar/") + + await page.goto("/kek/index.html") + await page.wait_for_url("/kek/index.html") + assert page.url == server.PREFIX + "/kek/index.html" + + await page.route( + "./kek/index.html", lambda route: route.fulfill(body="base-url-matched-route") + ) + + async with page.expect_request("./kek/index.html") as request_info: + async with page.expect_response("./kek/index.html") as response_info: + await page.goto("./kek/index.html") + request = await request_info.value + response = await response_info.value + assert request.url == server.PREFIX + "/foobar/kek/index.html" + assert response.url == server.PREFIX + "/foobar/kek/index.html" + assert await response.body() == b"base-url-matched-route" + + await page.close() diff --git a/tests/server.py b/tests/server.py index 580113d39..dbc517628 100644 --- a/tests/server.py +++ b/tests/server.py @@ -119,7 +119,7 @@ def process(self): else: request.write(file_content) self.setResponseCode(HTTPStatus.OK) - except (FileNotFoundError, IsADirectoryError): + except (FileNotFoundError, IsADirectoryError, PermissionError): request.setResponseCode(HTTPStatus.NOT_FOUND) self.finish() From 7640f9019f8379b67746859b146af3bcd55b687a Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 14 Jul 2021 17:06:15 +0200 Subject: [PATCH 004/646] fix: reverse route handlers order (#804) --- playwright/_impl/_browser_context.py | 4 ++-- playwright/_impl/_page.py | 5 +++-- tests/async/test_browsercontext.py | 19 +++++++++--------- tests/async/test_interception.py | 29 ++++++++++++++-------------- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 293500350..6f27cfed0 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -240,8 +240,8 @@ async def expose_function(self, name: str, callback: Callable) -> None: await self.expose_binding(name, lambda source, *args: callback(*args)) async def route(self, url: URLMatch, handler: RouteHandler) -> None: - self._routes.append( - RouteHandlerEntry(URLMatcher(self._options.get("baseURL"), url), handler) + self._routes.insert( + 0, RouteHandlerEntry(URLMatcher(self._options.get("baseURL"), url), handler) ) if len(self._routes) == 1: await self._channel.send( diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 766b4b0fa..1e4614710 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -510,10 +510,11 @@ async def add_init_script( await self._channel.send("addInitScript", dict(source=script)) async def route(self, url: URLMatch, handler: RouteHandler) -> None: - self._routes.append( + self._routes.insert( + 0, RouteHandlerEntry( URLMatcher(self._browser_context._options.get("baseURL"), url), handler - ) + ), ) if len(self._routes) == 1: await self._channel.send( diff --git a/tests/async/test_browsercontext.py b/tests/async/test_browsercontext.py index 426d1d664..0d7ae21eb 100644 --- a/tests/async/test_browsercontext.py +++ b/tests/async/test_browsercontext.py @@ -464,30 +464,31 @@ def handler(route, request, ordinal): intercepted.append(ordinal) asyncio.create_task(route.continue_()) - def handler1(route, request): - handler(route, request, 1) - - await context.route("**/empty.html", handler1) + await context.route("**/*", lambda route, request: handler(route, request, 1)) await context.route( "**/empty.html", lambda route, request: handler(route, request, 2) ) await context.route( "**/empty.html", lambda route, request: handler(route, request, 3) ) - await context.route("**/*", lambda route, request: handler(route, request, 4)) + + def handler4(route, request): + handler(route, request, 4) + + await context.route("**/empty.html", handler4) await page.goto(server.EMPTY_PAGE) - assert intercepted == [1] + assert intercepted == [4] intercepted = [] - await context.unroute("**/empty.html", handler1) + await context.unroute("**/empty.html", handler4) await page.goto(server.EMPTY_PAGE) - assert intercepted == [2] + assert intercepted == [3] intercepted = [] await context.unroute("**/empty.html") await page.goto(server.EMPTY_PAGE) - assert intercepted == [4] + assert intercepted == [1] async def test_route_should_yield_to_page_route(context, server): diff --git a/tests/async/test_interception.py b/tests/async/test_interception.py index 9304bc544..722811441 100644 --- a/tests/async/test_interception.py +++ b/tests/async/test_interception.py @@ -46,15 +46,10 @@ async def handle_request(route, request): async def test_page_route_should_unroute(page: Page, server): intercepted = [] - def handler1(route): - intercepted.append(1) - asyncio.create_task(route.continue_()) - - await page.route("**/empty.html", handler1) await page.route( - "**/empty.html", + "**/*", lambda route: ( - intercepted.append(2), # type: ignore + intercepted.append(1), asyncio.create_task(route.continue_()), ), ) @@ -62,31 +57,37 @@ def handler1(route): await page.route( "**/empty.html", lambda route: ( - intercepted.append(3), # type: ignore + intercepted.append(2), asyncio.create_task(route.continue_()), ), ) await page.route( - "**/*", + "**/empty.html", lambda route: ( - intercepted.append(4), # type: ignore + intercepted.append(3), asyncio.create_task(route.continue_()), ), ) + def handler4(route): + intercepted.append(4) + asyncio.create_task(route.continue_()) + + await page.route("**/empty.html", handler4) + await page.goto(server.EMPTY_PAGE) - assert intercepted == [1] + assert intercepted == [4] intercepted = [] - await page.unroute("**/empty.html", handler1) + await page.unroute("**/empty.html", handler4) await page.goto(server.EMPTY_PAGE) - assert intercepted == [2] + assert intercepted == [3] intercepted = [] await page.unroute("**/empty.html") await page.goto(server.EMPTY_PAGE) - assert intercepted == [4] + assert intercepted == [1] async def test_page_route_should_work_when_POST_is_redirected_with_302(page, server): From 809727dd64b4a6c8c66a844b7a426b8616106106 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 14 Jul 2021 17:06:26 +0200 Subject: [PATCH 005/646] feat(roll): roll Playwright to 1.13.0-next-1626254028000 (#805) --- playwright/_impl/_api_structures.py | 4 +-- playwright/_impl/_frame.py | 4 +-- playwright/_impl/_page.py | 4 +-- playwright/async_api/_generated.py | 34 ++++++++++++++++------- playwright/sync_api/_generated.py | 42 ++++++++++++++++++++++------- setup.py | 2 +- 6 files changed, 65 insertions(+), 25 deletions(-) diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py index e96c8186e..6eec5f690 100644 --- a/playwright/_impl/_api_structures.py +++ b/playwright/_impl/_api_structures.py @@ -130,5 +130,5 @@ class SecurityDetails(TypedDict): issuer: Optional[str] protocol: Optional[str] subjectName: Optional[str] - validFrom: Optional[int] - validTo: Optional[int] + validFrom: Optional[float] + validTo: Optional[float] diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 1a45e2790..268beb110 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -278,10 +278,10 @@ async def is_editable(self, selector: str, timeout: float = None) -> bool: async def is_enabled(self, selector: str, timeout: float = None) -> bool: return await self._channel.send("isEnabled", locals_to_params(locals())) - async def is_hidden(self, selector: str) -> bool: + async def is_hidden(self, selector: str, timeout: float = None) -> bool: return await self._channel.send("isHidden", locals_to_params(locals())) - async def is_visible(self, selector: str) -> bool: + async def is_visible(self, selector: str, timeout: float = None) -> bool: return await self._channel.send("isVisible", locals_to_params(locals())) async def dispatch_event( diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 1e4614710..e230d43ed 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -339,10 +339,10 @@ async def is_editable(self, selector: str, timeout: float = None) -> bool: async def is_enabled(self, selector: str, timeout: float = None) -> bool: return await self._main_frame.is_enabled(**locals_to_params(locals())) - async def is_hidden(self, selector: str) -> bool: + async def is_hidden(self, selector: str, timeout: float = None) -> bool: return await self._main_frame.is_hidden(**locals_to_params(locals())) - async def is_visible(self, selector: str) -> bool: + async def is_visible(self, selector: str, timeout: float = None) -> bool: return await self._main_frame.is_visible(**locals_to_params(locals())) async def dispatch_event( diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 225a1a56f..c8d0b8f3e 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -404,7 +404,7 @@ async def security_details(self) -> typing.Optional[SecurityDetails]: Returns ------- - Union[{issuer: Union[str, NoneType], protocol: Union[str, NoneType], subjectName: Union[str, NoneType], validFrom: Union[int, NoneType], validTo: Union[int, NoneType]}, NoneType] + Union[{issuer: Union[str, NoneType], protocol: Union[str, NoneType], subjectName: Union[str, NoneType], validFrom: Union[float, NoneType], validTo: Union[float, NoneType]}, NoneType] """ return mapping.from_impl_nullable( @@ -3283,7 +3283,7 @@ async def is_enabled(self, selector: str, *, timeout: float = None) -> bool: ) ) - async def is_hidden(self, selector: str) -> bool: + async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: """Frame.is_hidden Returns whether the element is hidden, the opposite of [visible](./actionability.md#visible). `selector` that does not @@ -3294,6 +3294,9 @@ async def is_hidden(self, selector: str) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + timeout : Union[float, NoneType] + Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -3302,11 +3305,12 @@ async def is_hidden(self, selector: str) -> bool: return mapping.from_maybe_impl( await self._async( - "frame.is_hidden", self._impl_obj.is_hidden(selector=selector) + "frame.is_hidden", + self._impl_obj.is_hidden(selector=selector, timeout=timeout), ) ) - async def is_visible(self, selector: str) -> bool: + async def is_visible(self, selector: str, *, timeout: float = None) -> bool: """Frame.is_visible Returns whether the element is [visible](./actionability.md#visible). `selector` that does not match any elements is @@ -3317,6 +3321,9 @@ async def is_visible(self, selector: str) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + timeout : Union[float, NoneType] + Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -3325,7 +3332,8 @@ async def is_visible(self, selector: str) -> bool: return mapping.from_maybe_impl( await self._async( - "frame.is_visible", self._impl_obj.is_visible(selector=selector) + "frame.is_visible", + self._impl_obj.is_visible(selector=selector, timeout=timeout), ) ) @@ -5455,7 +5463,7 @@ async def is_enabled(self, selector: str, *, timeout: float = None) -> bool: ) ) - async def is_hidden(self, selector: str) -> bool: + async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: """Page.is_hidden Returns whether the element is hidden, the opposite of [visible](./actionability.md#visible). `selector` that does not @@ -5466,6 +5474,9 @@ async def is_hidden(self, selector: str) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + timeout : Union[float, NoneType] + Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -5474,11 +5485,12 @@ async def is_hidden(self, selector: str) -> bool: return mapping.from_maybe_impl( await self._async( - "page.is_hidden", self._impl_obj.is_hidden(selector=selector) + "page.is_hidden", + self._impl_obj.is_hidden(selector=selector, timeout=timeout), ) ) - async def is_visible(self, selector: str) -> bool: + async def is_visible(self, selector: str, *, timeout: float = None) -> bool: """Page.is_visible Returns whether the element is [visible](./actionability.md#visible). `selector` that does not match any elements is @@ -5489,6 +5501,9 @@ async def is_visible(self, selector: str) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + timeout : Union[float, NoneType] + Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -5497,7 +5512,8 @@ async def is_visible(self, selector: str) -> bool: return mapping.from_maybe_impl( await self._async( - "page.is_visible", self._impl_obj.is_visible(selector=selector) + "page.is_visible", + self._impl_obj.is_visible(selector=selector, timeout=timeout), ) ) diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 3a444150e..e5c212be6 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -404,7 +404,7 @@ def security_details(self) -> typing.Optional[SecurityDetails]: Returns ------- - Union[{issuer: Union[str, NoneType], protocol: Union[str, NoneType], subjectName: Union[str, NoneType], validFrom: Union[int, NoneType], validTo: Union[int, NoneType]}, NoneType] + Union[{issuer: Union[str, NoneType], protocol: Union[str, NoneType], subjectName: Union[str, NoneType], validFrom: Union[float, NoneType], validTo: Union[float, NoneType]}, NoneType] """ return mapping.from_impl_nullable( @@ -3261,7 +3261,7 @@ def is_enabled(self, selector: str, *, timeout: float = None) -> bool: ) ) - def is_hidden(self, selector: str) -> bool: + def is_hidden(self, selector: str, *, timeout: float = None) -> bool: """Frame.is_hidden Returns whether the element is hidden, the opposite of [visible](./actionability.md#visible). `selector` that does not @@ -3272,6 +3272,9 @@ def is_hidden(self, selector: str) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + timeout : Union[float, NoneType] + Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -3279,10 +3282,13 @@ def is_hidden(self, selector: str) -> bool: """ return mapping.from_maybe_impl( - self._sync("frame.is_hidden", self._impl_obj.is_hidden(selector=selector)) + self._sync( + "frame.is_hidden", + self._impl_obj.is_hidden(selector=selector, timeout=timeout), + ) ) - def is_visible(self, selector: str) -> bool: + def is_visible(self, selector: str, *, timeout: float = None) -> bool: """Frame.is_visible Returns whether the element is [visible](./actionability.md#visible). `selector` that does not match any elements is @@ -3293,6 +3299,9 @@ def is_visible(self, selector: str) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + timeout : Union[float, NoneType] + Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -3300,7 +3309,10 @@ def is_visible(self, selector: str) -> bool: """ return mapping.from_maybe_impl( - self._sync("frame.is_visible", self._impl_obj.is_visible(selector=selector)) + self._sync( + "frame.is_visible", + self._impl_obj.is_visible(selector=selector, timeout=timeout), + ) ) def dispatch_event( @@ -5417,7 +5429,7 @@ def is_enabled(self, selector: str, *, timeout: float = None) -> bool: ) ) - def is_hidden(self, selector: str) -> bool: + def is_hidden(self, selector: str, *, timeout: float = None) -> bool: """Page.is_hidden Returns whether the element is hidden, the opposite of [visible](./actionability.md#visible). `selector` that does not @@ -5428,6 +5440,9 @@ def is_hidden(self, selector: str) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + timeout : Union[float, NoneType] + Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -5435,10 +5450,13 @@ def is_hidden(self, selector: str) -> bool: """ return mapping.from_maybe_impl( - self._sync("page.is_hidden", self._impl_obj.is_hidden(selector=selector)) + self._sync( + "page.is_hidden", + self._impl_obj.is_hidden(selector=selector, timeout=timeout), + ) ) - def is_visible(self, selector: str) -> bool: + def is_visible(self, selector: str, *, timeout: float = None) -> bool: """Page.is_visible Returns whether the element is [visible](./actionability.md#visible). `selector` that does not match any elements is @@ -5449,6 +5467,9 @@ def is_visible(self, selector: str) -> bool: selector : str A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + timeout : Union[float, NoneType] + Maximum 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_timeout()` or `page.set_default_timeout()` methods. Returns ------- @@ -5456,7 +5477,10 @@ def is_visible(self, selector: str) -> bool: """ return mapping.from_maybe_impl( - self._sync("page.is_visible", self._impl_obj.is_visible(selector=selector)) + self._sync( + "page.is_visible", + self._impl_obj.is_visible(selector=selector, timeout=timeout), + ) ) def dispatch_event( diff --git a/setup.py b/setup.py index 2cc574bfa..6aef80b99 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ InWheel = None from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.13.0-next-1625812834000" +driver_version = "1.13.0-next-1626254028000" def extractall(zip: zipfile.ZipFile, path: str) -> None: From 673eed8f30afcc51f23d584993dd014f86c92b14 Mon Sep 17 00:00:00 2001 From: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com> Date: Fri, 16 Jul 2021 00:07:26 +0530 Subject: [PATCH 006/646] fix: improve typing for SyncBase and AsyncBase (#790) --- playwright/_impl/_async_base.py | 30 ++++++++++++++++++++---------- playwright/_impl/_sync_base.py | 30 +++++++++++++++++++----------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/playwright/_impl/_async_base.py b/playwright/_impl/_async_base.py index 587d0d200..4d678f668 100644 --- a/playwright/_impl/_async_base.py +++ b/playwright/_impl/_async_base.py @@ -15,7 +15,7 @@ import asyncio import traceback from types import TracebackType -from typing import Any, Awaitable, Callable, Generic, Type, TypeVar +from typing import Any, Awaitable, Callable, Generic, Type, TypeVar, Union from playwright._impl._impl_to_api_mapping import ImplToApiMapping, ImplWrapper @@ -23,11 +23,11 @@ T = TypeVar("T") -Self = TypeVar("Self", bound="AsyncBase") +Self = TypeVar("Self", bound="AsyncContextManager") class AsyncEventInfo(Generic[T]): - def __init__(self, future: asyncio.Future) -> None: + def __init__(self, future: "asyncio.Future[T]") -> None: self._future = future @property @@ -39,13 +39,18 @@ def is_done(self) -> bool: class AsyncEventContextManager(Generic[T]): - def __init__(self, future: asyncio.Future) -> None: - self._event: AsyncEventInfo = AsyncEventInfo(future) + def __init__(self, future: "asyncio.Future[T]") -> None: + self._event = AsyncEventInfo[T](future) async def __aenter__(self) -> AsyncEventInfo[T]: return self._event - async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + async def __aexit__( + self, + exc_type: Type[BaseException], + exc_val: BaseException, + exc_tb: TracebackType, + ) -> None: await self._event.value @@ -68,17 +73,19 @@ def _wrap_handler(self, handler: Any) -> Callable[..., None]: return mapping.wrap_handler(handler) return handler - def on(self, event: str, f: Any) -> None: + def on(self, event: str, f: Callable[..., Union[Awaitable[None], None]]) -> None: """Registers the function ``f`` to the event name ``event``.""" self._impl_obj.on(event, self._wrap_handler(f)) - def once(self, event: str, f: Any) -> None: + def once(self, event: str, f: Callable[..., Union[Awaitable[None], None]]) -> None: """The same as ``self.on``, except that the listener is automatically removed after being called. """ self._impl_obj.once(event, self._wrap_handler(f)) - def remove_listener(self, event: str, f: Any) -> None: + def remove_listener( + self, event: str, f: Callable[..., Union[Awaitable[None], None]] + ) -> None: """Removes the function ``f`` from ``event``.""" self._impl_obj.remove_listener(event, self._wrap_handler(f)) @@ -93,4 +100,7 @@ async def __aexit__( exc_val: BaseException, traceback: TracebackType, ) -> None: - await self.close() # type: ignore + await self.close() + + async def close(self: Self) -> None: + ... diff --git a/playwright/_impl/_sync_base.py b/playwright/_impl/_sync_base.py index 005161a51..4e0272420 100644 --- a/playwright/_impl/_sync_base.py +++ b/playwright/_impl/_sync_base.py @@ -36,18 +36,18 @@ T = TypeVar("T") -Self = TypeVar("Self") +Self = TypeVar("Self", bound="SyncContextManager") class EventInfo(Generic[T]): - def __init__(self, sync_base: "SyncBase", future: asyncio.Future) -> None: + def __init__(self, sync_base: "SyncBase", future: "asyncio.Future[T]") -> None: self._sync_base = sync_base self._value: Optional[T] = None - self._exception = None + self._exception: Optional[Exception] = None self._future = future g_self = greenlet.getcurrent() - def done_callback(task: Any) -> None: + def done_callback(task: "asyncio.Future[T]") -> None: try: self._value = mapping.from_maybe_impl(self._future.result()) except Exception as e: @@ -71,13 +71,18 @@ def is_done(self) -> bool: class EventContextManager(Generic[T]): - def __init__(self, sync_base: "SyncBase", future: asyncio.Future) -> None: - self._event: EventInfo = EventInfo(sync_base, future) + def __init__(self, sync_base: "SyncBase", future: "asyncio.Future[T]") -> None: + self._event = EventInfo[T](sync_base, future) def __enter__(self) -> EventInfo[T]: return self._event - def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + def __exit__( + self, + exc_type: Type[BaseException], + exc_val: BaseException, + exc_tb: TracebackType, + ) -> None: self._event.value @@ -110,17 +115,17 @@ def _wrap_handler(self, handler: Any) -> Callable[..., None]: return mapping.wrap_handler(handler) return handler - def on(self, event: str, f: Any) -> None: + def on(self, event: str, f: Callable[..., None]) -> None: """Registers the function ``f`` to the event name ``event``.""" self._impl_obj.on(event, self._wrap_handler(f)) - def once(self, event: str, f: Any) -> None: + def once(self, event: str, f: Callable[..., None]) -> None: """The same as ``self.on``, except that the listener is automatically removed after being called. """ self._impl_obj.once(event, self._wrap_handler(f)) - def remove_listener(self, event: str, f: Any) -> None: + def remove_listener(self, event: str, f: Callable[..., None]) -> None: """Removes the function ``f`` from ``event``.""" self._impl_obj.remove_listener(event, self._wrap_handler(f)) @@ -167,4 +172,7 @@ def __exit__( exc_val: BaseException, traceback: TracebackType, ) -> None: - self.close() # type: ignore + self.close() + + def close(self: Self) -> None: + ... From 5ffa2eff3dfa30d95cd0db322dc8188f2997716a Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 20 Jul 2021 15:12:41 +0200 Subject: [PATCH 007/646] feat(roll): roll Playwright to 1.13.0-1626733671000 (#814) --- README.md | 4 +- playwright/_impl/_browser_type.py | 4 +- playwright/_impl/_frame.py | 11 ++ playwright/_impl/_page.py | 11 ++ playwright/async_api/_generated.py | 190 +++++++++++++++++++++-------- playwright/sync_api/_generated.py | 190 +++++++++++++++++++++-------- setup.py | 2 +- tests/assets/drag-n-drop.html | 1 - tests/async/test_launcher.py | 11 +- tests/async/test_page.py | 11 ++ tests/sync/test_page.py | 11 ++ 11 files changed, 332 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index 1dd36df73..f76c89a14 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 93.0.4543.0 | ✅ | ✅ | ✅ | +| Chromium 93.0.4576.0 | ✅ | ✅ | ✅ | | WebKit 14.2 | ✅ | ✅ | ✅ | -| Firefox 89.0 | ✅ | ✅ | ✅ | +| Firefox 90.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index dff830f76..08aafc1f9 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -85,7 +85,7 @@ async def launch( try: return from_channel(await self._channel.send("launch", params)) except Exception as e: - if "because executable doesn't exist" in str(e): + if "npx playwright install" in str(e): raise not_installed_error(f'"{self.name}" browser was not found.') raise e @@ -145,7 +145,7 @@ async def launch_persistent_context( context._options = params return context except Exception as e: - if "because executable doesn't exist" in str(e): + if "npx playwright install" in str(e): raise not_installed_error(f'"{self.name}" browser was not found.') raise e diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 268beb110..4e1047a8b 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -466,6 +466,17 @@ async def hover( ) -> None: await self._channel.send("hover", locals_to_params(locals())) + async def drag_and_drop( + self, + source: str, + target: str, + force: bool = None, + noWaitAfter: bool = None, + timeout: float = None, + trial: bool = None, + ) -> None: + await self._channel.send("dragAndDrop", locals_to_params(locals())) + async def select_option( self, selector: str, diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index e230d43ed..ed756bf38 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -649,6 +649,17 @@ async def hover( ) -> None: return await self._main_frame.hover(**locals_to_params(locals())) + async def drag_and_drop( + self, + source: str, + target: str, + force: bool = None, + noWaitAfter: bool = None, + timeout: float = None, + trial: bool = None, + ) -> None: + return await self._main_frame.drag_and_drop(**locals_to_params(locals())) + async def select_option( self, selector: str, diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index c8d0b8f3e..295396e5d 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -3187,7 +3187,7 @@ async def is_checked(self, selector: str, *, timeout: float = None) -> bool: Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. timeout : Union[float, NoneType] Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by @@ -3213,7 +3213,7 @@ async def is_disabled(self, selector: str, *, timeout: float = None) -> bool: Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. timeout : Union[float, NoneType] Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by @@ -3239,7 +3239,7 @@ async def is_editable(self, selector: str, *, timeout: float = None) -> bool: Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. timeout : Union[float, NoneType] Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by @@ -3265,7 +3265,7 @@ async def is_enabled(self, selector: str, *, timeout: float = None) -> bool: Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. timeout : Union[float, NoneType] Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by @@ -3292,7 +3292,7 @@ async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. timeout : Union[float, NoneType] Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by @@ -3319,7 +3319,7 @@ async def is_visible(self, selector: str, *, timeout: float = None) -> bool: Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. timeout : Union[float, NoneType] Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by @@ -3378,7 +3378,7 @@ async def dispatch_event( Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. type : str DOM event type: `"click"`, `"dragstart"`, etc. @@ -3659,7 +3659,7 @@ async def click( Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. modifiers : Union[List[Union["Alt", "Control", "Meta", "Shift"]], NoneType] Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current @@ -3739,7 +3739,7 @@ async def dblclick( Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. modifiers : Union[List[Union["Alt", "Control", "Meta", "Shift"]], NoneType] Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current @@ -3813,7 +3813,7 @@ async def tap( Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. modifiers : Union[List[Union["Alt", "Control", "Meta", "Shift"]], NoneType] Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current @@ -3875,7 +3875,7 @@ async def fill( Parameters ---------- selector : str - A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See + A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. value : str Value to fill for the ``, ` diff --git a/tests/async/test_dispatch_event.py b/tests/async/test_dispatch_event.py index 701e5585f..897d797ef 100644 --- a/tests/async/test_dispatch_event.py +++ b/tests/async/test_dispatch_event.py @@ -123,7 +123,7 @@ async def test_should_be_atomic(selectors, page, utils): return result; }, queryAll(root, selector) { - const result = Array.from(root.query_selector_all(selector)); + const result = Array.from(root.querySelectorAll(selector)); for (const e of result) Promise.resolve().then(() => result.onclick = ""); return result; diff --git a/tests/async/test_locators.py b/tests/async/test_locators.py new file mode 100644 index 000000000..3d66e19c2 --- /dev/null +++ b/tests/async/test_locators.py @@ -0,0 +1,458 @@ +# 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 os + +import pytest + +from playwright._impl._path_utils import get_file_dirname +from playwright.async_api import Error, Page +from tests.server import Server + +_dirname = get_file_dirname() +FILE_TO_UPLOAD = _dirname / ".." / "assets/file-to-upload.txt" + + +async def test_locators_click_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + await button.click() + assert await page.evaluate("window['result']") == "Clicked" + + +async def test_locators_click_should_work_with_node_removed(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + await page.evaluate("delete window['Node']") + button = page.locator("button") + await button.click() + assert await page.evaluate("window['result']") == "Clicked" + + +async def test_locators_click_should_work_for_text_nodes(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + await page.evaluate( + """() => { + window['double'] = false; + const button = document.querySelector('button'); + button.addEventListener('dblclick', event => { + window['double'] = true; + }); + }""" + ) + button = page.locator("button") + await button.dblclick() + assert await page.evaluate("double") is True + assert await page.evaluate("result") == "Clicked" + + +async def test_locators_should_have_repr(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + await button.click() + assert ( + str(button) + == f" selector='button'>" + ) + + +async def test_locators_get_attribute_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + button = page.locator("#outer") + assert await button.get_attribute("name") == "value" + assert await button.get_attribute("foo") is None + + +async def test_locators_input_value_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + await page.fill("#textarea", "input value") + text_area = page.locator("#textarea") + assert await text_area.input_value() == "input value" + + +async def test_locators_inner_html_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#outer") + assert await locator.inner_html() == '
Text,\nmore text
' + + +async def test_locators_inner_text_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#inner") + assert await locator.inner_text() == "Text, more text" + + +async def test_locators_text_content_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#inner") + assert await locator.text_content() == "Text,\nmore text" + + +async def test_locators_is_hidden_and_is_visible_should_work(page: Page): + await page.set_content("
Hi
") + + div = page.locator("div") + assert await div.is_visible() is True + assert await div.is_hidden() is False + + span = page.locator("span") + assert await span.is_visible() is False + assert await span.is_hidden() is True + + +async def test_locators_is_enabled_and_is_disabled_should_work(page: Page): + await page.set_content( + """ + + +
div
+ """ + ) + + div = page.locator("div") + assert await div.is_enabled() is True + assert await div.is_disabled() is False + + button1 = page.locator(':text("button1")') + assert await button1.is_enabled() is False + assert await button1.is_disabled() is True + + button1 = page.locator(':text("button2")') + assert await button1.is_enabled() is True + assert await button1.is_disabled() is False + + +async def test_locators_is_editable_should_work(page: Page): + await page.set_content( + """ + + """ + ) + + input1 = page.locator("#input1") + assert await input1.is_editable() is False + + input2 = page.locator("#input2") + assert await input2.is_editable() is True + + +async def test_locators_is_checked_should_work(page: Page): + await page.set_content( + """ +
Not a checkbox
+ """ + ) + + element = page.locator("input") + assert await element.is_checked() is True + await element.evaluate("e => e.checked = false") + assert await element.is_checked() is False + + +async def test_locators_all_text_contents_should_work(page: Page): + await page.set_content( + """ +
A
B
C
+ """ + ) + + element = page.locator("div") + assert await element.all_text_contents() == ["A", "B", "C"] + + +async def test_locators_all_inner_texts(page: Page): + await page.set_content( + """ +
A
B
C
+ """ + ) + + element = page.locator("div") + assert await element.all_inner_texts() == ["A", "B", "C"] + + +async def test_locators_should_query_existing_element(page: Page, server: Server): + await page.goto(server.PREFIX + "/playground.html") + await page.set_content( + """
A
""" + ) + html = page.locator("html") + second = html.locator(".second") + inner = second.locator(".inner") + assert ( + await page.evaluate("e => e.textContent", await inner.element_handle()) == "A" + ) + + +async def test_locators_evaluate_handle_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + outer = page.locator("#outer") + inner = outer.locator("#inner") + check = inner.locator("#check") + text = await inner.evaluate_handle("e => e.firstChild") + await page.evaluate("1 + 1") + assert ( + str(outer) + == f" selector='#outer'>" + ) + assert ( + str(inner) + == f" selector='#outer >> #inner'>" + ) + assert str(text) == "JSHandle@#text=Text,↵more text" + assert ( + str(check) + == f" selector='#outer >> #inner >> #check'>" + ) + + +async def test_locators_should_query_existing_elements(page: Page): + await page.set_content( + """
A

B
""" + ) + html = page.locator("html") + elements = await html.locator("div").element_handles() + assert len(elements) == 2 + result = [] + for element in elements: + result.append(await page.evaluate("e => e.textContent", element)) + assert result == ["A", "B"] + + +async def test_locators_return_empty_array_for_non_existing_elements(page: Page): + await page.set_content( + """
A

B
""" + ) + html = page.locator("html") + elements = await html.locator("abc").element_handles() + assert len(elements) == 0 + assert elements == [] + + +async def test_locators_evaluate_all_should_work(page: Page): + await page.set_content( + """
""" + ) + tweet = page.locator(".tweet .like") + content = await tweet.evaluate_all("nodes => nodes.map(n => n.innerText)") + assert content == ["100", "10"] + + +async def test_locators_evaluate_all_should_work_with_missing_selector(page: Page): + await page.set_content( + """
not-a-child-div
nodes.length") + assert nodes_length == 0 + + +async def test_locators_hover_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/scrollable.html") + button = page.locator("#button-6") + await button.hover() + assert ( + await page.evaluate("document.querySelector('button:hover').id") == "button-6" + ) + + +async def test_locators_fill_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/textarea.html") + button = page.locator("input") + await button.fill("some value") + assert await page.evaluate("result") == "some value" + + +async def test_locators_check_should_work(page: Page): + await page.set_content("") + button = page.locator("input") + await button.check() + assert await page.evaluate("checkbox.checked") is True + + +async def test_locators_uncheck_should_work(page: Page): + await page.set_content("") + button = page.locator("input") + await button.uncheck() + assert await page.evaluate("checkbox.checked") is False + + +async def test_locators_select_option_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/select.html") + select = page.locator("select") + await select.select_option("blue") + assert await page.evaluate("result.onInput") == ["blue"] + assert await page.evaluate("result.onChange") == ["blue"] + + +async def test_locators_focus_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + assert await button.evaluate("button => document.activeElement === button") is False + await button.focus() + assert await button.evaluate("button => document.activeElement === button") is True + + +async def test_locators_dispatch_event_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + await button.dispatch_event("click") + assert await page.evaluate("result") == "Clicked" + + +async def test_locators_should_upload_a_file(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/fileupload.html") + input = page.locator("input[type=file]") + + file_path = os.path.relpath(FILE_TO_UPLOAD, os.getcwd()) + await input.set_input_files(file_path) + assert ( + await page.evaluate("e => e.files[0].name", await input.element_handle()) + == "file-to-upload.txt" + ) + + +async def test_locators_should_press(page: Page): + await page.set_content("") + await page.locator("input").press("h") + await page.eval_on_selector("input", "input => input.value") == "h" + + +async def test_locators_should_scroll_into_view(page: Page, server: Server): + await page.goto(server.PREFIX + "/offscreenbuttons.html") + for i in range(11): + button = page.locator(f"#btn{i}") + before = await button.evaluate( + "button => button.getBoundingClientRect().right - window.innerWidth" + ) + assert before == 10 * i + await button.scroll_into_view_if_needed() + after = await button.evaluate( + "button => button.getBoundingClientRect().right - window.innerWidth" + ) + assert after <= 0 + await page.evaluate("window.scrollTo(0, 0)") + + +async def test_locators_should_select_textarea( + page: Page, server: Server, browser_name: str +): + await page.goto(server.PREFIX + "/input/textarea.html") + textarea = page.locator("textarea") + await textarea.evaluate("textarea => textarea.value = 'some value'") + await textarea.select_text() + if browser_name == "firefox": + assert await textarea.evaluate("el => el.selectionStart") == 0 + assert await textarea.evaluate("el => el.selectionEnd") == 10 + else: + assert await page.evaluate("window.getSelection().toString()") == "some value" + + +async def test_locators_should_type(page: Page): + await page.set_content("") + await page.locator("input").type("hello") + await page.eval_on_selector("input", "input => input.value") == "hello" + + +async def test_locators_should_screenshot( + page: Page, server: Server, assert_to_be_golden +): + await page.set_viewport_size( + { + "width": 500, + "height": 500, + } + ) + await page.goto(server.PREFIX + "/grid.html") + await page.evaluate("window.scrollBy(50, 100)") + element = page.locator(".box:nth-of-type(3)") + assert_to_be_golden( + await element.screenshot(), "screenshot-element-bounding-box.png" + ) + + +async def test_locators_should_return_bounding_box(page: Page, server: Server): + await page.set_viewport_size( + { + "width": 500, + "height": 500, + } + ) + await page.goto(server.PREFIX + "/grid.html") + element = page.locator(".box:nth-of-type(13)") + box = await element.bounding_box() + assert box == { + "x": 100, + "y": 50, + "width": 50, + "height": 50, + } + + +async def test_locators_should_respect_first_and_last(page: Page): + await page.set_content( + """ +
+

A

+

A

A

+

A

A

A

+
""" + ) + assert await page.locator("div >> p").count() == 6 + assert await page.locator("div").locator("p").count() == 6 + assert await page.locator("div").first.locator("p").count() == 1 + assert await page.locator("div").last.locator("p").count() == 3 + + +async def test_locators_should_respect_nth(page: Page): + await page.set_content( + """ +
+

A

+

A

A

+

A

A

A

+
""" + ) + assert await page.locator("div >> p").nth(0).count() == 1 + assert await page.locator("div").nth(1).locator("p").count() == 2 + assert await page.locator("div").nth(2).locator("p").count() == 3 + + +async def test_locators_should_throw_on_capture_without_nth(page: Page): + await page.set_content( + """ +

A

+ """ + ) + with pytest.raises(Error, match="Can't query n-th element"): + await page.locator("*css=div >> p").nth(0).click() + + +async def test_locators_should_throw_due_to_strictness(page: Page): + await page.set_content( + """ +
A
B
+ """ + ) + with pytest.raises(Error, match="strict mode violation"): + await page.locator("div").is_visible() + + +async def test_locators_should_throw_due_to_strictness_2(page: Page): + await page.set_content( + """ + + """ + ) + with pytest.raises(Error, match="strict mode violation"): + await page.locator("option").evaluate("e => {}") diff --git a/tests/async/test_tap.py b/tests/async/test_tap.py index f60cfddce..026e3cdcd 100644 --- a/tests/async/test_tap.py +++ b/tests/async/test_tap.py @@ -16,7 +16,7 @@ import pytest -from playwright.async_api import ElementHandle, JSHandle +from playwright.async_api import ElementHandle, JSHandle, Page @pytest.fixture @@ -190,6 +190,34 @@ async def test_should_wait_until_an_element_is_visible_to_tap_it(page): assert await div.text_content() == "clicked" +async def test_locators_tap(page: Page): + await page.set_content( + """ +
a
+
b
+ """ + ) + await page.locator("#a").tap() + element_handle = await track_events(await page.query_selector("#b")) + await page.locator("#b").tap() + assert await element_handle.json_value() == [ + "pointerover", + "pointerenter", + "pointerdown", + "touchstart", + "pointerup", + "pointerout", + "pointerleave", + "touchend", + "mouseover", + "mouseenter", + "mousemove", + "mousedown", + "mouseup", + "click", + ] + + async def track_events(target: ElementHandle) -> JSHandle: return await target.evaluate_handle( """target => { diff --git a/tests/golden-chromium/screenshot-element-bounding-box.png b/tests/golden-chromium/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..c2c3ddca298aba5c502f56e6656fd9330220b327 GIT binary patch literal 474 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^TK3NDj4wz}vwUf`e5)Ukjg=c4B?ZY6Q{gD+<_wP}}l z6nSQ36>Zn#-f|$iNz7fE$&pKHm-_rCzvuN??>N7ATC?@xO>6j=CvYp+J%0ba_|K}- z?e1;A{tEc13fvXy$m4X`&ax<)>7s7qi)jue-U{}mHHEE$Jc%sMXWq*OW$s-$i%G;W zZF||t6r&}VGkq>ExtSx>>!xYDf7J|T5r=j2<5pbF65(PsvM%I1RJ=tim8p?oS*Dfk z^|OT?x8ELn{&}OJ?ag94p-v0itCJs3c=3Z{t=G@fKZ902`4Zw^|LI!P4gZAOW-CLy zZnhPjNK*3L8l^h>>?RwlH95zZzB$)Wuj{urPJRCQ;w>U!mP`4PSezL|x?Qg=R|`2? z63iqS9eMQe#}9S&%O8Ea|IFgamuF(Pw=sImn|nEL`|j&|;`G&T&-U|Y;!@~!TUip(bP*}7q&3SPEgSM@h9ZZ`&x!)qR}^g{rtKT7+DOSu6{1- HoD!M<45`mj literal 0 HcmV?d00001 diff --git a/tests/golden-firefox/screenshot-element-bounding-box.png b/tests/golden-firefox/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..9e208f86d8f7690a79f6bf31ccdd2032512693a3 GIT binary patch literal 311 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETf1WOmAsLNtuPUxdz^zaVj`B z@qPFONtbB0i_;vpqzdFBF)Le3V%-^74w(2>&P%VDpyJ8VkbApmUV0Y?W5NCJ1q!#f z2{KGi{dq{#RgmwA1z*>R>s67{5*_PQl^!~8(otrJyDqAya=w`9fkofFZy)$NAJ1U0 z@SS}9@F%XH^A%t3s&cJp?ci9f@rmyPmt@<+eZ9*vBSNpn-v9p4;nl8vit}0j|7H8R wg(pd%`Ne64TSgt*coyk4XF!TA6BrTzopr0K4LVFaQ7m literal 0 HcmV?d00001 diff --git a/tests/golden-webkit/screenshot-element-bounding-box.png b/tests/golden-webkit/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..1f74a3bafbc259b654dfd30bea8ac1eb72be4ccb GIT binary patch literal 445 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0B~AY)RhkE)4%caKYZ?lNlHoi#%N% zLn2z=-mvvPoFLNnQ26Dfg&abQC-YrB$iZkB!L_R+vZYC0K*WW`b%nRFXno_1rOpe3 z=d|#x+|qndPi)SdUCVE3Z2`eBeB+G?2?ilS$|-u1QW zZ;tN#6A4T@eqjs??&js6Kf7VQ>&8vX%*;05eDnLUv(hXJwQEoB)=IQ71nk=-$Jc%_ zP@tvz?_HC~dCy(@qt=SacOO+^(C*%>(_Pp5#}wIbP_Hf%Oig82a8rKC*&jFAr+$pQ VbeIWRsLJYD@<);T3K0RTXQytV)U literal 0 HcmV?d00001 diff --git a/tests/sync/test_locators.py b/tests/sync/test_locators.py new file mode 100644 index 000000000..2ffc8388f --- /dev/null +++ b/tests/sync/test_locators.py @@ -0,0 +1,442 @@ +# 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 os + +import pytest + +from playwright._impl._path_utils import get_file_dirname +from playwright.sync_api import Error, Page +from tests.server import Server + +_dirname = get_file_dirname() +FILE_TO_UPLOAD = _dirname / ".." / "assets/file-to-upload.txt" + + +def test_locators_click_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + button.click() + assert page.evaluate("window['result']") == "Clicked" + + +def test_locators_click_should_work_with_node_removed(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + page.evaluate("delete window['Node']") + button = page.locator("button") + button.click() + assert page.evaluate("window['result']") == "Clicked" + + +def test_locators_click_should_work_for_text_nodes(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + page.evaluate( + """() => { + window['double'] = false; + const button = document.querySelector('button'); + button.addEventListener('dblclick', event => { + window['double'] = true; + }); + }""" + ) + button = page.locator("button") + button.dblclick() + assert page.evaluate("double") is True + assert page.evaluate("result") == "Clicked" + + +def test_locators_should_have_repr(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + button.click() + assert ( + str(button) + == f" selector='button'>" + ) + + +def test_locators_get_attribute_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + button = page.locator("#outer") + assert button.get_attribute("name") == "value" + assert button.get_attribute("foo") is None + + +def test_locators_input_value_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + page.fill("#textarea", "input value") + text_area = page.locator("#textarea") + assert text_area.input_value() == "input value" + + +def test_locators_inner_html_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#outer") + assert locator.inner_html() == '
Text,\nmore text
' + + +def test_locators_inner_text_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#inner") + assert locator.inner_text() == "Text, more text" + + +def test_locators_text_content_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#inner") + assert locator.text_content() == "Text,\nmore text" + + +def test_locators_is_hidden_and_is_visible_should_work(page: Page): + page.set_content("
Hi
") + + div = page.locator("div") + assert div.is_visible() is True + assert div.is_hidden() is False + + span = page.locator("span") + assert span.is_visible() is False + assert span.is_hidden() is True + + +def test_locators_is_enabled_and_is_disabled_should_work(page: Page): + page.set_content( + """ + + +
div
+ """ + ) + + div = page.locator("div") + assert div.is_enabled() is True + assert div.is_disabled() is False + + button1 = page.locator(':text("button1")') + assert button1.is_enabled() is False + assert button1.is_disabled() is True + + button1 = page.locator(':text("button2")') + assert button1.is_enabled() is True + assert button1.is_disabled() is False + + +def test_locators_is_editable_should_work(page: Page): + page.set_content( + """ + + """ + ) + + input1 = page.locator("#input1") + assert input1.is_editable() is False + + input2 = page.locator("#input2") + assert input2.is_editable() is True + + +def test_locators_is_checked_should_work(page: Page): + page.set_content( + """ +
Not a checkbox
+ """ + ) + + element = page.locator("input") + assert element.is_checked() is True + element.evaluate("e => e.checked = false") + assert element.is_checked() is False + + +def test_locators_all_text_contents_should_work(page: Page): + page.set_content( + """ +
A
B
C
+ """ + ) + + element = page.locator("div") + assert element.all_text_contents() == ["A", "B", "C"] + + +def test_locators_all_inner_texts(page: Page): + page.set_content( + """ +
A
B
C
+ """ + ) + + element = page.locator("div") + assert element.all_inner_texts() == ["A", "B", "C"] + + +def test_locators_should_query_existing_element(page: Page, server: Server): + page.goto(server.PREFIX + "/playground.html") + page.set_content( + """
A
""" + ) + html = page.locator("html") + second = html.locator(".second") + inner = second.locator(".inner") + assert page.evaluate("e => e.textContent", inner.element_handle()) == "A" + + +def test_locators_evaluate_handle_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + outer = page.locator("#outer") + inner = outer.locator("#inner") + check = inner.locator("#check") + text = inner.evaluate_handle("e => e.firstChild") + page.evaluate("1 + 1") + assert ( + str(outer) + == f" selector='#outer'>" + ) + assert ( + str(inner) + == f" selector='#outer >> #inner'>" + ) + assert str(text) == "JSHandle@#text=Text,↵more text" + assert ( + str(check) + == f" selector='#outer >> #inner >> #check'>" + ) + + +def test_locators_should_query_existing_elements(page: Page): + page.set_content("""
A

B
""") + html = page.locator("html") + elements = html.locator("div").element_handles() + assert len(elements) == 2 + result = [] + for element in elements: + result.append(page.evaluate("e => e.textContent", element)) + assert result == ["A", "B"] + + +def test_locators_return_empty_array_for_non_existing_elements(page: Page): + page.set_content("""
A

B
""") + html = page.locator("html") + elements = html.locator("abc").element_handles() + assert len(elements) == 0 + assert elements == [] + + +def test_locators_evaluate_all_should_work(page: Page): + page.set_content( + """
""" + ) + tweet = page.locator(".tweet .like") + content = tweet.evaluate_all("nodes => nodes.map(n => n.innerText)") + assert content == ["100", "10"] + + +def test_locators_evaluate_all_should_work_with_missing_selector(page: Page): + page.set_content("""
not-a-child-div
nodes.length") + assert nodes_length == 0 + + +def test_locators_hover_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/scrollable.html") + button = page.locator("#button-6") + button.hover() + assert page.evaluate("document.querySelector('button:hover').id") == "button-6" + + +def test_locators_fill_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/textarea.html") + button = page.locator("input") + button.fill("some value") + assert page.evaluate("result") == "some value" + + +def test_locators_check_should_work(page: Page): + page.set_content("") + button = page.locator("input") + button.check() + assert page.evaluate("checkbox.checked") is True + + +def test_locators_uncheck_should_work(page: Page): + page.set_content("") + button = page.locator("input") + button.uncheck() + assert page.evaluate("checkbox.checked") is False + + +def test_locators_select_option_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/select.html") + select = page.locator("select") + select.select_option("blue") + assert page.evaluate("result.onInput") == ["blue"] + assert page.evaluate("result.onChange") == ["blue"] + + +def test_locators_focus_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + assert button.evaluate("button => document.activeElement === button") is False + button.focus() + assert button.evaluate("button => document.activeElement === button") is True + + +def test_locators_dispatch_event_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + button.dispatch_event("click") + assert page.evaluate("result") == "Clicked" + + +def test_locators_should_upload_a_file(page: Page, server: Server): + page.goto(server.PREFIX + "/input/fileupload.html") + input = page.locator("input[type=file]") + + file_path = os.path.relpath(FILE_TO_UPLOAD, os.getcwd()) + input.set_input_files(file_path) + assert ( + page.evaluate("e => e.files[0].name", input.element_handle()) + == "file-to-upload.txt" + ) + + +def test_locators_should_press(page: Page): + page.set_content("") + page.locator("input").press("h") + page.eval_on_selector("input", "input => input.value") == "h" + + +def test_locators_should_scroll_into_view(page: Page, server: Server): + page.goto(server.PREFIX + "/offscreenbuttons.html") + for i in range(11): + button = page.locator(f"#btn{i}") + before = button.evaluate( + "button => button.getBoundingClientRect().right - window.innerWidth" + ) + assert before == 10 * i + button.scroll_into_view_if_needed() + after = button.evaluate( + "button => button.getBoundingClientRect().right - window.innerWidth" + ) + assert after <= 0 + page.evaluate("window.scrollTo(0, 0)") + + +def test_locators_should_select_textarea(page: Page, server: Server, browser_name: str): + page.goto(server.PREFIX + "/input/textarea.html") + textarea = page.locator("textarea") + textarea.evaluate("textarea => textarea.value = 'some value'") + textarea.select_text() + if browser_name == "firefox": + assert textarea.evaluate("el => el.selectionStart") == 0 + assert textarea.evaluate("el => el.selectionEnd") == 10 + else: + assert page.evaluate("window.getSelection().toString()") == "some value" + + +def test_locators_should_type(page: Page): + page.set_content("") + page.locator("input").type("hello") + page.eval_on_selector("input", "input => input.value") == "hello" + + +def test_locators_should_screenshot(page: Page, server: Server, assert_to_be_golden): + page.set_viewport_size( + { + "width": 500, + "height": 500, + } + ) + page.goto(server.PREFIX + "/grid.html") + page.evaluate("window.scrollBy(50, 100)") + element = page.locator(".box:nth-of-type(3)") + assert_to_be_golden(element.screenshot(), "screenshot-element-bounding-box.png") + + +def test_locators_should_return_bounding_box(page: Page, server: Server): + page.set_viewport_size( + { + "width": 500, + "height": 500, + } + ) + page.goto(server.PREFIX + "/grid.html") + element = page.locator(".box:nth-of-type(13)") + box = element.bounding_box() + assert box == { + "x": 100, + "y": 50, + "width": 50, + "height": 50, + } + + +def test_locators_should_respect_first_and_last(page: Page): + page.set_content( + """ +
+

A

+

A

A

+

A

A

A

+
""" + ) + assert page.locator("div >> p").count() == 6 + assert page.locator("div").locator("p").count() == 6 + assert page.locator("div").first.locator("p").count() == 1 + assert page.locator("div").last.locator("p").count() == 3 + + +def test_locators_should_respect_nth(page: Page): + page.set_content( + """ +
+

A

+

A

A

+

A

A

A

+
""" + ) + assert page.locator("div >> p").nth(0).count() == 1 + assert page.locator("div").nth(1).locator("p").count() == 2 + assert page.locator("div").nth(2).locator("p").count() == 3 + + +def test_locators_should_throw_on_capture_without_nth(page: Page): + page.set_content( + """ +

A

+ """ + ) + with pytest.raises(Error, match="Can't query n-th element"): + page.locator("*css=div >> p").nth(0).click() + + +def test_locators_should_throw_due_to_strictness(page: Page): + page.set_content( + """ +
A
B
+ """ + ) + with pytest.raises(Error, match="strict mode violation"): + page.locator("div").is_visible() + + +def test_locators_should_throw_due_to_strictness_2(page: Page): + page.set_content( + """ + + """ + ) + with pytest.raises(Error, match="strict mode violation"): + page.locator("option").evaluate("e => {}") From 12ba09d279b2bb8da75a853f9cd04cd67121c619 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 9 Aug 2021 01:23:19 +0200 Subject: [PATCH 015/646] feat(roll): roll Playwright 1.14.0-next-1628399336000 (#839) --- playwright/_impl/_frame.py | 2 ++ playwright/_impl/_page.py | 2 ++ playwright/_impl/_tracing.py | 2 +- playwright/async_api/_generated.py | 28 ++++++++++++++++++++++++---- playwright/sync_api/_generated.py | 28 ++++++++++++++++++++++++---- setup.py | 2 +- tests/async/test_console.py | 19 +++++++++++++------ tests/async/test_jshandle.py | 13 ++++++++----- tests/async/test_worker.py | 7 +++++-- tests/sync/test_console.py | 19 +++++++++++++------ tests/sync/test_sync.py | 10 +++++++--- 11 files changed, 100 insertions(+), 32 deletions(-) diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index c79a50b95..a947d9cf1 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -522,6 +522,8 @@ async def drag_and_drop( self, source: str, target: str, + source_position: Position = None, + target_position: Position = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index fd0c2aa19..5e0bce5cd 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -699,6 +699,8 @@ async def drag_and_drop( self, source: str, target: str, + source_position: Position = None, + target_position: Position = None, force: bool = None, noWaitAfter: bool = None, timeout: float = None, diff --git a/playwright/_impl/_tracing.py b/playwright/_impl/_tracing.py index 45b9bb0da..784770888 100644 --- a/playwright/_impl/_tracing.py +++ b/playwright/_impl/_tracing.py @@ -37,7 +37,6 @@ async def start( await self._channel.send("tracingStart", params) async def stop(self, path: Union[pathlib.Path, str] = None) -> None: - await self._channel.send("tracingStop") if path: artifact = cast( Artifact, from_channel(await self._channel.send("tracingExport")) @@ -46,3 +45,4 @@ async def stop(self, path: Union[pathlib.Path, str] = None) -> None: artifact._is_remote = self._context._browser._is_remote await artifact.save_as(path) await artifact.delete() + await self._channel.send("tracingStop") diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 6d2371ef7..c0372b75d 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -1905,7 +1905,7 @@ async def select_text( async def input_value(self, *, timeout: float = None) -> str: """ElementHandle.input_value - Returns `input.value` for `` or `") page.eval_on_selector("textarea", "t => t.readOnly = true") input1 = page.query_selector("#input1") + assert input1 assert input1.is_editable() is False assert page.is_editable("#input1") is False input2 = page.query_selector("#input2") + assert input2 assert input2.is_editable() assert page.is_editable("#input2") textarea = page.query_selector("textarea") + assert textarea assert textarea.is_editable() is False assert page.is_editable("textarea") is False -def test_is_checked_should_work(page): +def test_is_checked_should_work(page: Page) -> None: page.set_content('
Not a checkbox
') handle = page.query_selector("input") + assert handle assert handle.is_checked() assert page.is_checked("input") handle.evaluate("input => input.checked = false") @@ -557,9 +637,10 @@ def test_is_checked_should_work(page): assert "Not a checkbox or radio button" in exc_info.value.message -def test_input_value(page: Page, server: Server): +def test_input_value(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/textarea.html") element = page.query_selector("input") + assert element element.fill("my-text-content") assert element.input_value() == "my-text-content" @@ -567,9 +648,10 @@ def test_input_value(page: Page, server: Server): assert element.input_value() == "" -def test_set_checked(page: Page): +def test_set_checked(page: Page) -> None: page.set_content("``") input = page.query_selector("input") + assert input input.set_checked(True) assert page.evaluate("checkbox.checked") input.set_checked(False) diff --git a/tests/sync/test_element_handle_wait_for_element_state.py b/tests/sync/test_element_handle_wait_for_element_state.py index c5604c2a7..8f1f2912c 100644 --- a/tests/sync/test_element_handle_wait_for_element_state.py +++ b/tests/sync/test_element_handle_wait_for_element_state.py @@ -14,94 +14,105 @@ import pytest -from playwright.async_api import Error +from playwright.sync_api import Error, Page -def test_should_wait_for_visible(page): +def test_should_wait_for_visible(page: Page) -> None: page.set_content('') div = page.query_selector("div") + assert div page.evaluate('setTimeout(() => div.style.display = "block", 500)') assert div.is_visible() is False div.wait_for_element_state("visible") assert div.is_visible() -def test_should_wait_for_already_visible(page): +def test_should_wait_for_already_visible(page: Page) -> None: page.set_content("
content
") div = page.query_selector("div") + assert div div.wait_for_element_state("visible") -def test_should_timeout_waiting_for_visible(page): +def test_should_timeout_waiting_for_visible(page: Page) -> None: page.set_content('
content
') div = page.query_selector("div") + assert div with pytest.raises(Error) as exc_info: div.wait_for_element_state("visible", timeout=1000) assert "Timeout 1000ms exceeded" in exc_info.value.message -def test_should_throw_waiting_for_visible_when_detached(page): +def test_should_throw_waiting_for_visible_when_detached(page: Page) -> None: page.set_content('') div = page.query_selector("div") + assert div page.evaluate("setTimeout(() => div.remove(), 500)") with pytest.raises(Error) as exc_info: div.wait_for_element_state("visible") assert "Element is not attached to the DOM" in exc_info.value.message -def test_should_wait_for_hidden(page): +def test_should_wait_for_hidden(page: Page) -> None: page.set_content("
content
") div = page.query_selector("div") + assert div page.evaluate('setTimeout(() => div.style.display = "none", 500)') assert div.is_hidden() is False div.wait_for_element_state("hidden") assert div.is_hidden() -def test_should_wait_for_already_hidden(page): +def test_should_wait_for_already_hidden(page: Page) -> None: page.set_content("
") div = page.query_selector("div") + assert div div.wait_for_element_state("hidden") -def test_should_wait_for_hidden_when_detached(page): +def test_should_wait_for_hidden_when_detached(page: Page) -> None: page.set_content("
content
") div = page.query_selector("div") + assert div page.evaluate("setTimeout(() => div.remove(), 500)") div.wait_for_element_state("hidden") assert div.is_hidden() -def test_should_wait_for_enabled_button(page, server): +def test_should_wait_for_enabled_button(page: Page) -> None: page.set_content("") span = page.query_selector("text=Target") + assert span assert span.is_enabled() is False page.evaluate("setTimeout(() => button.disabled = false, 500)") span.wait_for_element_state("enabled") assert span.is_enabled() -def test_should_throw_waiting_for_enabled_when_detached(page): +def test_should_throw_waiting_for_enabled_when_detached(page: Page) -> None: page.set_content("") button = page.query_selector("button") + assert button page.evaluate("setTimeout(() => button.remove(), 500)") with pytest.raises(Error) as exc_info: button.wait_for_element_state("enabled") assert "Element is not attached to the DOM" in exc_info.value.message -def test_should_wait_for_disabled_button(page): +def test_should_wait_for_disabled_button(page: Page) -> None: page.set_content("") span = page.query_selector("text=Target") + assert span assert span.is_disabled() is False page.evaluate("setTimeout(() => button.disabled = true, 500)") span.wait_for_element_state("disabled") assert span.is_disabled() -def test_should_wait_for_editable_input(page, server): +def test_should_wait_for_editable_input(page: Page) -> None: page.set_content("") input = page.query_selector("input") + assert input page.evaluate("setTimeout(() => input.readOnly = false, 500)") assert input.is_editable() is False input.wait_for_element_state("editable") diff --git a/tests/sync/test_har.py b/tests/sync/test_har.py index f511c74c9..52e3166cf 100644 --- a/tests/sync/test_har.py +++ b/tests/sync/test_har.py @@ -15,9 +15,13 @@ import base64 import json import os +from pathlib import Path +from playwright.sync_api import Browser +from tests.server import Server -def test_should_work(browser, server, tmpdir): + +def test_should_work(browser: Browser, server: Server, tmpdir: Path) -> None: path = os.path.join(tmpdir, "log.har") context = browser.new_context(record_har_path=path) page = context.new_page() @@ -28,7 +32,7 @@ def test_should_work(browser, server, tmpdir): assert "log" in data -def test_should_omit_content(browser, server, tmpdir): +def test_should_omit_content(browser: Browser, server: Server, tmpdir: Path) -> None: path = os.path.join(tmpdir, "log.har") context = browser.new_context(record_har_path=path, record_har_omit_content=True) page = context.new_page() @@ -43,7 +47,7 @@ def test_should_omit_content(browser, server, tmpdir): assert "text" not in content1 -def test_should_include_content(browser, server, tmpdir): +def test_should_include_content(browser: Browser, server: Server, tmpdir: Path) -> None: path = os.path.join(tmpdir, "log.har") context = browser.new_context(record_har_path=path) page = context.new_page() diff --git a/tests/sync/test_input.py b/tests/sync/test_input.py index 86a64d106..ff28f6a63 100644 --- a/tests/sync/test_input.py +++ b/tests/sync/test_input.py @@ -13,7 +13,10 @@ # limitations under the License. -def test_expect_file_chooser(page, server): +from playwright.sync_api import Page + + +def test_expect_file_chooser(page: Page) -> None: page.set_content("") with page.expect_file_chooser() as fc_info: page.click('input[type="file"]') diff --git a/tests/sync/test_listeners.py b/tests/sync/test_listeners.py index 49d766114..56a7afb2f 100644 --- a/tests/sync/test_listeners.py +++ b/tests/sync/test_listeners.py @@ -13,10 +13,14 @@ # limitations under the License. -def test_listeners(page, server): +from playwright.sync_api import Page, Response +from tests.server import Server + + +def test_listeners(page: Page, server: Server) -> None: log = [] - def print_response(response): + def print_response(response: Response) -> None: log.append(response) page.on("response", print_response) diff --git a/tests/sync/test_locators.py b/tests/sync/test_locators.py index 3c9cac1c5..07050542e 100644 --- a/tests/sync/test_locators.py +++ b/tests/sync/test_locators.py @@ -13,6 +13,7 @@ # limitations under the License. import os +from typing import Callable import pytest @@ -24,14 +25,16 @@ FILE_TO_UPLOAD = _dirname / ".." / "assets/file-to-upload.txt" -def test_locators_click_should_work(page: Page, server: Server): +def test_locators_click_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/button.html") button = page.locator("button") button.click() assert page.evaluate("window['result']") == "Clicked" -def test_locators_click_should_work_with_node_removed(page: Page, server: Server): +def test_locators_click_should_work_with_node_removed( + page: Page, server: Server +) -> None: page.goto(server.PREFIX + "/input/button.html") page.evaluate("delete window['Node']") button = page.locator("button") @@ -39,7 +42,7 @@ def test_locators_click_should_work_with_node_removed(page: Page, server: Server assert page.evaluate("window['result']") == "Clicked" -def test_locators_click_should_work_for_text_nodes(page: Page, server: Server): +def test_locators_click_should_work_for_text_nodes(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/button.html") page.evaluate( """() => { @@ -56,7 +59,7 @@ def test_locators_click_should_work_for_text_nodes(page: Page, server: Server): assert page.evaluate("result") == "Clicked" -def test_locators_should_have_repr(page: Page, server: Server): +def test_locators_should_have_repr(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/button.html") button = page.locator("button") button.click() @@ -66,39 +69,39 @@ def test_locators_should_have_repr(page: Page, server: Server): ) -def test_locators_get_attribute_should_work(page: Page, server: Server): +def test_locators_get_attribute_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") button = page.locator("#outer") assert button.get_attribute("name") == "value" assert button.get_attribute("foo") is None -def test_locators_input_value_should_work(page: Page, server: Server): +def test_locators_input_value_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") page.fill("#textarea", "input value") text_area = page.locator("#textarea") assert text_area.input_value() == "input value" -def test_locators_inner_html_should_work(page: Page, server: Server): +def test_locators_inner_html_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") locator = page.locator("#outer") assert locator.inner_html() == '
Text,\nmore text
' -def test_locators_inner_text_should_work(page: Page, server: Server): +def test_locators_inner_text_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") locator = page.locator("#inner") assert locator.inner_text() == "Text, more text" -def test_locators_text_content_should_work(page: Page, server: Server): +def test_locators_text_content_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") locator = page.locator("#inner") assert locator.text_content() == "Text,\nmore text" -def test_locators_is_hidden_and_is_visible_should_work(page: Page): +def test_locators_is_hidden_and_is_visible_should_work(page: Page) -> None: page.set_content("
Hi
") div = page.locator("div") @@ -110,7 +113,7 @@ def test_locators_is_hidden_and_is_visible_should_work(page: Page): assert span.is_hidden() is True -def test_locators_is_enabled_and_is_disabled_should_work(page: Page): +def test_locators_is_enabled_and_is_disabled_should_work(page: Page) -> None: page.set_content( """ @@ -132,7 +135,7 @@ def test_locators_is_enabled_and_is_disabled_should_work(page: Page): assert button1.is_disabled() is False -def test_locators_is_editable_should_work(page: Page): +def test_locators_is_editable_should_work(page: Page) -> None: page.set_content( """ @@ -146,7 +149,7 @@ def test_locators_is_editable_should_work(page: Page): assert input2.is_editable() is True -def test_locators_is_checked_should_work(page: Page): +def test_locators_is_checked_should_work(page: Page) -> None: page.set_content( """
Not a checkbox
@@ -159,7 +162,7 @@ def test_locators_is_checked_should_work(page: Page): assert element.is_checked() is False -def test_locators_all_text_contents_should_work(page: Page): +def test_locators_all_text_contents_should_work(page: Page) -> None: page.set_content( """
A
B
C
@@ -170,7 +173,7 @@ def test_locators_all_text_contents_should_work(page: Page): assert element.all_text_contents() == ["A", "B", "C"] -def test_locators_all_inner_texts(page: Page): +def test_locators_all_inner_texts(page: Page) -> None: page.set_content( """
A
B
C
@@ -181,7 +184,7 @@ def test_locators_all_inner_texts(page: Page): assert element.all_inner_texts() == ["A", "B", "C"] -def test_locators_should_query_existing_element(page: Page, server: Server): +def test_locators_should_query_existing_element(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/playground.html") page.set_content( """
A
""" @@ -192,7 +195,7 @@ def test_locators_should_query_existing_element(page: Page, server: Server): assert page.evaluate("e => e.textContent", inner.element_handle()) == "A" -def test_locators_evaluate_handle_should_work(page: Page, server: Server): +def test_locators_evaluate_handle_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") outer = page.locator("#outer") inner = outer.locator("#inner") @@ -214,7 +217,7 @@ def test_locators_evaluate_handle_should_work(page: Page, server: Server): ) -def test_locators_should_query_existing_elements(page: Page): +def test_locators_should_query_existing_elements(page: Page) -> None: page.set_content("""
A

B
""") html = page.locator("html") elements = html.locator("div").element_handles() @@ -225,7 +228,7 @@ def test_locators_should_query_existing_elements(page: Page): assert result == ["A", "B"] -def test_locators_return_empty_array_for_non_existing_elements(page: Page): +def test_locators_return_empty_array_for_non_existing_elements(page: Page) -> None: page.set_content("""
A

B
""") html = page.locator("html") elements = html.locator("abc").element_handles() @@ -233,7 +236,7 @@ def test_locators_return_empty_array_for_non_existing_elements(page: Page): assert elements == [] -def test_locators_evaluate_all_should_work(page: Page): +def test_locators_evaluate_all_should_work(page: Page) -> None: page.set_content( """
""" ) @@ -242,42 +245,42 @@ def test_locators_evaluate_all_should_work(page: Page): assert content == ["100", "10"] -def test_locators_evaluate_all_should_work_with_missing_selector(page: Page): +def test_locators_evaluate_all_should_work_with_missing_selector(page: Page) -> None: page.set_content("""
not-a-child-div
nodes.length") assert nodes_length == 0 -def test_locators_hover_should_work(page: Page, server: Server): +def test_locators_hover_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/scrollable.html") button = page.locator("#button-6") button.hover() assert page.evaluate("document.querySelector('button:hover').id") == "button-6" -def test_locators_fill_should_work(page: Page, server: Server): +def test_locators_fill_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/textarea.html") button = page.locator("input") button.fill("some value") assert page.evaluate("result") == "some value" -def test_locators_check_should_work(page: Page): +def test_locators_check_should_work(page: Page) -> None: page.set_content("") button = page.locator("input") button.check() assert page.evaluate("checkbox.checked") is True -def test_locators_uncheck_should_work(page: Page): +def test_locators_uncheck_should_work(page: Page) -> None: page.set_content("") button = page.locator("input") button.uncheck() assert page.evaluate("checkbox.checked") is False -def test_locators_select_option_should_work(page: Page, server: Server): +def test_locators_select_option_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/select.html") select = page.locator("select") select.select_option("blue") @@ -285,7 +288,7 @@ def test_locators_select_option_should_work(page: Page, server: Server): assert page.evaluate("result.onChange") == ["blue"] -def test_locators_focus_should_work(page: Page, server: Server): +def test_locators_focus_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/button.html") button = page.locator("button") assert button.evaluate("button => document.activeElement === button") is False @@ -293,14 +296,14 @@ def test_locators_focus_should_work(page: Page, server: Server): assert button.evaluate("button => document.activeElement === button") is True -def test_locators_dispatch_event_should_work(page: Page, server: Server): +def test_locators_dispatch_event_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/button.html") button = page.locator("button") button.dispatch_event("click") assert page.evaluate("result") == "Clicked" -def test_locators_should_upload_a_file(page: Page, server: Server): +def test_locators_should_upload_a_file(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/fileupload.html") input = page.locator("input[type=file]") @@ -312,13 +315,13 @@ def test_locators_should_upload_a_file(page: Page, server: Server): ) -def test_locators_should_press(page: Page): +def test_locators_should_press(page: Page) -> None: page.set_content("") page.locator("input").press("h") page.eval_on_selector("input", "input => input.value") == "h" -def test_locators_should_scroll_into_view(page: Page, server: Server): +def test_locators_should_scroll_into_view(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/offscreenbuttons.html") for i in range(11): button = page.locator(f"#btn{i}") @@ -334,7 +337,9 @@ def test_locators_should_scroll_into_view(page: Page, server: Server): page.evaluate("window.scrollTo(0, 0)") -def test_locators_should_select_textarea(page: Page, server: Server, browser_name: str): +def test_locators_should_select_textarea( + page: Page, server: Server, browser_name: str +) -> None: page.goto(server.PREFIX + "/input/textarea.html") textarea = page.locator("textarea") textarea.evaluate("textarea => textarea.value = 'some value'") @@ -346,13 +351,15 @@ def test_locators_should_select_textarea(page: Page, server: Server, browser_nam assert page.evaluate("window.getSelection().toString()") == "some value" -def test_locators_should_type(page: Page): +def test_locators_should_type(page: Page) -> None: page.set_content("") page.locator("input").type("hello") page.eval_on_selector("input", "input => input.value") == "hello" -def test_locators_should_screenshot(page: Page, server: Server, assert_to_be_golden): +def test_locators_should_screenshot( + page: Page, server: Server, assert_to_be_golden: Callable[[bytes, str], None] +) -> None: page.set_viewport_size( { "width": 500, @@ -365,7 +372,7 @@ def test_locators_should_screenshot(page: Page, server: Server, assert_to_be_gol assert_to_be_golden(element.screenshot(), "screenshot-element-bounding-box.png") -def test_locators_should_return_bounding_box(page: Page, server: Server): +def test_locators_should_return_bounding_box(page: Page, server: Server) -> None: page.set_viewport_size( { "width": 500, @@ -383,7 +390,7 @@ def test_locators_should_return_bounding_box(page: Page, server: Server): } -def test_locators_should_respect_first_and_last(page: Page): +def test_locators_should_respect_first_and_last(page: Page) -> None: page.set_content( """
@@ -398,7 +405,7 @@ def test_locators_should_respect_first_and_last(page: Page): assert page.locator("div").last.locator("p").count() == 3 -def test_locators_should_respect_nth(page: Page): +def test_locators_should_respect_nth(page: Page) -> None: page.set_content( """
@@ -412,7 +419,7 @@ def test_locators_should_respect_nth(page: Page): assert page.locator("div").nth(2).locator("p").count() == 3 -def test_locators_should_throw_on_capture_without_nth(page: Page): +def test_locators_should_throw_on_capture_without_nth(page: Page) -> None: page.set_content( """

A

@@ -422,7 +429,7 @@ def test_locators_should_throw_on_capture_without_nth(page: Page): page.locator("*css=div >> p").nth(1).click() -def test_locators_should_throw_due_to_strictness(page: Page): +def test_locators_should_throw_due_to_strictness(page: Page) -> None: page.set_content( """
A
B
@@ -432,7 +439,7 @@ def test_locators_should_throw_due_to_strictness(page: Page): page.locator("div").is_visible() -def test_locators_should_throw_due_to_strictness_2(page: Page): +def test_locators_should_throw_due_to_strictness_2(page: Page) -> None: page.set_content( """ @@ -442,7 +449,7 @@ def test_locators_should_throw_due_to_strictness_2(page: Page): page.locator("option").evaluate("e => {}") -def test_locators_set_checked(page: Page): +def test_locators_set_checked(page: Page) -> None: page.set_content("``") locator = page.locator("input") locator.set_checked(True) diff --git a/tests/sync/test_network.py b/tests/sync/test_network.py index c81263799..f974b0a6c 100644 --- a/tests/sync/test_network.py +++ b/tests/sync/test_network.py @@ -18,8 +18,9 @@ from tests.server import Server -def test_response_server_addr(page: Page, server: Server): +def test_response_server_addr(page: Page, server: Server) -> None: response = page.goto(server.EMPTY_PAGE) + assert response server_addr = response.server_addr() assert server_addr assert server_addr["port"] == server.PORT @@ -27,12 +28,17 @@ def test_response_server_addr(page: Page, server: Server): def test_response_security_details( - browser: Browser, https_server: Server, browser_name, is_win, is_linux -): + browser: Browser, + https_server: Server, + browser_name: str, + is_win: bool, + is_linux: bool, +) -> None: if browser_name == "webkit" and is_linux: pytest.skip("https://github.com/microsoft/playwright/issues/6759") page = browser.new_page(ignore_https_errors=True) response = page.goto(https_server.EMPTY_PAGE) + assert response response.finished() security_details = response.security_details() assert security_details @@ -60,7 +66,10 @@ def test_response_security_details( page.close() -def test_response_security_details_none_without_https(page: Page, server: Server): +def test_response_security_details_none_without_https( + page: Page, server: Server +) -> None: response = page.goto(server.EMPTY_PAGE) + assert response security_details = response.security_details() assert security_details is None diff --git a/tests/sync/test_page.py b/tests/sync/test_page.py index 9e59256c2..372aecda6 100644 --- a/tests/sync/test_page.py +++ b/tests/sync/test_page.py @@ -16,7 +16,7 @@ from tests.server import Server -def test_input_value(page: Page, server: Server): +def test_input_value(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/textarea.html") page.fill("input", "my-text-content") @@ -26,7 +26,7 @@ def test_input_value(page: Page, server: Server): assert page.input_value("input") == "" -def test_drag_and_drop_helper_method(page: Page, server: Server): +def test_drag_and_drop_helper_method(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/drag-n-drop.html") page.drag_and_drop("#source", "#target") assert ( @@ -37,7 +37,7 @@ def test_drag_and_drop_helper_method(page: Page, server: Server): ) -def test_should_check_box_using_set_checked(page: Page): +def test_should_check_box_using_set_checked(page: Page) -> None: page.set_content("``") page.set_checked("input", True) assert page.evaluate("checkbox.checked") is True @@ -45,7 +45,7 @@ def test_should_check_box_using_set_checked(page: Page): assert page.evaluate("checkbox.checked") is False -def test_should_set_bodysize_and_headersize(page: Page, server: Server): +def test_should_set_bodysize_and_headersize(page: Page, server: Server) -> None: page.goto(server.EMPTY_PAGE) with page.expect_event("request") as req_info: page.evaluate( @@ -57,7 +57,7 @@ def test_should_set_bodysize_and_headersize(page: Page, server: Server): assert req.sizes["requestHeadersSize"] >= 300 -def test_should_set_bodysize_to_0(page: Page, server: Server): +def test_should_set_bodysize_to_0(page: Page, server: Server) -> None: page.goto(server.EMPTY_PAGE) with page.expect_event("request") as req_info: page.evaluate("() => fetch('./get').then(r => r.text())") diff --git a/tests/sync/test_pdf.py b/tests/sync/test_pdf.py index b93de201d..af9217ea7 100644 --- a/tests/sync/test_pdf.py +++ b/tests/sync/test_pdf.py @@ -21,13 +21,13 @@ @pytest.mark.only_browser("chromium") -def test_should_be_able_to_save_pdf_file(page: Page, server, tmpdir: Path): +def test_should_be_able_to_save_pdf_file(page: Page, tmpdir: Path) -> None: output_file = tmpdir / "foo.png" page.pdf(path=str(output_file)) assert os.path.getsize(output_file) > 0 @pytest.mark.only_browser("chromium") -def test_should_be_able_capture_pdf_without_path(page: Page): +def test_should_be_able_capture_pdf_without_path(page: Page) -> None: buffer = page.pdf() assert buffer diff --git a/tests/sync/test_resource_timing.py b/tests/sync/test_resource_timing.py index 00d8de4ab..a68143a6e 100644 --- a/tests/sync/test_resource_timing.py +++ b/tests/sync/test_resource_timing.py @@ -15,8 +15,11 @@ import pytest from flaky import flaky +from playwright.sync_api import Browser, Page +from tests.server import Server -def test_should_work(page, server, is_webkit, is_mac): + +def test_should_work(page: Page, server: Server, is_webkit: bool, is_mac: bool) -> None: if is_webkit and is_mac: pytest.skip() with page.expect_event("requestfinished") as request_info: @@ -35,7 +38,9 @@ def test_should_work(page, server, is_webkit, is_mac): @flaky -def test_should_work_for_subresource(page, server, is_win, is_mac, is_webkit): +def test_should_work_for_subresource( + page: Page, server: Server, is_win: bool, is_mac: bool, is_webkit: bool +) -> None: if is_webkit and is_mac: pytest.skip() requests = [] @@ -63,7 +68,9 @@ def test_should_work_for_subresource(page, server, is_win, is_mac, is_webkit): assert timing["responseEnd"] < 10000 -def test_should_work_for_ssl(browser, https_server, is_mac, is_webkit): +def test_should_work_for_ssl( + browser: Browser, https_server: Server, is_mac: bool, is_webkit: bool +) -> None: if is_webkit and is_mac: pytest.skip() page = browser.new_page(ignore_https_errors=True) @@ -86,7 +93,7 @@ def test_should_work_for_ssl(browser, https_server, is_mac, is_webkit): @pytest.mark.skip_browser("webkit") # In WebKit, redirects don"t carry the timing info -def test_should_work_for_redirect(page, server): +def test_should_work_for_redirect(page: Page, server: Server) -> None: server.set_redirect("/foo.html", "/empty.html") responses = [] page.on("response", lambda response: responses.append(response)) diff --git a/tests/sync/test_sync.py b/tests/sync/test_sync.py index 175a34b65..ef2304507 100644 --- a/tests/sync/test_sync.py +++ b/tests/sync/test_sync.py @@ -25,26 +25,28 @@ TimeoutError, sync_playwright, ) +from tests.server import Server -def test_sync_query_selector(page): +def test_sync_query_selector(page: Page) -> None: page.set_content( """

Bar

""" ) - assert ( - page.query_selector("#foo").inner_text() - == page.query_selector("h1").inner_text() - ) + e1 = page.query_selector("#foo") + assert e1 + e2 = page.query_selector("h1") + assert e2 + assert e1.inner_text() == e2.inner_text() -def test_page_repr(page): +def test_page_repr(page: Page) -> None: page.goto("https://example.com") assert repr(page) == f"" -def test_frame_repr(page: Page): +def test_frame_repr(page: Page) -> None: page.goto("https://example.com") assert ( repr(page.main_frame) @@ -52,18 +54,18 @@ def test_frame_repr(page: Page): ) -def test_browser_context_repr(context: BrowserContext): +def test_browser_context_repr(context: BrowserContext) -> None: assert repr(context) == f"" -def test_browser_repr(browser: Browser): +def test_browser_repr(browser: Browser) -> None: assert ( repr(browser) == f"" ) -def test_browser_type_repr(browser: Browser): +def test_browser_type_repr(browser: Browser) -> None: browser_type = browser._impl_obj._browser_type assert ( repr(browser_type) @@ -71,8 +73,8 @@ def test_browser_type_repr(browser: Browser): ) -def test_dialog_repr(page: Page, server): - def on_dialog(dialog: Dialog): +def test_dialog_repr(page: Page) -> None: + def on_dialog(dialog: Dialog) -> None: dialog.accept() assert ( repr(dialog) @@ -83,7 +85,7 @@ def on_dialog(dialog: Dialog): page.evaluate("alert('yo')") -def test_console_repr(page: Page, server): +def test_console_repr(page: Page) -> None: messages = [] page.on("console", lambda m: messages.append(m)) page.evaluate('() => console.log("Hello world")') @@ -91,7 +93,7 @@ def test_console_repr(page: Page, server): assert repr(message) == f"" -def test_sync_click(page): +def test_sync_click(page: Page) -> None: page.set_content( """ @@ -101,7 +103,7 @@ def test_sync_click(page): assert page.evaluate("()=>window.clicked") -def test_sync_nested_query_selector(page): +def test_sync_nested_query_selector(page: Page) -> None: page.set_content( """
@@ -114,12 +116,15 @@ def test_sync_nested_query_selector(page): """ ) e1 = page.query_selector("#one") + assert e1 e2 = e1.query_selector(".two") + assert e2 e3 = e2.query_selector("label") + assert e3 assert e3.inner_text() == "MyValue" -def test_sync_handle_multiple_pages(context): +def test_sync_handle_multiple_pages(context: BrowserContext) -> None: page1 = context.new_page() page2 = context.new_page() assert len(context.pages) == 2 @@ -136,27 +141,27 @@ def test_sync_handle_multiple_pages(context): page.content() -def test_sync_wait_for_event(page: Page, server): +def test_sync_wait_for_event(page: Page, server: Server) -> None: with page.expect_event("popup", timeout=10000) as popup: page.evaluate("(url) => window.open(url)", server.EMPTY_PAGE) assert popup.value -def test_sync_wait_for_event_raise(page): +def test_sync_wait_for_event_raise(page: Page) -> None: with pytest.raises(Error): with page.expect_event("popup", timeout=500) as popup: assert False assert popup.value is None -def test_sync_make_existing_page_sync(page): +def test_sync_make_existing_page_sync(page: Page) -> None: page = page assert page.evaluate("() => ({'playwright': true})") == {"playwright": True} page.set_content("

myElement

") page.wait_for_selector("text=myElement") -def test_sync_network_events(page, server): +def test_sync_network_events(page: Page, server: Server) -> None: server.set_route( "/hello-world", lambda request: ( @@ -182,7 +187,7 @@ def test_sync_network_events(page, server): ] -def test_console_should_work(page, browser_name): +def test_console_should_work(page: Page, browser_name: str) -> None: messages = [] page.once("console", lambda m: messages.append(m)) page.evaluate('() => console.log("hello", 5, {foo: "bar"})'), @@ -200,7 +205,7 @@ def test_console_should_work(page, browser_name): assert message.args[2].json_value() == {"foo": "bar"} -def test_sync_download(browser: Browser, server): +def test_sync_download(browser: Browser, server: Server) -> None: server.set_route( "/downloadWithFilename", lambda request: ( @@ -224,7 +229,7 @@ def test_sync_download(browser: Browser, server): page.close() -def test_sync_workers_page_workers(page: Page, server): +def test_sync_workers_page_workers(page: Page, server: Server) -> None: with page.expect_event("worker") as event_worker: page.goto(server.PREFIX + "/worker/worker.html") assert event_worker.value @@ -237,7 +242,7 @@ def test_sync_workers_page_workers(page: Page, server): assert len(page.workers) == 0 -def test_sync_playwright_multiple_times(): +def test_sync_playwright_multiple_times() -> None: with pytest.raises(Error) as exc: with sync_playwright() as pw: assert pw.chromium @@ -247,14 +252,14 @@ def test_sync_playwright_multiple_times(): ) -def test_sync_set_default_timeout(page): +def test_sync_set_default_timeout(page: Page) -> None: page.set_default_timeout(1) with pytest.raises(TimeoutError) as exc: page.wait_for_function("false") assert "Timeout 1ms exceeded." in exc.value.message -def test_close_should_reject_all_promises(context): +def test_close_should_reject_all_promises(context: BrowserContext) -> None: new_page = context.new_page() with pytest.raises(Error) as exc_info: new_page._gather( @@ -264,7 +269,7 @@ def test_close_should_reject_all_promises(context): assert "Target closed" in exc_info.value.message -def test_expect_response_should_work(page: Page, server): +def test_expect_response_should_work(page: Page, server: Server) -> None: with page.expect_response("**/*") as resp: page.goto(server.EMPTY_PAGE) assert resp.value diff --git a/tests/sync/test_tap.py b/tests/sync/test_tap.py index 560d3be96..762698b04 100644 --- a/tests/sync/test_tap.py +++ b/tests/sync/test_tap.py @@ -12,19 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Generator, Optional + import pytest -from playwright.sync_api import ElementHandle, JSHandle +from playwright.sync_api import Browser, BrowserContext, ElementHandle, JSHandle, Page @pytest.fixture -def context(browser): +def context(browser: Browser) -> Generator[BrowserContext, None, None]: context = browser.new_context(has_touch=True) yield context context.close() -def test_should_send_all_of_the_correct_events(page): +def test_should_send_all_of_the_correct_events(page: Page) -> None: page.set_content( """
a
@@ -52,7 +54,7 @@ def test_should_send_all_of_the_correct_events(page): ] -def test_should_not_send_mouse_events_touchstart_is_canceled(page): +def test_should_not_send_mouse_events_touchstart_is_canceled(page: Page) -> None: page.set_content("hello world") page.evaluate( """() => { @@ -74,7 +76,7 @@ def test_should_not_send_mouse_events_touchstart_is_canceled(page): ] -def test_should_not_send_mouse_events_touchend_is_canceled(page): +def test_should_not_send_mouse_events_touchend_is_canceled(page: Page) -> None: page.set_content("hello world") page.evaluate( """() => { @@ -96,7 +98,8 @@ def test_should_not_send_mouse_events_touchend_is_canceled(page): ] -def track_events(target: ElementHandle) -> JSHandle: +def track_events(target: Optional[ElementHandle]) -> JSHandle: + assert target return target.evaluate_handle( """target => { const events = []; diff --git a/tests/sync/test_tracing.py b/tests/sync/test_tracing.py index 5adf6e679..2cce1548d 100644 --- a/tests/sync/test_tracing.py +++ b/tests/sync/test_tracing.py @@ -14,8 +14,13 @@ from pathlib import Path +from playwright.sync_api import Browser +from tests.server import Server -def test_browser_context_output_trace(browser, server, tmp_path): + +def test_browser_context_output_trace( + browser: Browser, server: Server, tmp_path: Path +) -> None: context = browser.new_context() context.tracing.start(screenshots=True, snapshots=True) page = context.new_page() diff --git a/tests/sync/test_video.py b/tests/sync/test_video.py index f354e92f5..1a2d3ba41 100644 --- a/tests/sync/test_video.py +++ b/tests/sync/test_video.py @@ -13,34 +13,47 @@ # limitations under the License. import os +from pathlib import Path +from typing import Dict +from playwright.sync_api import Browser, BrowserType +from tests.server import Server -def test_should_expose_video_path(browser, tmpdir, server): + +def test_should_expose_video_path( + browser: Browser, tmpdir: Path, server: Server +) -> None: page = browser.new_page( record_video_dir=tmpdir, record_video_size={"width": 100, "height": 200} ) page.goto(server.PREFIX + "/grid.html") - path = page.video.path() + video = page.video + assert video + path = video.path() assert repr(page.video) == f"