From 5580420d83609718ab8b48e21e3c754af7d9e106 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 7 Aug 2020 02:11:40 +0200 Subject: [PATCH 1/9] tests: added defaultbrowsercontext tests (#157) --- playwright/browser_type.py | 3 + tests/async/test_defaultbrowsercontext.py | 376 ++++++++++++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 tests/async/test_defaultbrowsercontext.py diff --git a/playwright/browser_type.py b/playwright/browser_type.py index 3373fc85d..d2a53d8b5 100644 --- a/playwright/browser_type.py +++ b/playwright/browser_type.py @@ -29,6 +29,7 @@ locals_to_params, not_installed_error, ) +from playwright.network import serialize_headers class BrowserType(ChannelOwner): @@ -134,6 +135,8 @@ async def launchPersistentContext( ) -> BrowserContext: userDataDir = str(Path(userDataDir)) params = locals_to_params(locals()) + if extraHTTPHeaders: + params["extraHTTPHeaders"] = serialize_headers(extraHTTPHeaders) normalize_launch_params(params) try: return from_channel( diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py new file mode 100644 index 000000000..4ff816e6e --- /dev/null +++ b/tests/async/test_defaultbrowsercontext.py @@ -0,0 +1,376 @@ +# 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 asyncio +import os + +import pytest + +from playwright.helper import Error + + +@pytest.fixture() +async def launch_persistent(tmpdir, launch_arguments, browser_type): + context = None + + async def _launch(**options): + nonlocal context + if context: + raise ValueError("can only launch one persitent context") + context = await browser_type.launchPersistentContext( + str(tmpdir), **{**launch_arguments, **options} + ) + return (context.pages[0], context) + + yield _launch + await context.close() + + +async def test_context_cookies_should_work(server, launch_persistent): + (page, context) = await launch_persistent() + await page.goto(server.EMPTY_PAGE) + document_cookie = await page.evaluate( + """() => { + document.cookie = 'username=John Doe'; + return document.cookie; + }""" + ) + + assert document_cookie == "username=John Doe" + assert await page.context.cookies() == [ + { + "name": "username", + "value": "John Doe", + "domain": "localhost", + "path": "/", + "expires": -1, + "httpOnly": False, + "secure": False, + "sameSite": "None", + } + ] + + +async def test_context_add_cookies_should_work(server, launch_persistent): + (page, context) = await launch_persistent() + await page.goto(server.EMPTY_PAGE) + await page.context.addCookies( + [{"url": server.EMPTY_PAGE, "name": "username", "value": "John Doe"}] + ) + assert await page.evaluate("() => document.cookie") == "username=John Doe" + assert await page.context.cookies() == [ + { + "name": "username", + "value": "John Doe", + "domain": "localhost", + "path": "/", + "expires": -1, + "httpOnly": False, + "secure": False, + "sameSite": "None", + } + ] + + +async def test_context_clear_cookies_should_work(server, launch_persistent): + (page, context) = await launch_persistent() + await page.goto(server.EMPTY_PAGE) + await page.context.addCookies( + [ + {"url": server.EMPTY_PAGE, "name": "cookie1", "value": "1"}, + {"url": server.EMPTY_PAGE, "name": "cookie2", "value": "2"}, + ] + ) + assert await page.evaluate("document.cookie") == "cookie1=1; cookie2=2" + await page.context.clearCookies() + await page.reload() + assert await page.context.cookies([]) == [] + assert await page.evaluate("document.cookie") == "" + + +async def test_should_not_block_third_party_cookies( + server, launch_persistent, is_chromium, is_firefox +): + (page, context) = await launch_persistent() + await page.goto(server.EMPTY_PAGE) + await page.evaluate( + """src => { + let fulfill; + const promise = new Promise(x => fulfill = x); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.onload = fulfill; + iframe.src = src; + return promise; + }""", + server.CROSS_PROCESS_PREFIX + "/grid.html", + ) + document_cookie = await page.frames[1].evaluate( + """() => { + document.cookie = 'username=John Doe'; + return document.cookie; + }""" + ) + + await page.waitForTimeout(2000) + allows_third_party = is_chromium or is_firefox + assert document_cookie == ("username=John Doe" if allows_third_party else "") + cookies = await context.cookies(server.CROSS_PROCESS_PREFIX + "/grid.html") + if allows_third_party: + assert cookies == [ + { + "domain": "127.0.0.1", + "expires": -1, + "httpOnly": False, + "name": "username", + "path": "/", + "sameSite": "None", + "secure": False, + "value": "John Doe", + } + ] + else: + assert cookies == [] + + +async def test_should_support_viewport_option(launch_persistent, utils): + (page, context) = await launch_persistent(viewport={"width": 456, "height": 789}) + await utils.verify_viewport(page, 456, 789) + page2 = await context.newPage() + await utils.verify_viewport(page2, 456, 789) + + +async def test_should_support_device_scale_factor_option(launch_persistent, utils): + (page, context) = await launch_persistent(deviceScaleFactor=3) + assert await page.evaluate("window.devicePixelRatio") == 3 + + +async def test_should_support_user_agent_option(launch_persistent, server): + (page, context) = await launch_persistent(userAgent="foobar") + assert await page.evaluate("() => navigator.userAgent") == "foobar" + [request, _] = await asyncio.gather( + server.wait_for_request("/empty.html"), page.goto(server.EMPTY_PAGE), + ) + assert request.getHeader("user-agent") == "foobar" + + +async def test_should_support_bypass_csp_option(launch_persistent, server): + (page, context) = await launch_persistent(bypassCSP=True) + await page.goto(server.PREFIX + "/csp.html") + await page.addScriptTag(content="window.__injected = 42;") + assert await page.evaluate("() => window.__injected") == 42 + + +async def test_should_support_javascript_enabled_option(launch_persistent, is_webkit): + (page, context) = await launch_persistent(javaScriptEnabled=False) + await page.goto('data:text/html, ') + with pytest.raises(Error) as exc: + await page.evaluate("something") + if is_webkit: + assert "Can't find variable: something" in exc.value.message + else: + assert "something is not defined" in exc.value.message + + +async def test_should_support_http_credentials_option(server, launch_persistent): + (page, context) = await launch_persistent( + httpCredentials={"username": "user", "password": "pass"} + ) + server.set_auth("/playground.html", b"user", b"pass") + response = await page.goto(server.PREFIX + "/playground.html") + assert response.status == 200 + + +async def test_should_support_offline_option(server, launch_persistent): + (page, context) = await launch_persistent(offline=True) + with pytest.raises(Error): + await page.goto(server.EMPTY_PAGE) + + +async def test_should_support_has_touch_option(server, launch_persistent): + (page, context) = await launch_persistent(hasTouch=True) + await page.goto(server.PREFIX + "/mobile.html") + assert await page.evaluate('() => "ontouchstart" in window') + + +@pytest.mark.skip_browser("firefox") +async def test_should_work_in_persistent_context(server, launch_persistent): + # Firefox does not support mobile. + (page, context) = await launch_persistent( + viewport={"width": 320, "height": 480}, isMobile=True + ) + await page.goto(server.PREFIX + "/empty.html") + assert await page.evaluate("() => window.innerWidth") == 980 + + +async def test_should_support_color_scheme_option(server, launch_persistent): + (page, context) = await launch_persistent(colorScheme="dark") + assert ( + await page.evaluate('() => matchMedia("(prefers-color-scheme: light)").matches') + is False + ) + assert await page.evaluate( + '() => matchMedia("(prefers-color-scheme: dark)").matches' + ) + + +async def test_should_support_timezone_id_option(launch_persistent): + (page, context) = await launch_persistent(timezoneId="America/Jamaica") + assert ( + await page.evaluate("() => new Date(1479579154987).toString()") + == "Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)" + ) + + +async def test_should_support_locale_option(launch_persistent): + (page, context) = await launch_persistent(locale="fr-CH") + assert await page.evaluate("() => navigator.language") == "fr-CH" + + +async def test_should_support_geolocation_and_permission_option( + server, launch_persistent +): + (page, context) = await launch_persistent( + geolocation={"longitude": 10, "latitude": 10}, permissions=["geolocation"] + ) + await page.goto(server.EMPTY_PAGE) + geolocation = await page.evaluate( + """() => new Promise(resolve => navigator.geolocation.getCurrentPosition(position => { + resolve({latitude: position.coords.latitude, longitude: position.coords.longitude}); + }))""" + ) + assert geolocation == {"latitude": 10, "longitude": 10} + + +async def test_should_support_ignore_https_errors_option( + https_server, launch_persistent +): + (page, context) = await launch_persistent(ignoreHTTPSErrors=True) + response = await page.goto(https_server.EMPTY_PAGE) + assert response.ok + + +async def test_should_support_extra_http_headers_option(server, launch_persistent): + (page, context) = await launch_persistent(extraHTTPHeaders={"foo": "bar"}) + [request, _] = await asyncio.gather( + server.wait_for_request("/empty.html"), page.goto(server.EMPTY_PAGE), + ) + assert request.getHeader("foo") == "bar" + + +async def test_should_accept_user_data_dir(server, tmpdir, launch_persistent): + (page, context) = await launch_persistent() + # Note: we need an open page to make sure its functional. + assert len(os.listdir(tmpdir)) > 0 + await context.close() + assert len(os.listdir(tmpdir)) > 0 + + +async def test_should_restore_state_from_userDataDir( + browser_type, launch_arguments, server, tmp_path_factory +): + user_data_dir1 = tmp_path_factory.mktemp("test") + browser_context = await browser_type.launchPersistentContext( + user_data_dir1, **launch_arguments + ) + page = await browser_context.newPage() + await page.goto(server.EMPTY_PAGE) + await page.evaluate('() => localStorage.hey = "hello"') + await browser_context.close() + + browser_context2 = await browser_type.launchPersistentContext( + user_data_dir1, **launch_arguments + ) + page2 = await browser_context2.newPage() + await page2.goto(server.EMPTY_PAGE) + assert await page2.evaluate("() => localStorage.hey") == "hello" + await browser_context2.close() + + user_data_dir2 = tmp_path_factory.mktemp("test") + browser_context3 = await browser_type.launchPersistentContext( + user_data_dir2, **launch_arguments + ) + page3 = await browser_context3.newPage() + await page3.goto(server.EMPTY_PAGE) + assert await page3.evaluate("() => localStorage.hey") != "hello" + await browser_context3.close() + + +async def test_should_restore_cookies_from_userDataDir( + browser_type, + launch_arguments, + tmp_path_factory, + server, + is_chromium, + is_win, + is_mac, +): + if is_chromium and (is_win or is_mac): + pytest.skip() + userDataDir = tmp_path_factory.mktemp("1") + browser_context = await browser_type.launchPersistentContext( + userDataDir, **launch_arguments + ) + page = await browser_context.newPage() + await page.goto(server.EMPTY_PAGE) + document_cookie = await page.evaluate( + """() => { + document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'; + return document.cookie; + }""" + ) + + assert document_cookie == "doSomethingOnlyOnce=true" + await browser_context.close() + + browser_context2 = await browser_type.launchPersistentContext( + userDataDir, **launch_arguments + ) + page2 = await browser_context2.newPage() + await page2.goto(server.EMPTY_PAGE) + assert await page2.evaluate("() => document.cookie") == "doSomethingOnlyOnce=true" + await browser_context2.close() + + userDataDir2 = tmp_path_factory.mktemp("2") + browser_context3 = await browser_type.launchPersistentContext( + userDataDir2, **launch_arguments + ) + page3 = await browser_context3.newPage() + await page3.goto(server.EMPTY_PAGE) + assert await page3.evaluate("() => document.cookie") != "doSomethingOnlyOnce=true" + await browser_context3.close() + + +async def test_should_have_default_url_when_launching_browser(launch_persistent): + (page, context) = await launch_persistent() + urls = list(map(lambda p: p.url, context.pages)) + assert urls == ["about:blank"] + + +@pytest.mark.skip_browser("firefox") +async def test_should_throw_if_page_argument_is_passed( + browser_type, server, tmpdir, launch_arguments +): + options = {**launch_arguments, "args": [server.EMPTY_PAGE]} + with pytest.raises(Error) as exc: + await browser_type.launchPersistentContext(tmpdir, **options) + assert "can not specify page" in exc.value.message + + +async def test_should_fire_close_event_for_a_persistent_context(launch_persistent): + (page, context) = await launch_persistent() + fired_event = asyncio.Future() + context.on("close", lambda: fired_event.set_result(True)) + await context.close() + await fired_event From 05e48f6a23f988bfbf68431ebee8eabe3e8d7bd7 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 7 Aug 2020 10:11:18 +0200 Subject: [PATCH 2/9] fix: stderr redirect with monkeypatched stderr (#159) --- playwright/main.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/playwright/main.py b/playwright/main.py index 4726ef4bf..049535ec6 100644 --- a/playwright/main.py +++ b/playwright/main.py @@ -13,6 +13,7 @@ # limitations under the License. import asyncio +import io import subprocess import sys from typing import Any @@ -45,11 +46,21 @@ async def run_driver_async() -> Connection: driver_name = compute_driver_name() driver_executable = package_path / "drivers" / driver_name + # Sourced from: https://github.com/pytest-dev/pytest/blob/49827adcb9256c9c9c06a25729421dcc3c385edc/src/_pytest/faulthandler.py#L73-L80 + def _get_stderr_fileno() -> int: + try: + return sys.stderr.fileno() + except io.UnsupportedOperation: + # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. + # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors + # This is potentially dangerous, but the best we can do. + return sys.__stderr__.fileno() + proc = await asyncio.create_subprocess_exec( str(driver_executable), stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, - stderr=sys.stderr, + stderr=_get_stderr_fileno(), limit=32768, ) assert proc.stdout From 759eec817fcd54435869d29c9fc665b20d1b2abe Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Sat, 8 Aug 2020 17:41:42 +0200 Subject: [PATCH 3/9] core: drop coveralls tool (#162) --- .github/workflows/ci.yml | 9 ++------- README.md | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49e60e558..2c79d5810 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install coveralls pip install -r local-requirements.txt pip install -e . - name: Build driver @@ -77,14 +76,10 @@ jobs: run: python -m playwright install - name: Test if: matrix.os != 'ubuntu-latest' - run: pytest -vv --browser=${{ matrix.browser }} --junitxml=junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.browser }}.xml --cov=playwright --cov=scripts --cov-report xml --timeout 90 + run: pytest -vv --browser=${{ matrix.browser }} --junitxml=junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.browser }}.xml --timeout 90 - name: Test if: matrix.os == 'ubuntu-latest' - run: xvfb-run pytest -vv --browser=${{ matrix.browser }} --junitxml=junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.browser }}.xml --cov=playwright --cov=scripts --cov-report xml --timeout 90 - - name: Coveralls - run: coveralls - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: xvfb-run pytest -vv --browser=${{ matrix.browser }} --junitxml=junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.browser }}.xml --timeout 90 - name: Upload pytest test results uses: actions/upload-artifact@v1 with: diff --git a/README.md b/README.md index c209ca198..2a334b422 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # 🎭 [Playwright](https://github.com/microsoft/playwright) for Python [![PyPI version](https://badge.fury.io/py/playwright.svg)](https://pypi.python.org/pypi/playwright/) [![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://join.slack.com/t/playwright/shared_invite/enQtOTEyMTUxMzgxMjIwLThjMDUxZmIyNTRiMTJjNjIyMzdmZDA3MTQxZWUwZTFjZjQwNGYxZGM5MzRmNzZlMWI5ZWUyOTkzMjE5Njg1NDg) [![Chromium version](https://img.shields.io/badge/chromium-86.0.4217.0-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-78.0b5-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-14.0-blue.svg?logo=safari)](https://webkit.org/) -[![Coverage Status](https://coveralls.io/repos/github/microsoft/playwright-python/badge.svg?branch=master)](https://coveralls.io/github/microsoft/playwright-python?branch=master) ##### [Docs](#documentation) | [API reference](https://github.com/microsoft/playwright/blob/master/docs/api.md) From 9364c37273a1943d16a454170b740d161d30efaa Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 10 Aug 2020 17:44:18 +0200 Subject: [PATCH 4/9] fix(error): display correct message (#165) --- playwright/helper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/playwright/helper.py b/playwright/helper.py index 32282bcf9..522678a81 100644 --- a/playwright/helper.py +++ b/playwright/helper.py @@ -232,6 +232,7 @@ class Error(Exception): def __init__(self, message: str, stack: str = None) -> None: self.message = message self.stack = stack + super().__init__(message) class TimeoutError(Error): From af97862582125bbcf6d476479342907e33356da6 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 10 Aug 2020 10:50:42 -0700 Subject: [PATCH 5/9] feat: roll driver to 1.3.0-next.1596843106133, handle paths on the client side (#163) Co-authored-by: Max Schmitt --- README.md | 4 +- api.json | 80 +++++++++---------- driver/package.json | 2 +- playwright/async_api.py | 32 ++++---- playwright/drivers/browsers.json | 2 +- playwright/page.py | 14 +++- playwright/sync_api.py | 32 ++++---- tests/async/test_browser.py | 2 +- .../async/test_browsercontext_add_cookies.py | 6 +- tests/async/test_browsercontext_cookies.py | 14 ++-- tests/async/test_defaultbrowsercontext.py | 8 +- tests/async/test_headful.py | 2 +- tests/async/test_navigation.py | 2 + tests/async/test_pdf.py | 6 ++ tests/sync/test_pdf.py | 6 ++ 15 files changed, 116 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 2a334b422..e4c1624da 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 [Playwright](https://github.com/microsoft/playwright) for Python -[![PyPI version](https://badge.fury.io/py/playwright.svg)](https://pypi.python.org/pypi/playwright/) [![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://join.slack.com/t/playwright/shared_invite/enQtOTEyMTUxMzgxMjIwLThjMDUxZmIyNTRiMTJjNjIyMzdmZDA3MTQxZWUwZTFjZjQwNGYxZGM5MzRmNzZlMWI5ZWUyOTkzMjE5Njg1NDg) [![Chromium version](https://img.shields.io/badge/chromium-86.0.4217.0-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-78.0b5-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-14.0-blue.svg?logo=safari)](https://webkit.org/) +[![PyPI version](https://badge.fury.io/py/playwright.svg)](https://pypi.python.org/pypi/playwright/) [![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://join.slack.com/t/playwright/shared_invite/enQtOTEyMTUxMzgxMjIwLThjMDUxZmIyNTRiMTJjNjIyMzdmZDA3MTQxZWUwZTFjZjQwNGYxZGM5MzRmNzZlMWI5ZWUyOTkzMjE5Njg1NDg) [![Chromium version](https://img.shields.io/badge/chromium-86.0.4217.0-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-79.0a1-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-14.0-blue.svg?logo=safari)](https://webkit.org/) ##### [Docs](#documentation) | [API reference](https://github.com/microsoft/playwright/blob/master/docs/api.md) @@ -10,7 +10,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | :--- | :---: | :---: | :---: | | Chromium 86.0.4217.0 | ✅ | ✅ | ✅ | | WebKit 14.0 | ✅ | ✅ | ✅ | -| Firefox 78.0b5 | ✅ | ✅ | ✅ | +| Firefox 79.0a1 | ✅ | ✅ | ✅ | Headless execution is supported for all the browsers on all platforms. diff --git a/api.json b/api.json index 807be6d17..b0bf75cd2 100644 --- a/api.json +++ b/api.json @@ -1954,7 +1954,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -2004,7 +2004,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -2248,7 +2248,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method fetches an element with `selector`, if element is not already checked, it scrolls it into view if needed, and then uses page.click to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nShortcut for page.mainFrame().check(selector[, options]).", + "comment": "This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already checked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nShortcut for page.mainFrame().check(selector[, options]).", "returnComment": "Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`.", "required": true, "templates": [], @@ -2323,7 +2323,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nShortcut for page.mainFrame().click(selector[, options]).", + "comment": "This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nShortcut for page.mainFrame().click(selector[, options]).", "returnComment": "Promise which resolves when the element matching `selector` is successfully clicked. The Promise will be rejected if there is no element matching `selector`.", "required": true, "templates": [], @@ -2522,7 +2522,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to double click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nBear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception.\n\n**NOTE** `page.dblclick()` dispatches two `click` events and a single `dblclick` event.\n\nShortcut for page.mainFrame().dblclick(selector[, options]).", + "comment": "This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to double click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nBear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception.\n\n**NOTE** `page.dblclick()` dispatches two `click` events and a single `dblclick` event.\n\nShortcut for page.mainFrame().dblclick(selector[, options]).", "returnComment": "Promise which resolves when the element matching `selector` is successfully double clicked. The Promise will be rejected if there is no element matching `selector`.", "required": true, "templates": [], @@ -2817,7 +2817,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -2855,7 +2855,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -3378,7 +3378,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to hover over the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nShortcut for page.mainFrame().hover(selector[, options]).", + "comment": "This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to hover over the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nShortcut for page.mainFrame().hover(selector[, options]).", "returnComment": "Promise which resolves when the element matching `selector` is successfully hovered. Promise gets rejected if there's no element matching `selector`.", "required": true, "templates": [], @@ -4786,7 +4786,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses page.click to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nShortcut for page.mainFrame().uncheck(selector[, options]).", + "comment": "This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already unchecked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nShortcut for page.mainFrame().uncheck(selector[, options]).", "returnComment": "Promise which resolves when the element matching `selector` is successfully unchecked. The Promise will be rejected if there is no element matching `selector`.", "required": true, "templates": [], @@ -5013,7 +5013,7 @@ "name": "Promise", "properties": {} }, - "comment": "The `waitForFunction` can be used to observe viewport size change:\n```js\nconst { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.\n\n(async () => {\n const browser = await webkit.launch();\n const page = await browser.newPage();\n const watchDog = page.waitForFunction('window.innerWidth < 100');\n await page.setViewportSize({width: 50, height: 50});\n await watchDog;\n await browser.close();\n})();\n```\nTo pass an argument from Node.js to the predicate of `page.waitForFunction` function:\n```js\nconst selector = '.foo';\nawait page.waitForFunction(selector => !!document.querySelector(selector), selector);\n```\nShortcut for [page.mainFrame().waitForFunction(pageFunction, arg, options]])](#framewaitforfunctionpagefunction-arg-options).", + "comment": "The `waitForFunction` can be used to observe viewport size change:\n```js\nconst { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.\n\n(async () => {\n const browser = await webkit.launch();\n const page = await browser.newPage();\n const watchDog = page.waitForFunction('window.innerWidth < 100');\n await page.setViewportSize({width: 50, height: 50});\n await watchDog;\n await browser.close();\n})();\n```\nTo pass an argument from Node.js to the predicate of `page.waitForFunction` function:\n```js\nconst selector = '.foo';\nawait page.waitForFunction(selector => !!document.querySelector(selector), selector);\n```\nShortcut for page.mainFrame().waitForFunction(pageFunction[, arg, options]).", "returnComment": "Promise which resolves when the `pageFunction` returns a truthy value. It resolves to a JSHandle of the truthy value.", "required": true, "templates": [], @@ -5034,7 +5034,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -5496,7 +5496,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -5546,7 +5546,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -5701,7 +5701,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method fetches an element with `selector`, if element is not already checked, it scrolls it into view if needed, and then uses frame.click to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.", + "comment": "This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already checked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.", "returnComment": "Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`.", "required": true, "templates": [], @@ -5789,7 +5789,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.", + "comment": "This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.", "returnComment": "Promise which resolves when the element matching `selector` is successfully clicked. The Promise will be rejected if there is no element matching `selector`.", "required": true, "templates": [], @@ -5962,7 +5962,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to double click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nBear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception.\n\n**NOTE** `frame.dblclick()` dispatches two `click` events and a single `dblclick` event.", + "comment": "This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to double click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.\nBear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception.\n\n**NOTE** `frame.dblclick()` dispatches two `click` events and a single `dblclick` event.", "returnComment": "Promise which resolves when the element matching `selector` is successfully double clicked. The Promise will be rejected if there is no element matching `selector`.", "required": true, "templates": [], @@ -6206,7 +6206,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -6244,7 +6244,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -6538,7 +6538,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to hover over the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.", + "comment": "This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to hover over the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.", "returnComment": "Promise which resolves when the element matching `selector` is successfully hovered. Promise gets rejected if there's no element matching `selector`.", "required": true, "templates": [], @@ -7316,7 +7316,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses frame.click to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.", + "comment": "This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already unchecked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element.\nIf there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried.", "returnComment": "Promise which resolves when the element matching `selector` is successfully unchecked. The Promise will be rejected if there is no element matching `selector`.", "required": true, "templates": [], @@ -7425,7 +7425,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -7772,7 +7772,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -7822,7 +7822,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -7901,7 +7901,7 @@ "name": "Promise", "properties": {} }, - "comment": "If element is not already checked, it scrolls it into view if needed, and then uses elementHandle.click to click in the center of the element.", + "comment": "This method waits for actionability checks. Then, if the element is not already checked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element.", "returnComment": "Promise which resolves when the element is successfully checked. Promise gets rejected if the operation fails.", "required": true, "templates": [], @@ -7964,7 +7964,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method scrolls element into view if needed, and then uses page.mouse to click in the center of the element.\nIf the element is detached from DOM, the method throws an error.", + "comment": "This method waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to click in the center of the element.\nIf the element is detached from DOM, the method throws an error.", "returnComment": "Promise which resolves when the element is successfully clicked. Promise gets rejected if the element is detached from DOM.", "required": true, "templates": [], @@ -8125,7 +8125,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method scrolls element into view if needed, and then uses page.mouse to click in the center of the element.\nIf the element is detached from DOM, the method throws an error.\nBear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception.\n\n**NOTE** `elementHandle.dblclick()` dispatches two `click` events and a single `dblclick` event.", + "comment": "This method waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to double click in the center of the element.\nIf the element is detached from DOM, the method throws an error.\nBear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception.\n\n**NOTE** `elementHandle.dblclick()` dispatches two `click` events and a single `dblclick` event.", "returnComment": "Promise which resolves when the element is successfully double clicked. Promise gets rejected if the element is detached from DOM.", "required": true, "templates": [], @@ -8401,7 +8401,7 @@ "name": "Promise", "properties": {} }, - "comment": "This method scrolls element into view if needed, and then uses page.mouse to hover over the center of the element.\nIf the element is detached from DOM, the method throws an error.", + "comment": "This method waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to hover over the center of the element.\nIf the element is detached from DOM, the method throws an error.", "returnComment": "Promise which resolves when the element is successfully hovered.", "required": true, "templates": [], @@ -9081,7 +9081,7 @@ "name": "Promise", "properties": {} }, - "comment": "If element is not already unchecked, it scrolls it into view if needed, and then uses elementHandle.click to click in the center of the element.", + "comment": "This method waits for actionability checks. Then, if the element is not already unchecked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element.", "returnComment": "Promise which resolves when the element is successfully unchecked. Promise gets rejected if the operation fails.", "required": true, "templates": [], @@ -9191,7 +9191,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -9229,7 +9229,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -9350,7 +9350,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -9388,7 +9388,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -9481,9 +9481,9 @@ "name": "string", "properties": {} }, - "comment": "Optional URL of the resource if available.", + "comment": "URL of the resource if available, otherwise empty string.", "returnComment": "", - "required": false, + "required": true, "templates": [] }, "lineNumber": { @@ -9493,9 +9493,9 @@ "name": "number", "properties": {} }, - "comment": "Optional 0-based line number in the resource if available.", + "comment": "0-based line number in the resource.", "returnComment": "", - "required": false, + "required": true, "templates": [] }, "columnNumber": { @@ -9505,9 +9505,9 @@ "name": "number", "properties": {} }, - "comment": "Optional 0-based column number in the resource if available.", + "comment": "0-based column number in the resource.", "returnComment": "", - "required": false, + "required": true, "templates": [] } } @@ -11438,7 +11438,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", @@ -11476,7 +11476,7 @@ "kind": "property", "name": "arg", "type": { - "name": "Serializable|JSHandle", + "name": "EvaluationArgument", "properties": {} }, "comment": "Optional argument to pass to `pageFunction`", diff --git a/driver/package.json b/driver/package.json index af51d7670..810f564f2 100644 --- a/driver/package.json +++ b/driver/package.json @@ -13,7 +13,7 @@ }, "license": "Apache-2.0", "dependencies": { - "playwright": "1.3.0-next.1596659749397" + "playwright": "1.3.0-next.1596843106133" }, "devDependencies": { "pkg": "^4.4.9" diff --git a/playwright/async_api.py b/playwright/async_api.py index b4ec806f5..323de3c25 100644 --- a/playwright/async_api.py +++ b/playwright/async_api.py @@ -944,7 +944,7 @@ async def hover( ) -> NoneType: """ElementHandle.hover - This method scrolls element into view if needed, and then uses page.mouse to hover over the center of the element. + This method waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to hover over the center of the element. If the element is detached from DOM, the method throws an error. Parameters @@ -979,7 +979,7 @@ async def click( ) -> NoneType: """ElementHandle.click - This method scrolls element into view if needed, and then uses page.mouse to click in the center of the element. + This method waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to click in the center of the element. If the element is detached from DOM, the method throws an error. Parameters @@ -1028,7 +1028,7 @@ async def dblclick( ) -> NoneType: """ElementHandle.dblclick - This method scrolls element into view if needed, and then uses page.mouse to click in the center of the element. + This method waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to double click in the center of the element. If the element is detached from DOM, the method throws an error. Bear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception. @@ -1241,7 +1241,7 @@ async def check( ) -> NoneType: """ElementHandle.check - If element is not already checked, it scrolls it into view if needed, and then uses elementHandle.click to click in the center of the element. + This method waits for actionability checks. Then, if the element is not already checked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. Parameters ---------- @@ -1263,7 +1263,7 @@ async def uncheck( ) -> NoneType: """ElementHandle.uncheck - If element is not already unchecked, it scrolls it into view if needed, and then uses elementHandle.click to click in the center of the element. + This method waits for actionability checks. Then, if the element is not already unchecked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. Parameters ---------- @@ -2108,7 +2108,7 @@ async def click( ) -> NoneType: """Frame.click - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Parameters @@ -2160,7 +2160,7 @@ async def dblclick( ) -> NoneType: """Frame.dblclick - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to double click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to double click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Bear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception. @@ -2339,7 +2339,7 @@ async def hover( ) -> NoneType: """Frame.hover - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to hover over the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to hover over the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Parameters @@ -2529,7 +2529,7 @@ async def check( ) -> NoneType: """Frame.check - This method fetches an element with `selector`, if element is not already checked, it scrolls it into view if needed, and then uses frame.click to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already checked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Parameters @@ -2558,7 +2558,7 @@ async def uncheck( ) -> NoneType: """Frame.uncheck - This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses frame.click to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already unchecked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Parameters @@ -4068,7 +4068,7 @@ async def click( ) -> NoneType: """Page.click - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Shortcut for page.mainFrame().click(selector[, options]). @@ -4121,7 +4121,7 @@ async def dblclick( ) -> NoneType: """Page.dblclick - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to double click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to double click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Bear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception. @@ -4304,7 +4304,7 @@ async def hover( ) -> NoneType: """Page.hover - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to hover over the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to hover over the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Shortcut for page.mainFrame().hover(selector[, options]). @@ -4494,7 +4494,7 @@ async def check( ) -> NoneType: """Page.check - This method fetches an element with `selector`, if element is not already checked, it scrolls it into view if needed, and then uses page.click to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already checked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Shortcut for page.mainFrame().check(selector[, options]). @@ -4524,7 +4524,7 @@ async def uncheck( ) -> NoneType: """Page.uncheck - This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses page.click to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already unchecked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Shortcut for page.mainFrame().uncheck(selector[, options]). @@ -4574,7 +4574,7 @@ async def waitForFunction( The `waitForFunction` can be used to observe viewport size change: To pass an argument from Node.js to the predicate of `page.waitForFunction` function: - Shortcut for [page.mainFrame().waitForFunction(pageFunction, arg, options]])](#framewaitforfunctionpagefunction-arg-options). + Shortcut for page.mainFrame().waitForFunction(pageFunction[, arg, options]). Parameters ---------- diff --git a/playwright/drivers/browsers.json b/playwright/drivers/browsers.json index 839fd9f18..f6ac44fb9 100644 --- a/playwright/drivers/browsers.json +++ b/playwright/drivers/browsers.json @@ -8,7 +8,7 @@ }, { "name": "firefox", - "revision": "1144", + "revision": "1154", "download": true }, { diff --git a/playwright/page.py b/playwright/page.py index 7bf024cc2..45ae02bed 100644 --- a/playwright/page.py +++ b/playwright/page.py @@ -539,8 +539,15 @@ async def screenshot( fullPage: bool = None, clip: FloatRect = None, ) -> bytes: - binary = await self._channel.send("screenshot", locals_to_params(locals())) - return base64.b64decode(binary) + params = locals_to_params(locals()) + if "path" in params: + del params["path"] + encoded_binary = await self._channel.send("screenshot", params) + decoded_binary = base64.b64decode(encoded_binary) + if path: + with open(path, "wb") as fd: + fd.write(decoded_binary) + return decoded_binary async def title(self) -> str: return await self._main_frame.title() @@ -722,7 +729,8 @@ async def pdf( path: str = None, ) -> bytes: params = locals_to_params(locals()) - del params["path"] + if "path" in params: + del params["path"] encoded_binary = await self._channel.send("pdf", params) decoded_binary = base64.b64decode(encoded_binary) if path: diff --git a/playwright/sync_api.py b/playwright/sync_api.py index b8fc47675..ad9a2363a 100644 --- a/playwright/sync_api.py +++ b/playwright/sync_api.py @@ -968,7 +968,7 @@ def hover( ) -> NoneType: """ElementHandle.hover - This method scrolls element into view if needed, and then uses page.mouse to hover over the center of the element. + This method waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to hover over the center of the element. If the element is detached from DOM, the method throws an error. Parameters @@ -1005,7 +1005,7 @@ def click( ) -> NoneType: """ElementHandle.click - This method scrolls element into view if needed, and then uses page.mouse to click in the center of the element. + This method waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to click in the center of the element. If the element is detached from DOM, the method throws an error. Parameters @@ -1056,7 +1056,7 @@ def dblclick( ) -> NoneType: """ElementHandle.dblclick - This method scrolls element into view if needed, and then uses page.mouse to click in the center of the element. + This method waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to double click in the center of the element. If the element is detached from DOM, the method throws an error. Bear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception. @@ -1285,7 +1285,7 @@ def check( ) -> NoneType: """ElementHandle.check - If element is not already checked, it scrolls it into view if needed, and then uses elementHandle.click to click in the center of the element. + This method waits for actionability checks. Then, if the element is not already checked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. Parameters ---------- @@ -1309,7 +1309,7 @@ def uncheck( ) -> NoneType: """ElementHandle.uncheck - If element is not already unchecked, it scrolls it into view if needed, and then uses elementHandle.click to click in the center of the element. + This method waits for actionability checks. Then, if the element is not already unchecked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. Parameters ---------- @@ -2186,7 +2186,7 @@ def click( ) -> NoneType: """Frame.click - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Parameters @@ -2240,7 +2240,7 @@ def dblclick( ) -> NoneType: """Frame.dblclick - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to double click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to double click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Bear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception. @@ -2428,7 +2428,7 @@ def hover( ) -> NoneType: """Frame.hover - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to hover over the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to hover over the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Parameters @@ -2631,7 +2631,7 @@ def check( ) -> NoneType: """Frame.check - This method fetches an element with `selector`, if element is not already checked, it scrolls it into view if needed, and then uses frame.click to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already checked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Parameters @@ -2665,7 +2665,7 @@ def uncheck( ) -> NoneType: """Frame.uncheck - This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses frame.click to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already unchecked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Parameters @@ -4238,7 +4238,7 @@ def click( ) -> NoneType: """Page.click - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Shortcut for page.mainFrame().click(selector[, options]). @@ -4293,7 +4293,7 @@ def dblclick( ) -> NoneType: """Page.dblclick - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to double click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to double click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Bear in mind that if the first click of the `dblclick()` triggers a navigation event, there will be an exception. @@ -4485,7 +4485,7 @@ def hover( ) -> NoneType: """Page.hover - This method fetches an element with `selector`, scrolls it into view if needed, and then uses page.mouse to hover over the center of the element. + This method waits for an element matching `selector`, waits for actionability checks, then scrolls the element into view if needed and uses page.mouse to hover over the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Shortcut for page.mainFrame().hover(selector[, options]). @@ -4688,7 +4688,7 @@ def check( ) -> NoneType: """Page.check - This method fetches an element with `selector`, if element is not already checked, it scrolls it into view if needed, and then uses page.click to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already checked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Shortcut for page.mainFrame().check(selector[, options]). @@ -4723,7 +4723,7 @@ def uncheck( ) -> NoneType: """Page.uncheck - This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses page.click to click in the center of the element. + This method waits for an element matching `selector`, waits for actionability checks. Then, if the element is not already unchecked, this method scrolls the element into view and uses elementHandle.click to click in the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. Shortcut for page.mainFrame().uncheck(selector[, options]). @@ -4778,7 +4778,7 @@ def waitForFunction( The `waitForFunction` can be used to observe viewport size change: To pass an argument from Node.js to the predicate of `page.waitForFunction` function: - Shortcut for [page.mainFrame().waitForFunction(pageFunction, arg, options]])](#framewaitforfunctionpagefunction-arg-options). + Shortcut for page.mainFrame().waitForFunction(pageFunction[, arg, options]). Parameters ---------- diff --git a/tests/async/test_browser.py b/tests/async/test_browser.py index 167a09f20..9baa18087 100644 --- a/tests/async/test_browser.py +++ b/tests/async/test_browser.py @@ -47,4 +47,4 @@ async def test_version_should_work(browser: Browser, is_chromium): if is_chromium: assert re.match(r"^\d+\.\d+\.\d+\.\d+$", version) else: - assert re.match(r"^\d+\.\d+$", version) + assert re.match(r"^\d+\.\d+", version) diff --git a/tests/async/test_browsercontext_add_cookies.py b/tests/async/test_browsercontext_add_cookies.py index 19e026cfc..63d7dd855 100644 --- a/tests/async/test_browsercontext_add_cookies.py +++ b/tests/async/test_browsercontext_add_cookies.py @@ -349,9 +349,7 @@ async def test_should_set_cookies_for_a_frame(context, page, server): assert await page.frames[1].evaluate("document.cookie") == "frame-cookie=value" -async def test_should_not_block_third_party_cookies( - context, page, server, is_chromium, is_firefox -): +async def test_should_not_block_third_party_cookies(context, page, server, is_chromium): await page.goto(server.EMPTY_PAGE) await page.evaluate( """src => { @@ -367,7 +365,7 @@ async def test_should_not_block_third_party_cookies( ) await page.frames[1].evaluate("document.cookie = 'username=John Doe'") await page.waitForTimeout(2000) - allows_third_party = is_chromium or is_firefox + allows_third_party = is_chromium cookies = await context.cookies(server.CROSS_PROCESS_PREFIX + "/grid.html") if allows_third_party: diff --git a/tests/async/test_browsercontext_cookies.py b/tests/async/test_browsercontext_cookies.py index 4c27e87fc..d2e868208 100644 --- a/tests/async/test_browsercontext_cookies.py +++ b/tests/async/test_browsercontext_cookies.py @@ -21,7 +21,7 @@ async def test_should_return_no_cookies_in_pristine_browser_context(context): assert await context.cookies() == [] -async def test_should_get_a_cookie(context, page, server): +async def test_should_get_a_cookie(context, page, server, is_firefox): await page.goto(server.EMPTY_PAGE) document_cookie = await page.evaluate( """() => { @@ -39,12 +39,12 @@ async def test_should_get_a_cookie(context, page, server): "expires": -1, "httpOnly": False, "secure": False, - "sameSite": "None", + "sameSite": "Lax" if is_firefox else "None", } ] -async def test_should_get_a_non_session_cookie(context, page, server): +async def test_should_get_a_non_session_cookie(context, page, server, is_firefox): await page.goto(server.EMPTY_PAGE) # @see https://en.wikipedia.org/wiki/Year_2038_problem date = int(datetime.datetime(2038, 1, 1).timestamp() * 1000) @@ -66,7 +66,7 @@ async def test_should_get_a_non_session_cookie(context, page, server): "expires": date / 1000, "httpOnly": False, "secure": False, - "sameSite": "None", + "sameSite": "Lax" if is_firefox else "None", } ] @@ -121,7 +121,7 @@ async def test_should_properly_report_lax_samesite_cookie( assert cookies[0]["sameSite"] == "Lax" -async def test_should_get_multiple_cookies(context, page, server): +async def test_should_get_multiple_cookies(context, page, server, is_firefox): await page.goto(server.EMPTY_PAGE) document_cookie = await page.evaluate( """() => { @@ -142,7 +142,7 @@ async def test_should_get_multiple_cookies(context, page, server): "expires": -1, "httpOnly": False, "secure": False, - "sameSite": "None", + "sameSite": "Lax" if is_firefox else "None", }, { "name": "username", @@ -152,7 +152,7 @@ async def test_should_get_multiple_cookies(context, page, server): "expires": -1, "httpOnly": False, "secure": False, - "sameSite": "None", + "sameSite": "Lax" if is_firefox else "None", }, ] diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py index 4ff816e6e..99af57582 100644 --- a/tests/async/test_defaultbrowsercontext.py +++ b/tests/async/test_defaultbrowsercontext.py @@ -37,7 +37,7 @@ async def _launch(**options): await context.close() -async def test_context_cookies_should_work(server, launch_persistent): +async def test_context_cookies_should_work(server, launch_persistent, is_firefox): (page, context) = await launch_persistent() await page.goto(server.EMPTY_PAGE) document_cookie = await page.evaluate( @@ -57,7 +57,7 @@ async def test_context_cookies_should_work(server, launch_persistent): "expires": -1, "httpOnly": False, "secure": False, - "sameSite": "None", + "sameSite": "Lax" if is_firefox else "None", } ] @@ -100,7 +100,7 @@ async def test_context_clear_cookies_should_work(server, launch_persistent): async def test_should_not_block_third_party_cookies( - server, launch_persistent, is_chromium, is_firefox + server, launch_persistent, is_chromium ): (page, context) = await launch_persistent() await page.goto(server.EMPTY_PAGE) @@ -124,7 +124,7 @@ async def test_should_not_block_third_party_cookies( ) await page.waitForTimeout(2000) - allows_third_party = is_chromium or is_firefox + allows_third_party = is_chromium assert document_cookie == ("username=John Doe" if allows_third_party else "") cookies = await context.cookies(server.CROSS_PROCESS_PREFIX + "/grid.html") if allows_third_party: diff --git a/tests/async/test_headful.py b/tests/async/test_headful.py index 3bdb50c85..2a8e8f4c3 100644 --- a/tests/async/test_headful.py +++ b/tests/async/test_headful.py @@ -131,7 +131,7 @@ async def test_should_not_block_third_party_cookies( ) await page.waitForTimeout(2000) - allowsThirdParty = is_chromium or is_firefox + allowsThirdParty = is_chromium assert document_cookie == ("username=John Doe" if allowsThirdParty else "") cookies = await page.context.cookies(server.CROSS_PROCESS_PREFIX + "/grid.html") if allowsThirdParty: diff --git a/tests/async/test_navigation.py b/tests/async/test_navigation.py index 5c02712e9..e629ed5b5 100644 --- a/tests/async/test_navigation.py +++ b/tests/async/test_navigation.py @@ -133,6 +133,7 @@ async def test_goto_should_return_response_when_page_changes_its_url_after_load( assert response.status == 200 +@pytest.mark.skip_browser("firefox") async def test_goto_should_work_with_subframes_return_204(page, server): def handle(request): request.setResponseCode(204) @@ -554,6 +555,7 @@ async def test_wait_for_nav_should_work_with_dom_history_back_forward(page, serv assert page.url == server.PREFIX + "/second.html" +@pytest.mark.skip_browser("firefox") async def test_wait_for_nav_should_work_when_subframe_issues_window_stop(page, server): server.set_route("/frames/style.css", lambda _: None) navigation_promise = asyncio.create_task( diff --git a/tests/async/test_pdf.py b/tests/async/test_pdf.py index 8e414ad9f..a94efb92f 100644 --- a/tests/async/test_pdf.py +++ b/tests/async/test_pdf.py @@ -25,3 +25,9 @@ async def test_should_be_able_to_save_pdf_file(page: Page, server, tmpdir: Path) output_file = tmpdir / "foo.png" await page.pdf(path=str(output_file)) assert os.path.getsize(output_file) > 0 + + +@pytest.mark.only_browser("chromium") +async def test_should_be_able_capture_pdf_without_path(page: Page): + buffer = await page.pdf() + assert buffer diff --git a/tests/sync/test_pdf.py b/tests/sync/test_pdf.py index 196129892..f6d081880 100644 --- a/tests/sync/test_pdf.py +++ b/tests/sync/test_pdf.py @@ -25,3 +25,9 @@ def test_should_be_able_to_save_pdf_file(page: Page, server, tmpdir: Path): 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): + buffer = page.pdf() + assert buffer From fc5485c6057da52d80e5aa9ddcba1fb71e15b2ab Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 13 Aug 2020 17:19:39 +0200 Subject: [PATCH 6/9] enh: cleanup of path and pathlib usages (#166) --- playwright/async_api.py | 84 +++++++++++++++++++++-------------- playwright/browser_context.py | 5 ++- playwright/browser_type.py | 18 +++++--- playwright/download.py | 2 +- playwright/element_handle.py | 13 ++++-- playwright/frame.py | 8 +++- playwright/page.py | 17 ++++--- playwright/selectors.py | 5 ++- playwright/sync_api.py | 84 +++++++++++++++++++++-------------- 9 files changed, 149 insertions(+), 87 deletions(-) diff --git a/playwright/async_api.py b/playwright/async_api.py index 323de3c25..4bb1831e7 100644 --- a/playwright/async_api.py +++ b/playwright/async_api.py @@ -1295,7 +1295,7 @@ async def screenshot( self, timeout: int = None, type: Literal["png", "jpeg"] = None, - path: str = None, + path: typing.Union[str, pathlib.Path] = None, quality: int = None, omitBackground: bool = None, ) -> bytes: @@ -1309,7 +1309,7 @@ async def screenshot( Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the browserContext.setDefaultTimeout(timeout) or page.setDefaultTimeout(timeout) methods. type : Optional[Literal['png', 'jpeg']] Specify screenshot type, defaults to `png`. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to current working directory. If no path is provided, the image won't be saved to the disk. quality : Optional[int] The quality of the image, between 0-100. Not applicable to `png` images. @@ -2039,7 +2039,11 @@ def isDetached(self) -> bool: return mapping.from_maybe_impl(self._impl_obj.isDetached()) async def addScriptTag( - self, url: str = None, path: str = None, content: str = None, type: str = None + self, + url: str = None, + path: typing.Union[str, pathlib.Path] = None, + content: str = None, + type: str = None, ) -> "ElementHandle": """Frame.addScriptTag @@ -2049,7 +2053,7 @@ async def addScriptTag( ---------- url : Optional[str] URL of a script to be added. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] Path to the JavaScript file to be injected into frame. If `path` is a relative path, then it is resolved relative to current working directory. content : Optional[str] Raw JavaScript content to be injected into frame. @@ -2068,7 +2072,10 @@ async def addScriptTag( ) async def addStyleTag( - self, url: str = None, path: str = None, content: str = None + self, + url: str = None, + path: typing.Union[str, pathlib.Path] = None, + content: str = None, ) -> "ElementHandle": """Frame.addStyleTag @@ -2078,7 +2085,7 @@ async def addStyleTag( ---------- url : Optional[str] URL of the `` tag. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] Path to the CSS file to be injected into frame. If `path` is a relative path, then it is resolved relative to current working directory. content : Optional[str] Raw CSS content to be injected into frame. @@ -2748,7 +2755,7 @@ async def register( self, name: str, source: str = None, - path: str = None, + path: typing.Union[str, pathlib.Path] = None, contentScript: bool = None, ) -> NoneType: """Selectors.register @@ -2939,14 +2946,14 @@ async def path(self) -> typing.Union[str, NoneType]: """ return mapping.from_maybe_impl(await self._impl_obj.path()) - async def saveAs(self, path: typing.Union[pathlib.Path, str]) -> NoneType: + async def saveAs(self, path: typing.Union[str, pathlib.Path]) -> NoneType: """Download.saveAs Saves the download to a user-specified path. Parameters ---------- - path : Union[pathlib.Path, str] + path : Union[str, pathlib.Path] Path where the download should be saved. """ return mapping.from_maybe_impl(await self._impl_obj.saveAs(path=path)) @@ -3397,7 +3404,11 @@ async def evalOnSelectorAll( ) async def addScriptTag( - self, url: str = None, path: str = None, content: str = None, type: str = None + self, + url: str = None, + path: typing.Union[str, pathlib.Path] = None, + content: str = None, + type: str = None, ) -> "ElementHandle": """Page.addScriptTag @@ -3408,7 +3419,7 @@ async def addScriptTag( ---------- url : Optional[str] URL of a script to be added. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] Path to the JavaScript file to be injected into frame. If `path` is a relative path, then it is resolved relative to current working directory. content : Optional[str] Raw JavaScript content to be injected into frame. @@ -3427,7 +3438,10 @@ async def addScriptTag( ) async def addStyleTag( - self, url: str = None, path: str = None, content: str = None + self, + url: str = None, + path: typing.Union[str, pathlib.Path] = None, + content: str = None, ) -> "ElementHandle": """Page.addStyleTag @@ -3438,7 +3452,7 @@ async def addStyleTag( ---------- url : Optional[str] URL of the `` tag. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] Path to the CSS file to be injected into frame. If `path` is a relative path, then it is resolved relative to current working directory. content : Optional[str] Raw CSS content to be injected into frame. @@ -3890,7 +3904,9 @@ async def bringToFront(self) -> NoneType: """ return mapping.from_maybe_impl(await self._impl_obj.bringToFront()) - async def addInitScript(self, source: str = None, path: str = None) -> NoneType: + async def addInitScript( + self, source: str = None, path: typing.Union[str, pathlib.Path] = None + ) -> NoneType: """Page.addInitScript Adds a script which would be evaluated in one of the following scenarios: @@ -3966,7 +3982,7 @@ async def screenshot( self, timeout: int = None, type: Literal["png", "jpeg"] = None, - path: str = None, + path: typing.Union[str, pathlib.Path] = None, quality: int = None, omitBackground: bool = None, fullPage: bool = None, @@ -3982,7 +3998,7 @@ async def screenshot( Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the browserContext.setDefaultTimeout(timeout) or page.setDefaultTimeout(timeout) methods. type : Optional[Literal['png', 'jpeg']] Specify screenshot type, defaults to `png`. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to current working directory. If no path is provided, the image won't be saved to the disk. quality : Optional[int] The quality of the image, between 0-100. Not applicable to `png` images. @@ -4618,7 +4634,7 @@ async def pdf( height: typing.Union[str, float] = None, preferCSSPageSize: bool = None, margin: PdfMargins = None, - path: str = None, + path: typing.Union[str, pathlib.Path] = None, ) -> bytes: """Page.pdf @@ -4692,7 +4708,7 @@ async def pdf( Give any CSS `@page` size declared in the page priority over what is declared in `width` and `height` or `format` options. Defaults to `false`, which will scale the content to fit the paper size. margin : Optional[{"top": Union[str, int, NoneType], "right": Union[str, int, NoneType], "bottom": Union[str, int, NoneType], "left": Union[str, int, NoneType]}] Paper margins, defaults to none. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] The file path to save the PDF to. If `path` is a relative path, then it is resolved relative to current working directory. If no path is provided, the PDF won't be saved to the disk. Returns @@ -5016,7 +5032,9 @@ async def setOffline(self, offline: bool) -> NoneType: """ return mapping.from_maybe_impl(await self._impl_obj.setOffline(offline=offline)) - async def addInitScript(self, source: str = None, path: str = None) -> NoneType: + async def addInitScript( + self, source: str = None, path: typing.Union[str, pathlib.Path] = None + ) -> NoneType: """BrowserContext.addInitScript Adds a script which would be evaluated in one of the following scenarios: @@ -5568,7 +5586,7 @@ def executablePath(self) -> str: async def launch( self, - executablePath: str = None, + executablePath: typing.Union[str, pathlib.Path] = None, args: typing.List[str] = None, ignoreDefaultArgs: typing.Union[bool, typing.List[str]] = None, handleSIGINT: bool = None, @@ -5579,7 +5597,7 @@ async def launch( headless: bool = None, devtools: bool = None, proxy: ProxyServer = None, - downloadsPath: str = None, + downloadsPath: typing.Union[str, pathlib.Path] = None, slowMo: int = None, chromiumSandbox: bool = None, ) -> "Browser": @@ -5594,7 +5612,7 @@ async def launch( Parameters ---------- - executablePath : Optional[str] + executablePath : Union[str, pathlib.Path, NoneType] Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to current working directory. Note that Playwright only works with the bundled Chromium, Firefox or WebKit, use at your own risk. args : Optional[List[str]] Additional arguments to pass to the browser instance. The list of Chromium flags can be found here. @@ -5616,7 +5634,7 @@ async def launch( **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. proxy : Optional[{"server": str, "bypass": Optional[str], "username": Optional[str], "password": Optional[str]}] Network proxy settings. - downloadsPath : Optional[str] + downloadsPath : Union[str, pathlib.Path, NoneType] If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. slowMo : Optional[int] Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. @@ -5649,7 +5667,7 @@ async def launch( async def launchServer( self, - executablePath: str = None, + executablePath: typing.Union[str, pathlib.Path] = None, args: typing.List[str] = None, ignoreDefaultArgs: typing.Union[bool, typing.List[str]] = None, handleSIGINT: bool = None, @@ -5660,7 +5678,7 @@ async def launchServer( headless: bool = None, devtools: bool = None, proxy: ProxyServer = None, - downloadsPath: str = None, + downloadsPath: typing.Union[str, pathlib.Path] = None, port: int = None, chromiumSandbox: bool = None, ) -> "BrowserServer": @@ -5670,7 +5688,7 @@ async def launchServer( Parameters ---------- - executablePath : Optional[str] + executablePath : Union[str, pathlib.Path, NoneType] Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to current working directory. **BEWARE**: Playwright is only guaranteed to work with the bundled Chromium, Firefox or WebKit, use at your own risk. args : Optional[List[str]] Additional arguments to pass to the browser instance. The list of Chromium flags can be found here. @@ -5692,7 +5710,7 @@ async def launchServer( **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. proxy : Optional[{"server": str, "bypass": Optional[str], "username": Optional[str], "password": Optional[str]}] Network proxy settings. - downloadsPath : Optional[str] + downloadsPath : Union[str, pathlib.Path, NoneType] If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. port : Optional[int] Port to use for the web socket. Defaults to 0 that picks any available port. @@ -5725,8 +5743,8 @@ async def launchServer( async def launchPersistentContext( self, - userDataDir: str, - executablePath: str = None, + userDataDir: typing.Union[str, pathlib.Path], + executablePath: typing.Union[str, pathlib.Path] = None, args: typing.List[str] = None, ignoreDefaultArgs: typing.Union[bool, typing.List[str]] = None, handleSIGINT: bool = None, @@ -5737,7 +5755,7 @@ async def launchPersistentContext( headless: bool = None, devtools: bool = None, proxy: ProxyServer = None, - downloadsPath: str = None, + downloadsPath: typing.Union[str, pathlib.Path] = None, slowMo: int = None, viewport: IntSize = None, ignoreHTTPSErrors: bool = None, @@ -5764,9 +5782,9 @@ async def launchPersistentContext( Parameters ---------- - userDataDir : str + userDataDir : Union[str, pathlib.Path] Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for Chromium and Firefox. - executablePath : Optional[str] + executablePath : Union[str, pathlib.Path, NoneType] Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to current working directory. **BEWARE**: Playwright is only guaranteed to work with the bundled Chromium, Firefox or WebKit, use at your own risk. args : Optional[List[str]] Additional arguments to pass to the browser instance. The list of Chromium flags can be found here. @@ -5788,7 +5806,7 @@ async def launchPersistentContext( **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. proxy : Optional[{"server": str, "bypass": Optional[str], "username": Optional[str], "password": Optional[str]}] Network proxy settings. - downloadsPath : Optional[str] + downloadsPath : Union[str, pathlib.Path, NoneType] If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. slowMo : Optional[int] Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0. diff --git a/playwright/browser_context.py b/playwright/browser_context.py index be2490441..d00b1bf6a 100644 --- a/playwright/browser_context.py +++ b/playwright/browser_context.py @@ -13,6 +13,7 @@ # limitations under the License. import asyncio +from pathlib import Path from types import SimpleNamespace from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union @@ -139,7 +140,9 @@ async def setExtraHTTPHeaders(self, headers: Dict[str, str]) -> None: async def setOffline(self, offline: bool) -> None: await self._channel.send("setOffline", dict(offline=offline)) - async def addInitScript(self, source: str = None, path: str = None) -> None: + async def addInitScript( + self, source: str = None, path: Union[str, Path] = None + ) -> None: if path: with open(path, "r") as file: source = file.read() diff --git a/playwright/browser_type.py b/playwright/browser_type.py index d2a53d8b5..cf7075c57 100644 --- a/playwright/browser_type.py +++ b/playwright/browser_type.py @@ -48,7 +48,7 @@ def executablePath(self) -> str: async def launch( self, - executablePath: str = None, + executablePath: Union[str, Path] = None, args: List[str] = None, ignoreDefaultArgs: Union[bool, List[str]] = None, handleSIGINT: bool = None, @@ -59,7 +59,7 @@ async def launch( headless: bool = None, devtools: bool = None, proxy: ProxyServer = None, - downloadsPath: str = None, + downloadsPath: Union[str, Path] = None, slowMo: int = None, chromiumSandbox: bool = None, ) -> Browser: @@ -74,7 +74,7 @@ async def launch( async def launchServer( self, - executablePath: str = None, + executablePath: Union[str, Path] = None, args: List[str] = None, ignoreDefaultArgs: Union[bool, List[str]] = None, handleSIGINT: bool = None, @@ -85,7 +85,7 @@ async def launchServer( headless: bool = None, devtools: bool = None, proxy: ProxyServer = None, - downloadsPath: str = None, + downloadsPath: Union[str, Path] = None, port: int = None, chromiumSandbox: bool = None, ) -> BrowserServer: @@ -100,8 +100,8 @@ async def launchServer( async def launchPersistentContext( self, - userDataDir: str, - executablePath: str = None, + userDataDir: Union[str, Path], + executablePath: Union[str, Path] = None, args: List[str] = None, ignoreDefaultArgs: Union[bool, List[str]] = None, handleSIGINT: bool = None, @@ -112,7 +112,7 @@ async def launchPersistentContext( headless: bool = None, devtools: bool = None, proxy: ProxyServer = None, - downloadsPath: str = None, + downloadsPath: Union[str, Path] = None, slowMo: int = None, viewport: IntSize = None, ignoreHTTPSErrors: bool = None, @@ -163,3 +163,7 @@ def normalize_launch_params(params: Dict) -> None: params["ignoreAllDefaultArgs"] = True del params["ignoreDefaultArgs"] params["env"] = {name: str(value) for [name, value] in params["env"].items()} + if "executablePath" in params: + params["executablePath"] = str(Path(params["executablePath"])) + if "downloadsPath" in params: + params["downloadsPath"] = str(Path(params["downloadsPath"])) diff --git a/playwright/download.py b/playwright/download.py index 1c183fd86..6da4de75e 100644 --- a/playwright/download.py +++ b/playwright/download.py @@ -41,6 +41,6 @@ async def failure(self) -> Optional[str]: async def path(self) -> Optional[str]: return await self._channel.send("path") - async def saveAs(self, path: Union[Path, str]) -> None: + async def saveAs(self, path: Union[str, Path]) -> None: path = str(Path(path)) return await self._channel.send("saveAs", dict(path=path)) diff --git a/playwright/element_handle.py b/playwright/element_handle.py index 3986752f6..f90b41644 100644 --- a/playwright/element_handle.py +++ b/playwright/element_handle.py @@ -175,12 +175,19 @@ async def screenshot( self, timeout: int = None, type: Literal["png", "jpeg"] = None, - path: str = None, + path: Union[str, Path] = None, quality: int = None, omitBackground: bool = None, ) -> bytes: - binary = await self._channel.send("screenshot", locals_to_params(locals())) - return base64.b64decode(binary) + params = locals_to_params(locals()) + if "path" in params: + del params["path"] + encoded_binary = await self._channel.send("screenshot", params) + decoded_binary = base64.b64decode(encoded_binary) + if path: + with open(path, "wb") as fd: + fd.write(decoded_binary) + return decoded_binary async def querySelector(self, selector: str) -> Optional["ElementHandle"]: return from_nullable_channel( diff --git a/playwright/frame.py b/playwright/frame.py index 3cf200717..b548f4acd 100644 --- a/playwright/frame.py +++ b/playwright/frame.py @@ -314,7 +314,11 @@ def isDetached(self) -> bool: return self._detached async def addScriptTag( - self, url: str = None, path: str = None, content: str = None, type: str = None, + self, + url: str = None, + path: Union[str, Path] = None, + content: str = None, + type: str = None, ) -> ElementHandle: params = locals_to_params(locals()) if path: @@ -324,7 +328,7 @@ async def addScriptTag( return from_channel(await self._channel.send("addScriptTag", params)) async def addStyleTag( - self, url: str = None, path: str = None, content: str = None + self, url: str = None, path: Union[str, Path] = None, content: str = None ) -> ElementHandle: params = locals_to_params(locals()) if path: diff --git a/playwright/page.py b/playwright/page.py index 45ae02bed..9a19baafd 100644 --- a/playwright/page.py +++ b/playwright/page.py @@ -15,6 +15,7 @@ import asyncio import base64 import sys +from pathlib import Path from types import SimpleNamespace from typing import TYPE_CHECKING, Any, Callable, Dict, List, Union, cast @@ -342,12 +343,16 @@ async def evalOnSelectorAll( ) async def addScriptTag( - self, url: str = None, path: str = None, content: str = None, type: str = None + self, + url: str = None, + path: Union[str, Path] = None, + content: str = None, + type: str = None, ) -> ElementHandle: return await self._main_frame.addScriptTag(**locals_to_params(locals())) async def addStyleTag( - self, url: str = None, path: str = None, content: str = None + self, url: str = None, path: Union[str, Path] = None, content: str = None ) -> ElementHandle: return await self._main_frame.addStyleTag(**locals_to_params(locals())) @@ -500,7 +505,9 @@ def viewportSize(self) -> Optional[Viewport]: async def bringToFront(self) -> None: await self._channel.send("bringToFront") - async def addInitScript(self, source: str = None, path: str = None) -> None: + async def addInitScript( + self, source: str = None, path: Union[str, Path] = None + ) -> None: if path: with open(path, "r") as file: source = file.read() @@ -533,7 +540,7 @@ async def screenshot( self, timeout: int = None, type: Literal["png", "jpeg"] = None, - path: str = None, + path: Union[str, Path] = None, quality: int = None, omitBackground: bool = None, fullPage: bool = None, @@ -726,7 +733,7 @@ async def pdf( height: Union[str, float] = None, preferCSSPageSize: bool = None, margin: PdfMargins = None, - path: str = None, + path: Union[str, Path] = None, ) -> bytes: params = locals_to_params(locals()) if "path" in params: diff --git a/playwright/selectors.py b/playwright/selectors.py index e3f3d8aa9..0154ff964 100644 --- a/playwright/selectors.py +++ b/playwright/selectors.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, Optional +from pathlib import Path +from typing import Dict, Optional, Union from playwright.connection import ChannelOwner from playwright.element_handle import ElementHandle @@ -29,7 +30,7 @@ async def register( self, name: str, source: str = None, - path: str = None, + path: Union[str, Path] = None, contentScript: bool = None, ) -> None: if not source and not path: diff --git a/playwright/sync_api.py b/playwright/sync_api.py index ad9a2363a..f6473cefe 100644 --- a/playwright/sync_api.py +++ b/playwright/sync_api.py @@ -1343,7 +1343,7 @@ def screenshot( self, timeout: int = None, type: Literal["png", "jpeg"] = None, - path: str = None, + path: typing.Union[str, pathlib.Path] = None, quality: int = None, omitBackground: bool = None, ) -> bytes: @@ -1357,7 +1357,7 @@ def screenshot( Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the browserContext.setDefaultTimeout(timeout) or page.setDefaultTimeout(timeout) methods. type : Optional[Literal['png', 'jpeg']] Specify screenshot type, defaults to `png`. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to current working directory. If no path is provided, the image won't be saved to the disk. quality : Optional[int] The quality of the image, between 0-100. Not applicable to `png` images. @@ -2115,7 +2115,11 @@ def isDetached(self) -> bool: return mapping.from_maybe_impl(self._impl_obj.isDetached()) def addScriptTag( - self, url: str = None, path: str = None, content: str = None, type: str = None + self, + url: str = None, + path: typing.Union[str, pathlib.Path] = None, + content: str = None, + type: str = None, ) -> "ElementHandle": """Frame.addScriptTag @@ -2125,7 +2129,7 @@ def addScriptTag( ---------- url : Optional[str] URL of a script to be added. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] Path to the JavaScript file to be injected into frame. If `path` is a relative path, then it is resolved relative to current working directory. content : Optional[str] Raw JavaScript content to be injected into frame. @@ -2146,7 +2150,10 @@ def addScriptTag( ) def addStyleTag( - self, url: str = None, path: str = None, content: str = None + self, + url: str = None, + path: typing.Union[str, pathlib.Path] = None, + content: str = None, ) -> "ElementHandle": """Frame.addStyleTag @@ -2156,7 +2163,7 @@ def addStyleTag( ---------- url : Optional[str] URL of the `` tag. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] Path to the CSS file to be injected into frame. If `path` is a relative path, then it is resolved relative to current working directory. content : Optional[str] Raw CSS content to be injected into frame. @@ -2872,7 +2879,7 @@ def register( self, name: str, source: str = None, - path: str = None, + path: typing.Union[str, pathlib.Path] = None, contentScript: bool = None, ) -> NoneType: """Selectors.register @@ -3065,14 +3072,14 @@ def path(self) -> typing.Union[str, NoneType]: """ return mapping.from_maybe_impl(self._sync(self._impl_obj.path())) - def saveAs(self, path: typing.Union[pathlib.Path, str]) -> NoneType: + def saveAs(self, path: typing.Union[str, pathlib.Path]) -> NoneType: """Download.saveAs Saves the download to a user-specified path. Parameters ---------- - path : Union[pathlib.Path, str] + path : Union[str, pathlib.Path] Path where the download should be saved. """ return mapping.from_maybe_impl(self._sync(self._impl_obj.saveAs(path=path))) @@ -3537,7 +3544,11 @@ def evalOnSelectorAll( ) def addScriptTag( - self, url: str = None, path: str = None, content: str = None, type: str = None + self, + url: str = None, + path: typing.Union[str, pathlib.Path] = None, + content: str = None, + type: str = None, ) -> "ElementHandle": """Page.addScriptTag @@ -3548,7 +3559,7 @@ def addScriptTag( ---------- url : Optional[str] URL of a script to be added. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] Path to the JavaScript file to be injected into frame. If `path` is a relative path, then it is resolved relative to current working directory. content : Optional[str] Raw JavaScript content to be injected into frame. @@ -3569,7 +3580,10 @@ def addScriptTag( ) def addStyleTag( - self, url: str = None, path: str = None, content: str = None + self, + url: str = None, + path: typing.Union[str, pathlib.Path] = None, + content: str = None, ) -> "ElementHandle": """Page.addStyleTag @@ -3580,7 +3594,7 @@ def addStyleTag( ---------- url : Optional[str] URL of the `` tag. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] Path to the CSS file to be injected into frame. If `path` is a relative path, then it is resolved relative to current working directory. content : Optional[str] Raw CSS content to be injected into frame. @@ -4054,7 +4068,9 @@ def bringToFront(self) -> NoneType: """ return mapping.from_maybe_impl(self._sync(self._impl_obj.bringToFront())) - def addInitScript(self, source: str = None, path: str = None) -> NoneType: + def addInitScript( + self, source: str = None, path: typing.Union[str, pathlib.Path] = None + ) -> NoneType: """Page.addInitScript Adds a script which would be evaluated in one of the following scenarios: @@ -4134,7 +4150,7 @@ def screenshot( self, timeout: int = None, type: Literal["png", "jpeg"] = None, - path: str = None, + path: typing.Union[str, pathlib.Path] = None, quality: int = None, omitBackground: bool = None, fullPage: bool = None, @@ -4150,7 +4166,7 @@ def screenshot( Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the browserContext.setDefaultTimeout(timeout) or page.setDefaultTimeout(timeout) methods. type : Optional[Literal['png', 'jpeg']] Specify screenshot type, defaults to `png`. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to current working directory. If no path is provided, the image won't be saved to the disk. quality : Optional[int] The quality of the image, between 0-100. Not applicable to `png` images. @@ -4824,7 +4840,7 @@ def pdf( height: typing.Union[str, float] = None, preferCSSPageSize: bool = None, margin: PdfMargins = None, - path: str = None, + path: typing.Union[str, pathlib.Path] = None, ) -> bytes: """Page.pdf @@ -4898,7 +4914,7 @@ def pdf( Give any CSS `@page` size declared in the page priority over what is declared in `width` and `height` or `format` options. Defaults to `false`, which will scale the content to fit the paper size. margin : Optional[{"top": Union[str, int, NoneType], "right": Union[str, int, NoneType], "bottom": Union[str, int, NoneType], "left": Union[str, int, NoneType]}] Paper margins, defaults to none. - path : Optional[str] + path : Union[str, pathlib.Path, NoneType] The file path to save the PDF to. If `path` is a relative path, then it is resolved relative to current working directory. If no path is provided, the PDF won't be saved to the disk. Returns @@ -5230,7 +5246,9 @@ def setOffline(self, offline: bool) -> NoneType: self._sync(self._impl_obj.setOffline(offline=offline)) ) - def addInitScript(self, source: str = None, path: str = None) -> NoneType: + def addInitScript( + self, source: str = None, path: typing.Union[str, pathlib.Path] = None + ) -> NoneType: """BrowserContext.addInitScript Adds a script which would be evaluated in one of the following scenarios: @@ -5800,7 +5818,7 @@ def executablePath(self) -> str: def launch( self, - executablePath: str = None, + executablePath: typing.Union[str, pathlib.Path] = None, args: typing.List[str] = None, ignoreDefaultArgs: typing.Union[bool, typing.List[str]] = None, handleSIGINT: bool = None, @@ -5811,7 +5829,7 @@ def launch( headless: bool = None, devtools: bool = None, proxy: ProxyServer = None, - downloadsPath: str = None, + downloadsPath: typing.Union[str, pathlib.Path] = None, slowMo: int = None, chromiumSandbox: bool = None, ) -> "Browser": @@ -5826,7 +5844,7 @@ def launch( Parameters ---------- - executablePath : Optional[str] + executablePath : Union[str, pathlib.Path, NoneType] Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to current working directory. Note that Playwright only works with the bundled Chromium, Firefox or WebKit, use at your own risk. args : Optional[List[str]] Additional arguments to pass to the browser instance. The list of Chromium flags can be found here. @@ -5848,7 +5866,7 @@ def launch( **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. proxy : Optional[{"server": str, "bypass": Optional[str], "username": Optional[str], "password": Optional[str]}] Network proxy settings. - downloadsPath : Optional[str] + downloadsPath : Union[str, pathlib.Path, NoneType] If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. slowMo : Optional[int] Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. @@ -5883,7 +5901,7 @@ def launch( def launchServer( self, - executablePath: str = None, + executablePath: typing.Union[str, pathlib.Path] = None, args: typing.List[str] = None, ignoreDefaultArgs: typing.Union[bool, typing.List[str]] = None, handleSIGINT: bool = None, @@ -5894,7 +5912,7 @@ def launchServer( headless: bool = None, devtools: bool = None, proxy: ProxyServer = None, - downloadsPath: str = None, + downloadsPath: typing.Union[str, pathlib.Path] = None, port: int = None, chromiumSandbox: bool = None, ) -> "BrowserServer": @@ -5904,7 +5922,7 @@ def launchServer( Parameters ---------- - executablePath : Optional[str] + executablePath : Union[str, pathlib.Path, NoneType] Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to current working directory. **BEWARE**: Playwright is only guaranteed to work with the bundled Chromium, Firefox or WebKit, use at your own risk. args : Optional[List[str]] Additional arguments to pass to the browser instance. The list of Chromium flags can be found here. @@ -5926,7 +5944,7 @@ def launchServer( **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. proxy : Optional[{"server": str, "bypass": Optional[str], "username": Optional[str], "password": Optional[str]}] Network proxy settings. - downloadsPath : Optional[str] + downloadsPath : Union[str, pathlib.Path, NoneType] If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. port : Optional[int] Port to use for the web socket. Defaults to 0 that picks any available port. @@ -5961,8 +5979,8 @@ def launchServer( def launchPersistentContext( self, - userDataDir: str, - executablePath: str = None, + userDataDir: typing.Union[str, pathlib.Path], + executablePath: typing.Union[str, pathlib.Path] = None, args: typing.List[str] = None, ignoreDefaultArgs: typing.Union[bool, typing.List[str]] = None, handleSIGINT: bool = None, @@ -5973,7 +5991,7 @@ def launchPersistentContext( headless: bool = None, devtools: bool = None, proxy: ProxyServer = None, - downloadsPath: str = None, + downloadsPath: typing.Union[str, pathlib.Path] = None, slowMo: int = None, viewport: IntSize = None, ignoreHTTPSErrors: bool = None, @@ -6000,9 +6018,9 @@ def launchPersistentContext( Parameters ---------- - userDataDir : str + userDataDir : Union[str, pathlib.Path] Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for Chromium and Firefox. - executablePath : Optional[str] + executablePath : Union[str, pathlib.Path, NoneType] Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to current working directory. **BEWARE**: Playwright is only guaranteed to work with the bundled Chromium, Firefox or WebKit, use at your own risk. args : Optional[List[str]] Additional arguments to pass to the browser instance. The list of Chromium flags can be found here. @@ -6024,7 +6042,7 @@ def launchPersistentContext( **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. proxy : Optional[{"server": str, "bypass": Optional[str], "username": Optional[str], "password": Optional[str]}] Network proxy settings. - downloadsPath : Optional[str] + downloadsPath : Union[str, pathlib.Path, NoneType] If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. slowMo : Optional[int] Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0. From 8cea07528370586d70159674f5b15a73443c461f Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 14 Aug 2020 20:14:51 +0200 Subject: [PATCH 7/9] enh: support for usage in a REPL (#161) --- README.md | 18 ++++++++++++++++++ playwright/async_api.py | 3 +++ playwright/main.py | 18 ++++++++++++++---- playwright/playwright.py | 3 +++ playwright/sync_api.py | 3 +++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e4c1624da..a0c304952 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,24 @@ def test_playwright_is_visible_on_google(page): For more information on pytest-playwright, see [GitHub](https://github.com/microsoft/playwright-pytest#readme). +#### REPL support without context managers + +For scripting purposes, it is also possible to start and stop Playwright manually without relying on the indentation of the REPL. + +```py +from playwright import sync_playwright + +playwright = sync_playwright().start() +for browser_type in [playwright.chromium, playwright.firefox, playwright.webkit]: + browser = browser_type.launch() + page = browser.newPage() + page.goto("http://whatsmyuseragent.org/") + page.screenshot(path=f"example-{browser_type.name}.png") + browser.close() + +playwright.stop() +``` + ## More examples #### Mobile and geolocation diff --git a/playwright/async_api.py b/playwright/async_api.py index 4bb1831e7..e6ae2d60a 100644 --- a/playwright/async_api.py +++ b/playwright/async_api.py @@ -5942,5 +5942,8 @@ def selectors(self) -> "Selectors": def devices(self) -> typing.Dict[str, DeviceDescriptor]: return mapping.from_maybe_impl(self._impl_obj.devices) + def stop(self) -> NoneType: + return mapping.from_maybe_impl(self._impl_obj.stop()) + mapping.register(PlaywrightImpl, Playwright) diff --git a/playwright/main.py b/playwright/main.py index 049535ec6..5a7547e11 100644 --- a/playwright/main.py +++ b/playwright/main.py @@ -93,9 +93,14 @@ def callback_wrapper(playwright_impl: Playwright) -> None: self._connection.call_on_object_with_known_name("Playwright", callback_wrapper) set_dispatcher_fiber(greenlet(lambda: self._connection.run_sync())) dispatcher_fiber().switch() - return self._playwright + playwright = self._playwright + playwright.stop = self.__exit__ # type: ignore + return playwright - def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + def start(self) -> SyncPlaywright: + return self.__enter__() + + def __exit__(self, *args: Any) -> None: self._connection.stop_sync() @@ -106,11 +111,16 @@ def __init__(self) -> None: async def __aenter__(self) -> AsyncPlaywright: self._connection = await run_driver_async() self._connection.run_async() - return AsyncPlaywright( + playwright = AsyncPlaywright( await self._connection.wait_for_object_with_known_name("Playwright") ) + playwright.stop = self.__aexit__ # type: ignore + return playwright + + async def start(self) -> AsyncPlaywright: + return await self.__aenter__() - async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + async def __aexit__(self, *args: Any) -> None: self._connection.stop_async() diff --git a/playwright/playwright.py b/playwright/playwright.py index f2c561390..5aca9398f 100644 --- a/playwright/playwright.py +++ b/playwright/playwright.py @@ -39,3 +39,6 @@ def __init__( device["name"]: device["descriptor"] for device in initializer["deviceDescriptors"] } + + def stop(self) -> None: + pass diff --git a/playwright/sync_api.py b/playwright/sync_api.py index f6473cefe..e8ec9f16b 100644 --- a/playwright/sync_api.py +++ b/playwright/sync_api.py @@ -6182,5 +6182,8 @@ def selectors(self) -> "Selectors": def devices(self) -> typing.Dict[str, DeviceDescriptor]: return mapping.from_maybe_impl(self._impl_obj.devices) + def stop(self) -> NoneType: + return mapping.from_maybe_impl(self._impl_obj.stop()) + mapping.register(PlaywrightImpl, Playwright) From 1dfff87c3fb21fcb892f3547d053fecbef427866 Mon Sep 17 00:00:00 2001 From: Ross Wollman Date: Fri, 14 Aug 2020 19:37:10 -0700 Subject: [PATCH 8/9] Add downstream test discrepancy reporter (#167) --- scripts/report_downstream_test_diffs.py | 104 ++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 scripts/report_downstream_test_diffs.py diff --git a/scripts/report_downstream_test_diffs.py b/scripts/report_downstream_test_diffs.py new file mode 100644 index 000000000..8750ae9c1 --- /dev/null +++ b/scripts/report_downstream_test_diffs.py @@ -0,0 +1,104 @@ +import argparse +import json +import os +import re +import subprocess +import typing +from collections import namedtuple + +from playwright.path_utils import get_file_dirname + +_dirname = get_file_dirname() + +TestCase = namedtuple("TestCase", ["api", "file", "test"]) + + +def pytest_test_cases() -> typing.Generator[TestCase, None, None]: + p = subprocess.run( + ["pytest", "--browser", "chromium", "--collect-only", "-q"], + cwd=_dirname / ".." / "tests", + stdout=subprocess.PIPE, + check=True, + ) + regex = ( + r"tests/(?Pa?sync)/test_(?P.*)\.py::test_(?P.*)\[chromium\]" + ) + matches = re.finditer(regex, p.stdout.decode(), re.MULTILINE) + for match in matches: + yield TestCase( + match.group("api"), match.group("file"), match.group("test"), + ) + + +def jest_test_cases(playwright_js_path: str) -> typing.Generator[TestCase, None, None]: + p = subprocess.run( + [ + "node", + os.path.join("test", "runner"), + "test", + "--trial-run", + "--reporter", + "json", + ], + cwd=playwright_js_path, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=True, + ) + + tests = json.loads(p.stdout.decode()) + for test in [*tests["pending"], *tests["passes"], *tests["failures"]]: + regex = r"(.*/)?(?P[^/]+)\.spec\.[jt]s$" + + match = re.match(regex, test["file"]) + if not match: + continue + + file = match.group("file") + + yield TestCase("sync", normalized(file), normalized(test["title"])) + yield TestCase("async", normalized(file), normalized(test["title"])) + + +def normalized(original: str) -> str: + cleaned = re.sub(r"[^a-z0-9_]", "_", original, flags=re.IGNORECASE) + cleaned = re.sub(r"[_]+", "_", cleaned) + cleaned = cleaned.strip("_") + return cleaned + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--playwright-js-path", + type=str, + help="path to playwright JavaScript directory", + required=True, + ) + parser.add_argument( + "--api", + type=str, + help="filter test cases based on API", + choices=["sync", "async"], + ) + args = parser.parse_args() + + python_tests = set(pytest_test_cases()) + javascript_tests = set(jest_test_cases(args.playwright_js_path)) + + if args.api: + javascript_tests = set([x for x in javascript_tests if x.api == args.api]) + + missing = javascript_tests.difference(python_tests) + found = javascript_tests.intersection(python_tests) + + print("MISSING, MISPELLED, OR MISNAMED:") + print("=" * 80) + for (api, file, test) in sorted(missing): + print(f"{api}/test_{file}.py::test_{test}") + + print(f"\nMissing: {len(missing)}, Found: {len(found)}") + + +if __name__ == "__main__": + main() From 74c5f66cbe83478352e7d8338b6e30192720d2e7 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 17 Aug 2020 22:11:07 -0700 Subject: [PATCH 9/9] docs: updated readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0c304952..5d7e3fec3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI version](https://badge.fury.io/py/playwright.svg)](https://pypi.python.org/pypi/playwright/) [![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://join.slack.com/t/playwright/shared_invite/enQtOTEyMTUxMzgxMjIwLThjMDUxZmIyNTRiMTJjNjIyMzdmZDA3MTQxZWUwZTFjZjQwNGYxZGM5MzRmNzZlMWI5ZWUyOTkzMjE5Njg1NDg) [![Chromium version](https://img.shields.io/badge/chromium-86.0.4217.0-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-79.0a1-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-14.0-blue.svg?logo=safari)](https://webkit.org/) -##### [Docs](#documentation) | [API reference](https://github.com/microsoft/playwright/blob/master/docs/api.md) +##### [Docs](#documentation) | [API reference](https://playwright.dev/#?path=docs/api.md) | [Docstrings](https://github.com/microsoft/playwright-python/blob/master/playwright/sync_api.py) Playwright is a Python library to automate [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable** and **fast**.