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)