diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d797f9561..0489ee465 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,11 @@ jobs: - name: Install 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 30 + - 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 30 - name: Coveralls run: coveralls env: diff --git a/playwright/async_api.py b/playwright/async_api.py index 49ab2c2a0..efbaa86a8 100644 --- a/playwright/async_api.py +++ b/playwright/async_api.py @@ -5716,6 +5716,7 @@ async def launchPersistentContext( hasTouch: bool = None, colorScheme: Literal["light", "dark", "no-preference"] = None, acceptDownloads: bool = None, + chromiumSandbox: bool = None, ) -> "BrowserContext": """BrowserType.launchPersistentContext @@ -5784,6 +5785,8 @@ async def launchPersistentContext( Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See page.emulateMedia(options) for more details. Defaults to '`light`'. acceptDownloads : Optional[bool] Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled. + chromiumSandbox : Optional[bool] + Enable Chromium sandboxing. Defaults to `true`. Returns ------- @@ -5823,6 +5826,7 @@ async def launchPersistentContext( hasTouch=hasTouch, colorScheme=colorScheme, acceptDownloads=acceptDownloads, + chromiumSandbox=chromiumSandbox, ) ) diff --git a/playwright/browser_context.py b/playwright/browser_context.py index b24ce9a9d..78972cc44 100644 --- a/playwright/browser_context.py +++ b/playwright/browser_context.py @@ -109,6 +109,8 @@ async def newPage(self) -> Page: async def cookies(self, urls: Union[str, List[str]] = None) -> List[Cookie]: if urls is None: urls = [] + if not isinstance(urls, list): + urls = [urls] return await self._channel.send("cookies", dict(urls=urls)) async def addCookies(self, cookies: List[Cookie]) -> None: diff --git a/playwright/browser_type.py b/playwright/browser_type.py index 62872ef76..e76ca501d 100644 --- a/playwright/browser_type.py +++ b/playwright/browser_type.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path from typing import Dict, List from playwright.browser import Browser @@ -119,7 +120,9 @@ async def launchPersistentContext( hasTouch: bool = None, colorScheme: ColorScheme = None, acceptDownloads: bool = None, + chromiumSandbox: bool = None, ) -> BrowserContext: + userDataDir = str(Path(userDataDir)) try: return from_channel( await self._channel.send( diff --git a/playwright/sync_api.py b/playwright/sync_api.py index 99e8e3723..3bb0dd1f2 100644 --- a/playwright/sync_api.py +++ b/playwright/sync_api.py @@ -5952,6 +5952,7 @@ def launchPersistentContext( hasTouch: bool = None, colorScheme: Literal["light", "dark", "no-preference"] = None, acceptDownloads: bool = None, + chromiumSandbox: bool = None, ) -> "BrowserContext": """BrowserType.launchPersistentContext @@ -6020,6 +6021,8 @@ def launchPersistentContext( Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See page.emulateMedia(options) for more details. Defaults to '`light`'. acceptDownloads : Optional[bool] Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled. + chromiumSandbox : Optional[bool] + Enable Chromium sandboxing. Defaults to `true`. Returns ------- @@ -6060,6 +6063,7 @@ def launchPersistentContext( hasTouch=hasTouch, colorScheme=colorScheme, acceptDownloads=acceptDownloads, + chromiumSandbox=chromiumSandbox, ) ) ) diff --git a/scripts/update_api.sh b/scripts/update_api.sh new file mode 100755 index 000000000..a05afd6b6 --- /dev/null +++ b/scripts/update_api.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +function update_api { + echo "Generating $1" + file_name="$1" + generate_script="$2" + git checkout HEAD -- "$file_name" + + python "$generate_script" > .x + + mv .x "$file_name" + pre-commit run --files $file_name +} + +update_api "playwright/sync_api.py" "scripts/generate_sync_api.py" +update_api "playwright/async_api.py" "scripts/generate_async_api.py" + +echo "Regenerated APIs" diff --git a/tests/async/test_headful.py b/tests/async/test_headful.py new file mode 100644 index 000000000..43aa2b98d --- /dev/null +++ b/tests/async/test_headful.py @@ -0,0 +1,195 @@ +# 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 pytest + + +async def test_should_have_default_url_when_launching_browser( + browser_type, launch_arguments, tmpdir +): + browser_context = await browser_type.launchPersistentContext( + tmpdir, **{**launch_arguments, "headless": False} + ) + urls = [page.url for page in browser_context.pages] + assert urls == ["about:blank"] + await browser_context.close() + + +async def test_headless_should_be_able_to_read_cookies_written_by_headful( + browser_type, launch_arguments, server, tmpdir, is_chromium, is_win +): + if is_chromium and is_win: + pytest.skip("see https://github.com/microsoft/playwright/issues/717") + # Write a cookie in headful chrome + headful_context = await browser_type.launchPersistentContext( + tmpdir, **{**launch_arguments, "headless": False} + ) + headful_page = await headful_context.newPage() + await headful_page.goto(server.EMPTY_PAGE) + await headful_page.evaluate( + """() => document.cookie = 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'""" + ) + await headful_context.close() + # Read the cookie from headless chrome + headless_context = await browser_type.launchPersistentContext( + tmpdir, **{**launch_arguments, "headless": True} + ) + headless_page = await headless_context.newPage() + await headless_page.goto(server.EMPTY_PAGE) + cookie = await headless_page.evaluate("() => document.cookie") + await headless_context.close() + # This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778 + assert cookie == "foo=true" + + +async def test_should_close_browser_with_beforeunload_page( + browser_type, launch_arguments, server, tmpdir +): + browser_context = await browser_type.launchPersistentContext( + tmpdir, **{**launch_arguments, "headless": False} + ) + page = await browser_context.newPage() + await page.goto(server.PREFIX + "/beforeunload.html") + # We have to interact with a page so that 'beforeunload' handlers + # fire. + await page.click("body") + await browser_context.close() + + +async def test_should_not_crash_when_creating_second_context( + browser_type, launch_arguments, server +): + browser = await browser_type.launch(**{**launch_arguments, "headless": False}) + browser_context = await browser.newContext() + await browser_context.newPage() + await browser_context.close() + browser_context = await browser.newContext() + await browser_context.newPage() + await browser_context.close() + await browser.close() + + +async def test_should_click_background_tab(browser_type, launch_arguments, server): + browser = await browser_type.launch(**{**launch_arguments, "headless": False}) + page = await browser.newPage() + await page.setContent( + 'empty.html' + ) + await page.click("a") + await page.click("button") + await browser.close() + + +async def test_should_close_browser_after_context_menu_was_triggered( + browser_type, launch_arguments, server +): + browser = await browser_type.launch(**{**launch_arguments, "headless": False}) + page = await browser.newPage() + await page.goto(server.PREFIX + "/grid.html") + await page.click("body", button="right") + await browser.close() + + +async def test_should_not_block_third_party_cookies( + browser_type, launch_arguments, server, is_chromium, is_firefox +): + browser = await browser_type.launch(**{**launch_arguments, "headless": False}) + page = await browser.newPage() + 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) + allowsThirdParty = is_chromium or is_firefox + assert document_cookie == ("username=John Doe" if allowsThirdParty else "") + cookies = await page.context.cookies(server.CROSS_PROCESS_PREFIX + "/grid.html") + if allowsThirdParty: + assert cookies == [ + { + "domain": "127.0.0.1", + "expires": -1, + "httpOnly": False, + "name": "username", + "path": "/", + "sameSite": "None", + "secure": False, + "value": "John Doe", + } + ] + else: + assert cookies == [] + + await browser.close() + + +@pytest.mark.skip_browser("webkit") +async def test_should_not_override_viewport_size_when_passed_null( + browser_type, launch_arguments, server +): + # Our WebKit embedder does not respect window features. + browser = await browser_type.launch(**{**launch_arguments, "headless": False}) + context = await browser.newContext(viewport=0) + page = await context.newPage() + await page.goto(server.EMPTY_PAGE) + [popup, _] = await asyncio.gather( + page.waitForEvent("popup"), + page.evaluate( + """() => { + const win = window.open(window.location.href, 'Title', 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=300,top=0,left=0'); + win.resizeTo(500, 450); + }""" + ), + ) + await popup.waitForLoadState() + await popup.waitForFunction( + """() => window.outerWidth === 500 && window.outerHeight === 450""" + ) + await context.close() + await browser.close() + + +async def test_page_bring_to_front_should_work(browser_type, launch_arguments): + browser = await browser_type.launch(**{**launch_arguments, "headless": False}) + page1 = await browser.newPage() + await page1.setContent("Page1") + page2 = await browser.newPage() + await page2.setContent("Page2") + + await page1.bringToFront() + assert await page1.evaluate("document.visibilityState") == "visible" + assert await page2.evaluate("document.visibilityState") == "visible" + + await page2.bringToFront() + assert await page1.evaluate("document.visibilityState") == "visible" + assert await page2.evaluate("document.visibilityState") == "visible" + await browser.close() diff --git a/tests/server.py b/tests/server.py index 9c0a1b475..e34b3c105 100644 --- a/tests/server.py +++ b/tests/server.py @@ -14,6 +14,7 @@ import abc import asyncio +import contextlib import gzip import mimetypes import socket @@ -21,11 +22,13 @@ from contextlib import closing from http import HTTPStatus +import greenlet from OpenSSL import crypto from twisted.internet import reactor, ssl from twisted.web import http from playwright.path_utils import get_file_dirname +from playwright.sync_base import dispatcher_fiber _dirname = get_file_dirname() @@ -136,6 +139,30 @@ async def wait_for_request(self, path): self.request_subscribers[path] = future return await future + @contextlib.contextmanager + def expect_request(self, path): + future = asyncio.create_task(self.wait_for_request(path)) + + class CallbackValue: + def __init__(self) -> None: + self._value = None + + @property + def value(self): + return self._value + + g_self = greenlet.getcurrent() + cb_wrapper = CallbackValue() + + def done_cb(task): + cb_wrapper._value = future.result() + g_self.switch() + + future.add_done_callback(done_cb) + yield cb_wrapper + while not future.done(): + dispatcher_fiber.switch() + def set_auth(self, path: str, username: str, password: str): self.auth[path] = (username, password)