'
+ )
+ await page.click("text=Click target")
+ assert await page.evaluate("window.__CLICKED")
+
+
+async def test_wait_for_BUTTON_to_be_clickable_when_it_has_pointer_events_none(
+ page: Page,
+) -> None:
+ await page.set_content(
+ '
Click target '
+ )
+ done = []
+
+ async def click() -> None:
+ await page.click("text=Click target")
+ done.append(True)
+
+ click_promise = asyncio.create_task(click())
+ await give_it_a_chance_to_click(page)
+ assert await page.evaluate("window.__CLICKED") is None
+ assert done == []
+ await page.evaluate(
+ "document.querySelector('button').style.removeProperty('pointer-events')"
+ )
+ await click_promise
+ assert await page.evaluate("window.__CLICKED")
+
+
+async def test_wait_for_LABEL_to_be_clickable_when_it_has_pointer_events_none(
+ page: Page,
+) -> None:
+ await page.set_content(
+ '
Click target '
+ )
+ click_promise = asyncio.create_task(page.click("text=Click target"))
+ # Do a few roundtrips to the page.
+ for _ in range(5):
+ assert await page.evaluate("window.__CLICKED") is None
+ # remove 'pointer-events: none' css from button.
+ await page.evaluate(
+ "document.querySelector('label').style.removeProperty('pointer-events')"
+ )
+ await click_promise
+ assert await page.evaluate("window.__CLICKED")
+
+
+async def test_update_modifiers_correctly(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.click("button", modifiers=["Shift"])
+ assert await page.evaluate("shiftKey")
+ await page.click("button", modifiers=[])
+ assert await page.evaluate("shiftKey") is False
+
+ await page.keyboard.down("Shift")
+ await page.click("button", modifiers=[])
+ assert await page.evaluate("shiftKey") is False
+ await page.click("button")
+ assert await page.evaluate("shiftKey")
+ await page.keyboard.up("Shift")
+ await page.click("button")
+ assert await page.evaluate("shiftKey") is False
+
+
+async def test_click_an_offscreen_element_when_scroll_behavior_is_smooth(
+ page: Page,
+) -> None:
+ await page.set_content(
+ """
+
+ hi
+
+ """
+ )
+ await page.click("button")
+ assert await page.evaluate("window.clicked")
+
+
+async def test_report_nice_error_when_element_is_detached_and_force_clicked(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/animating-button.html")
+ await page.evaluate("addButton()")
+ handle = await page.query_selector("button")
+ assert handle
+ await page.evaluate("stopButton(true)")
+ error: Optional[Error] = None
+ try:
+ await handle.click(force=True)
+ except Error as e:
+ error = e
+ assert await page.evaluate("window.clicked") is None
+ assert error
+ assert "Element is not attached to the DOM" in error.message
+
+
+async def test_fail_when_element_detaches_after_animation(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/animating-button.html")
+ await page.evaluate("addButton()")
+ handle = await page.query_selector("button")
+ assert handle
+ promise = asyncio.create_task(handle.click())
+ await asyncio.sleep(0) # execute scheduled tasks, but don't await them
+ await page.evaluate("stopButton(true)")
+ with pytest.raises(Error) as exc_info:
+ await promise
+ assert await page.evaluate("window.clicked") is None
+ assert "Element is not attached to the DOM" in exc_info.value.message
+
+
+async def test_retry_when_element_detaches_after_animation(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/animating-button.html")
+ await page.evaluate("addButton()")
+ clicked = []
+
+ async def click() -> None:
+ await page.click("button")
+ clicked.append(True)
+
+ promise = asyncio.create_task(click())
+ await asyncio.sleep(0) # execute scheduled tasks, but don't await them
+ assert clicked == []
+ assert await page.evaluate("window.clicked") is None
+ await page.evaluate("stopButton(true)")
+ await page.evaluate("addButton()")
+ assert clicked == []
+ assert await page.evaluate("window.clicked") is None
+ await page.evaluate("stopButton(true)")
+ await page.evaluate("addButton()")
+ assert clicked == []
+ assert await page.evaluate("window.clicked") is None
+ await page.evaluate("stopButton(false)")
+ await promise
+ assert clicked == [True]
+ assert await page.evaluate("window.clicked")
+
+
+async def test_retry_when_element_is_animating_from_outside_the_viewport(
+ page: Page, server: Server
+) -> None:
+ await page.set_content(
+ """
+
+
+
+ """
+ )
+ handle = await page.query_selector("button")
+ assert handle
+ promise = asyncio.create_task(handle.click())
+ await asyncio.sleep(0) # execute scheduled tasks, but don't await them
+ await handle.evaluate("button => button.className = 'animated'")
+ await promise
+ assert await page.evaluate("window.clicked")
+
+
+async def test_fail_when_element_is_animating_from_outside_the_viewport_with_force(
+ page: Page,
+) -> None:
+ await page.set_content(
+ """
+
+
+
+ """
+ )
+ handle = await page.query_selector("button")
+ assert handle
+ promise = asyncio.create_task(handle.click(force=True))
+ await asyncio.sleep(0) # execute scheduled tasks, but don't await them
+ await handle.evaluate("button => button.className = 'animated'")
+ error: Optional[Error] = None
+ try:
+ await promise
+ except Error as e:
+ error = e
+ assert await page.evaluate("window.clicked") is None
+ assert error
+ assert "Element is outside of the viewport" in error.message
+
+
+async def test_not_retarget_when_element_changes_on_hover(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/react.html")
+ await page.evaluate(
+ """() => {
+ renderComponent(e('div', {}, [e(MyButton, { name: 'button1', renameOnHover: true }), e(MyButton, { name: 'button2' })] ));
+ }"""
+ )
+ await page.click("text=button1")
+ assert await page.evaluate("window.button1")
+ assert await page.evaluate("window.button2") is None
+
+
+async def test_not_retarget_when_element_is_recycled_on_hover(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/react.html")
+ await page.evaluate(
+ """() => {
+ function shuffle() {
+ renderComponent(e('div', {}, [e(MyButton, { name: 'button2' }), e(MyButton, { name: 'button1' })] ));
+ }
+ renderComponent(e('div', {}, [e(MyButton, { name: 'button1', onHover: shuffle }), e(MyButton, { name: 'button2' })] ));
+ }"""
+ )
+
+ await page.click("text=button1")
+ assert await page.evaluate("window.button1") is None
+ assert await page.evaluate("window.button2")
+
+
+async def test_click_the_button_when_window_inner_width_is_corrupted(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.evaluate("window.innerWidth = 0")
+ await page.click("button")
+ assert await page.evaluate("result") == "Clicked"
+
+
+async def test_timeout_when_click_opens_alert(page: Page, server: Server) -> None:
+ await page.set_content('
Click me
')
+ async with page.expect_event("dialog") as dialog_info:
+ with pytest.raises(Error) as exc_info:
+ await page.click("div", timeout=3000)
+ assert "Timeout 3000ms exceeded" in exc_info.value.message
+ dialog = await dialog_info.value
+ await dialog.dismiss()
+
+
+async def test_check_the_box(page: Page) -> None:
+ await page.set_content('
')
+ await page.check("input")
+ assert await page.evaluate("checkbox.checked")
+
+
+async def test_not_check_the_checked_box(page: Page) -> None:
+ await page.set_content('
')
+ await page.check("input")
+ assert await page.evaluate("checkbox.checked")
+
+
+async def test_uncheck_the_box(page: Page) -> None:
+ await page.set_content('
')
+ await page.uncheck("input")
+ assert await page.evaluate("checkbox.checked") is False
+
+
+async def test_not_uncheck_the_unchecked_box(page: Page) -> None:
+ await page.set_content('
')
+ await page.uncheck("input")
+ assert await page.evaluate("checkbox.checked") is False
+
+
+async def test_check_the_box_by_label(page: Page) -> None:
+ await page.set_content(
+ '
'
+ )
+ await page.check("label")
+ assert await page.evaluate("checkbox.checked")
+
+
+async def test_check_the_box_outside_label(page: Page) -> None:
+ await page.set_content(
+ '
Text
'
+ )
+ await page.check("label")
+ assert await page.evaluate("checkbox.checked")
+
+
+async def test_check_the_box_inside_label_without_id(page: Page) -> None:
+ await page.set_content(
+ '
Text '
+ )
+ await page.check("label")
+ assert await page.evaluate("checkbox.checked")
+
+
+async def test_check_radio(page: Page) -> None:
+ await page.set_content(
+ """
+
one
+
two
+
three"""
+ )
+ await page.check("#two")
+ assert await page.evaluate("two.checked")
+
+
+async def test_check_the_box_by_aria_role(page: Page) -> None:
+ await page.set_content(
+ """
CHECKBOX
+ """
+ )
+ await page.check("div")
+ assert await page.evaluate("checkbox.getAttribute ('aria-checked')")
diff --git a/tests/async/test_console.py b/tests/async/test_console.py
new file mode 100644
index 0000000..7772523
--- /dev/null
+++ b/tests/async/test_console.py
@@ -0,0 +1,152 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import List
+
+import pytest
+from playwright.async_api import ConsoleMessage, Page
+
+from tests.server import Server
+
+
+async def test_console_should_work(page: Page, browser_name: str) -> None:
+ messages: List[ConsoleMessage] = []
+ page.once("console", lambda m: messages.append(m))
+ async with page.expect_console_message() as message_info:
+ await page.evaluate('() => console.log("hello", 5, {foo: "bar"})')
+ message = await message_info.value
+ if browser_name != "firefox":
+ assert message.text == "hello 5 {foo: bar}"
+ assert str(message) == "hello 5 {foo: bar}"
+ else:
+ assert message.text == "hello 5 JSHandle@object"
+ assert str(message) == "hello 5 JSHandle@object"
+ assert message.type == "log"
+ assert await message.args[0].json_value() == "hello"
+ assert await message.args[1].json_value() == 5
+ assert await message.args[2].json_value() == {"foo": "bar"}
+
+
+async def test_console_should_emit_same_log_twice(page: Page) -> None:
+ messages = []
+ page.on("console", lambda m: messages.append(m.text))
+ await page.evaluate('() => { for (let i = 0; i < 2; ++i ) console.log("hello"); } ')
+ assert messages == ["hello", "hello"]
+
+
+async def test_console_should_use_text_for__str__(page: Page) -> None:
+ messages = []
+ page.on("console", lambda m: messages.append(m))
+ await page.evaluate('() => console.log("Hello world")')
+ assert len(messages) == 1
+ assert str(messages[0]) == "Hello world"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_console_should_work_for_different_console_api_calls(page: Page) -> None:
+ messages: List[ConsoleMessage] = []
+ page.on("console", lambda m: messages.append(m))
+ # All console events will be reported before 'page.evaluate' is finished.
+ await page.evaluate(
+ """() => {
+ // A pair of time/timeEnd generates only one Console API call.
+ console.time('calling console.time');
+ console.timeEnd('calling console.time');
+ console.trace('calling console.trace');
+ console.dir('calling console.dir');
+ console.warn('calling console.warn');
+ console.error('calling console.error');
+ console.log(Promise.resolve('should not wait until resolved!'));
+ }"""
+ )
+ assert list(map(lambda msg: msg.type, messages)) == [
+ "timeEnd",
+ "trace",
+ "dir",
+ "warning",
+ "error",
+ "log",
+ ]
+
+ assert "calling console.time" in messages[0].text
+ assert list(map(lambda msg: msg.text, messages[1:])) == [
+ "calling console.trace",
+ "calling console.dir",
+ "calling console.warn",
+ "calling console.error",
+ "Promise",
+ ]
+
+
+async def test_console_should_not_fail_for_window_object(page: Page, browser_name: str) -> None:
+ async with page.expect_console_message() as message_info:
+ await page.evaluate("console.error(window)")
+ message = await message_info.value
+ if browser_name != "firefox":
+ assert message.text == "Window"
+ else:
+ assert message.text == "JSHandle@object"
+
+
+# Upstream issue https://bugs.webkit.org/show_bug.cgi?id=229515
+@pytest.mark.skip_browser("webkit")
+async def test_console_should_trigger_correct_log(page: Page, server: Server) -> None:
+ await page.goto("about:blank")
+ async with page.expect_console_message() as message_info:
+ await page.evaluate("async url => fetch(url).catch(e => {})", server.EMPTY_PAGE)
+ message = await message_info.value
+ assert "Access-Control-Allow-Origin" in message.text
+ assert message.type == "error"
+
+
+async def test_console_should_have_location_for_console_api_calls(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_console_message() as message_info:
+ await page.goto(server.PREFIX + "/consolelog.html")
+ message = await message_info.value
+ assert message.text == "yellow"
+ assert message.type == "log"
+ location = message.location
+ # Engines have different column notion.
+ assert location["url"] == server.PREFIX + "/consolelog.html"
+ assert location["lineNumber"] == 7
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_console_should_not_throw_when_there_are_console_messages_in_detached_iframes(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as page_info:
+ await page.evaluate(
+ """async() => {
+ // 1. Create a popup that Playwright is not connected to.
+ const win = window.open('');
+ window._popup = win;
+ if (window.document.readyState !== 'complete')
+ await new Promise(f => window.addEventListener('load', f));
+ // 2. In this popup, create an iframe that console.logs a message.
+ win.document.body.innerHTML = `
`;
+ const frame = win.document.querySelector('iframe');
+ if (!frame.contentDocument || frame.contentDocument.readyState !== 'complete')
+ await new Promise(f => frame.addEventListener('load', f));
+ // 3. After that, remove the iframe.
+ frame.remove();
+ }"""
+ )
+ popup = await page_info.value
+ # 4. Connect to the popup and make sure it doesn't throw.
+ assert await popup.evaluate("1 + 1") == 2
diff --git a/tests/async/test_context_manager.py b/tests/async/test_context_manager.py
new file mode 100644
index 0000000..e1689c4
--- /dev/null
+++ b/tests/async/test_context_manager.py
@@ -0,0 +1,36 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Dict
+
+import pytest
+
+from playwright.async_api import BrowserContext, BrowserType
+
+
+async def test_context_managers(browser_type: BrowserType, launch_arguments: Dict) -> None:
+ async with await browser_type.launch(**launch_arguments) as browser:
+ async with await browser.new_context() as context:
+ async with await context.new_page():
+ assert len(context.pages) == 1
+ assert len(context.pages) == 0
+ assert len(browser.contexts) == 1
+ assert len(browser.contexts) == 0
+ assert not browser.is_connected()
+
+
+async def test_context_managers_not_hang(context: BrowserContext) -> None:
+ with pytest.raises(Exception, match="Oops!"):
+ async with await context.new_page():
+ raise Exception("Oops!")
diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py
new file mode 100644
index 0000000..b489914
--- /dev/null
+++ b/tests/async/test_defaultbrowsercontext.py
@@ -0,0 +1,438 @@
+# 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
+from pathlib import Path
+from typing import (
+ Any,
+ AsyncGenerator,
+ Awaitable,
+ Callable,
+ Dict,
+ Literal,
+ Optional,
+ Tuple,
+)
+
+import pytest
+from playwright.async_api import BrowserContext, BrowserType, Error, Page, expect
+
+from tests.server import Server
+from tests.utils import must
+
+from .utils import Utils
+
+
+@pytest.fixture()
+async def launch_persistent(
+ tmpdir: Path, launch_arguments: Dict, browser_type: BrowserType
+) -> AsyncGenerator[Callable[..., Awaitable[Tuple[Page, BrowserContext]]], None]:
+ context: Optional[BrowserContext] = None
+
+ async def _launch(**options: Any) -> Tuple[Page, BrowserContext]:
+ nonlocal context
+ if context:
+ raise ValueError("can only launch one persistent context")
+ context = await browser_type.launch_persistent_context(
+ str(tmpdir), **{**launch_arguments, **options}
+ )
+ assert context
+ return (context.pages[0], context)
+
+ yield _launch
+ await must(context).close()
+
+
+async def test_context_cookies_should_work(
+ server: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+ default_same_site_cookie_value: str,
+) -> None:
+ (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": default_same_site_cookie_value,
+ }
+ ]
+
+
+async def test_context_add_cookies_should_work(
+ server: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+ default_same_site_cookie_value: Literal["Lax", "None", "Strict"],
+) -> None:
+ (page, context) = await launch_persistent()
+ await page.goto(server.EMPTY_PAGE)
+ await page.context.add_cookies(
+ [
+ {
+ "url": server.EMPTY_PAGE,
+ "name": "username",
+ "value": "John Doe",
+ "sameSite": default_same_site_cookie_value,
+ }
+ ]
+ )
+ 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": default_same_site_cookie_value,
+ }
+ ]
+
+
+async def test_context_clear_cookies_should_work(
+ server: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent()
+ await page.goto(server.EMPTY_PAGE)
+ await page.context.add_cookies(
+ [
+ {"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.clear_cookies()
+ await page.reload()
+ assert await page.context.cookies([]) == []
+ assert await page.evaluate("document.cookie") == ""
+
+
+async def test_should_not_block_third_party_cookies(
+ server: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+ is_firefox: bool,
+) -> None:
+ (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.wait_for_timeout(2000)
+ allows_third_party = 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 == []
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox (WIP)")
+async def test_should_support_viewport_option(
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+ utils: Utils,
+) -> None:
+ (page, context) = await launch_persistent(viewport={"width": 456, "height": 789})
+ await utils.verify_viewport(page, 456, 789)
+ page2 = await context.new_page()
+ await utils.verify_viewport(page2, 456, 789)
+
+
+async def test_should_support_device_scale_factor_option(
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent(device_scale_factor=3)
+ assert await page.evaluate("window.devicePixelRatio") == 3
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_support_user_agent_option(
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+ server: Server,
+) -> None:
+ (page, context) = await launch_persistent(user_agent="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"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_support_bypass_csp_option(
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+ server: Server,
+) -> None:
+ (page, context) = await launch_persistent(bypass_csp=True)
+ await page.goto(server.PREFIX + "/csp.html")
+ await page.add_script_tag(content="window.__injected = 42;")
+ assert await page.evaluate("() => window.__injected") == 42
+
+
+async def test_should_support_javascript_enabled_option(
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+ is_webkit: bool,
+) -> None:
+ (page, context) = await launch_persistent(java_script_enabled=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: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent(
+ http_credentials={"username": "user", "password": "pass"}
+ )
+ server.set_auth("/playground.html", "user", "pass")
+ response = await page.goto(server.PREFIX + "/playground.html")
+ assert response
+ assert response.status == 200
+
+
+async def test_should_support_offline_option(
+ server: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (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: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent(has_touch=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: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ # Firefox does not support mobile.
+ (page, context) = await launch_persistent(
+ viewport={"width": 320, "height": 480}, is_mobile=True
+ )
+ await page.goto(server.PREFIX + "/empty.html")
+ assert await page.evaluate("() => window.innerWidth") == 980
+
+
+async def test_should_support_color_scheme_option(
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent(color_scheme="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: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent(timezone_id="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: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent(locale="fr-FR")
+ assert await page.evaluate("() => navigator.language") == "fr-FR"
+
+
+async def test_should_support_geolocation_and_permission_option(
+ server: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (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: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent(ignore_https_errors=True)
+ response = await page.goto(https_server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+
+
+async def test_should_support_extra_http_headers_option(
+ server: Server,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent(extra_http_headers={"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(
+ tmpdir: Path,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (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
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_restore_state_from_userDataDir(
+ browser_type: BrowserType,
+ launch_arguments: Dict,
+ server: Server,
+ tmp_path_factory: pytest.TempPathFactory,
+) -> None:
+ user_data_dir1 = tmp_path_factory.mktemp("test")
+ browser_context = await browser_type.launch_persistent_context(
+ user_data_dir1, **launch_arguments
+ )
+ page = await browser_context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ await page.evaluate('() => localStorage.hey = "hello"')
+ await browser_context.close()
+
+ browser_context2 = await browser_type.launch_persistent_context(
+ user_data_dir1, **launch_arguments
+ )
+ page2 = await browser_context2.new_page()
+ 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.launch_persistent_context(
+ user_data_dir2, **launch_arguments
+ )
+ page3 = await browser_context3.new_page()
+ await page3.goto(server.EMPTY_PAGE)
+ assert await page3.evaluate("() => localStorage.hey") != "hello"
+ await browser_context3.close()
+
+
+async def test_should_have_default_url_when_launching_browser(
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (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: BrowserType, server: Server, tmpdir: Path, launch_arguments: Dict
+) -> None:
+ options = {**launch_arguments, "args": [server.EMPTY_PAGE]}
+ with pytest.raises(Error) as exc:
+ await browser_type.launch_persistent_context(tmpdir, **options)
+ assert "can not specify page" in exc.value.message
+
+
+async def test_should_fire_close_event_for_a_persistent_context(
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent()
+ fired_event: "asyncio.Future[bool]" = asyncio.Future()
+ context.on("close", lambda _: fired_event.set_result(True))
+ await context.close()
+ await fired_event
+
+
+async def test_should_support_reduced_motion(
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent(reduced_motion="reduce")
+ assert await page.evaluate("matchMedia('(prefers-reduced-motion: reduce)').matches")
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_support_har_option(
+ assetdir: Path,
+ launch_persistent: "Callable[..., asyncio.Future[Tuple[Page, BrowserContext]]]",
+) -> None:
+ (page, context) = await launch_persistent()
+ await page.route_from_har(har=assetdir / "har-fulfill.har")
+ await page.goto("http://no.playwright/")
+ assert await page.evaluate("window.value") == "foo"
+ await expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)")
diff --git a/tests/async/test_device_descriptors.py b/tests/async/test_device_descriptors.py
new file mode 100644
index 0000000..f14fc1f
--- /dev/null
+++ b/tests/async/test_device_descriptors.py
@@ -0,0 +1,54 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import Dict
+
+import pytest
+
+from playwright.async_api import Playwright
+
+
+@pytest.mark.only_browser("chromium")
+async def test_should_work(playwright: Playwright, launch_arguments: Dict) -> None:
+ device_descriptor = playwright.devices["Pixel 2"]
+ device_type = device_descriptor["default_browser_type"]
+ browser = await playwright[device_type].launch(**launch_arguments)
+ context = await browser.new_context(
+ **device_descriptor,
+ )
+ page = await context.new_page()
+ assert device_descriptor["default_browser_type"] == "chromium"
+ assert browser.browser_type.name == "chromium"
+
+ assert "Pixel 2" in device_descriptor["user_agent"]
+ assert "Pixel 2" in await page.evaluate("navigator.userAgent")
+
+ assert device_descriptor["device_scale_factor"] > 2
+ assert await page.evaluate("window.devicePixelRatio") > 2
+
+ assert device_descriptor["viewport"]["height"] > 700
+ assert device_descriptor["viewport"]["height"] < 800
+ inner_height = await page.evaluate("window.screen.availHeight")
+ assert inner_height > 700
+ assert inner_height < 800
+
+ assert device_descriptor["viewport"]["width"] > 400
+ assert device_descriptor["viewport"]["width"] < 500
+ inner_width = await page.evaluate("window.screen.availWidth")
+ assert inner_width > 400
+ assert inner_width < 500
+
+ assert device_descriptor["has_touch"]
+ assert device_descriptor["is_mobile"]
+
+ await browser.close()
diff --git a/tests/async/test_dialog.py b/tests/async/test_dialog.py
new file mode 100644
index 0000000..d33b974
--- /dev/null
+++ b/tests/async/test_dialog.py
@@ -0,0 +1,102 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from playwright.async_api import Browser, Dialog, Page
+
+
+async def test_should_fire(page: Page) -> None:
+ result = []
+
+ async def on_dialog(dialog: Dialog) -> None:
+ result.append(True)
+ assert dialog.type == "alert"
+ assert dialog.default_value == ""
+ assert dialog.message == "yo"
+ await dialog.accept()
+
+ page.on("dialog", on_dialog)
+ await page.evaluate("alert('yo')")
+ assert result
+
+
+async def test_should_allow_accepting_prompts(page: Page) -> None:
+ result = []
+
+ async def on_dialog(dialog: Dialog) -> None:
+ result.append(True)
+ assert dialog.type == "prompt"
+ assert dialog.default_value == "yes."
+ assert dialog.message == "question?"
+ await dialog.accept("answer!")
+
+ page.on("dialog", on_dialog)
+ assert await page.evaluate("prompt('question?', 'yes.')") == "answer!"
+ assert result
+
+
+async def test_should_dismiss_the_prompt(page: Page) -> None:
+ result = []
+
+ async def on_dialog(dialog: Dialog) -> None:
+ result.append(True)
+ await dialog.dismiss()
+
+ page.on("dialog", on_dialog)
+ assert await page.evaluate("prompt('question?')") is None
+ assert result
+
+
+async def test_should_accept_the_confirm_prompt(page: Page) -> None:
+ result = []
+
+ async def on_dialog(dialog: Dialog) -> None:
+ result.append(True)
+ await dialog.accept()
+
+ page.on("dialog", on_dialog)
+ assert await page.evaluate("confirm('boolean?')") is True
+ assert result
+
+
+async def test_should_dismiss_the_confirm_prompt(page: Page) -> None:
+ result = []
+
+ async def on_dialog(dialog: Dialog) -> None:
+ result.append(True)
+ await dialog.dismiss()
+
+ page.on("dialog", on_dialog)
+ assert await page.evaluate("confirm('boolean?')") is False
+ assert result
+
+
+async def test_should_be_able_to_close_context_with_open_alert(
+ browser: Browser,
+) -> None:
+ context = await browser.new_context()
+ page = await context.new_page()
+ async with page.expect_event("dialog"):
+ await page.evaluate("() => setTimeout(() => alert('hello'), 0)", None)
+ await context.close()
+
+
+async def test_should_auto_dismiss_the_prompt_without_listeners(page: Page) -> None:
+ result = await page.evaluate('() => prompt("question?")')
+ assert not result
+
+
+async def test_should_auto_dismiss_the_alert_without_listeners(page: Page) -> None:
+ await page.set_content('
Click me
')
+ await page.click("div")
+ assert await page.evaluate('"window._clicked"')
diff --git a/tests/async/test_dispatch_event.py.disabled b/tests/async/test_dispatch_event.py.disabled
new file mode 100644
index 0000000..50b02ba
--- /dev/null
+++ b/tests/async/test_dispatch_event.py.disabled
@@ -0,0 +1,191 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from playwright.async_api import Page, Selectors
+from tests.server import Server
+
+from .utils import Utils
+
+
+async def test_should_dispatch_click_event(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.dispatch_event("button", "click")
+ assert await page.evaluate("() => result") == "Clicked"
+
+
+async def test_should_dispatch_click_event_properties(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.dispatch_event("button", "click")
+ assert await page.evaluate("() => bubbles")
+ assert await page.evaluate("() => cancelable")
+ assert await page.evaluate("() => composed")
+
+
+async def test_should_dispatch_click_svg(page: Page) -> None:
+ await page.set_content(
+ """
+
+
+
+ """
+ )
+ await page.dispatch_event("circle", "click")
+ assert await page.evaluate("() => window.__CLICKED") == 42
+
+
+async def test_should_dispatch_click_on_a_span_with_an_inline_element_inside(
+ page: Page,
+) -> None:
+ await page.set_content(
+ """
+
+
+ """
+ )
+ await page.dispatch_event("span", "click")
+ assert await page.evaluate("() => window.CLICKED") == 42
+
+
+async def test_should_dispatch_click_after_navigation(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.dispatch_event("button", "click")
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.dispatch_event("button", "click")
+ assert await page.evaluate("() => result") == "Clicked"
+
+
+async def test_should_dispatch_click_after_a_cross_origin_navigation(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.dispatch_event("button", "click")
+ await page.goto(server.CROSS_PROCESS_PREFIX + "/input/button.html")
+ await page.dispatch_event("button", "click")
+ assert await page.evaluate("() => result") == "Clicked"
+
+
+async def test_should_not_fail_when_element_is_blocked_on_hover(page: Page) -> None:
+ await page.set_content(
+ """
+
+ Click me
+
+ """
+ )
+ await page.dispatch_event("button", "click")
+ assert await page.evaluate("() => window.clicked")
+
+
+async def test_should_dispatch_click_when_node_is_added_in_shadow_dom(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ watchdog = page.dispatch_event("span", "click")
+ await page.evaluate(
+ """() => {
+ const div = document.createElement('div');
+ div.attachShadow({mode: 'open'});
+ document.body.appendChild(div);
+ }"""
+ )
+ await page.evaluate("() => new Promise(f => setTimeout(f, 100))")
+ await page.evaluate(
+ """() => {
+ const span = document.createElement('span');
+ span.textContent = 'Hello from shadow';
+ span.addEventListener('click', () => window.clicked = true);
+ document.querySelector('div').shadowRoot.appendChild(span);
+ }"""
+ )
+ await watchdog
+ assert await page.evaluate("() => window.clicked")
+
+
+async def test_should_be_atomic(selectors: Selectors, page: Page, utils: Utils) -> None:
+ await utils.register_selector_engine(
+ selectors,
+ "dispatch_event",
+ """{
+ create(root, target) { },
+ query(root, selector) {
+ const result = root.querySelector(selector);
+ if (result)
+ Promise.resolve().then(() => result.onclick = "");
+ return result;
+ },
+ queryAll(root, selector) {
+ const result = Array.from(root.querySelectorAll(selector));
+ for (const e of result)
+ Promise.resolve().then(() => result.onclick = "");
+ return result;
+ }
+ }""",
+ )
+ await page.set_content('
Hello
')
+ await page.dispatch_event("dispatch_event=div", "click")
+ assert await page.evaluate("() => window._clicked")
+
+
+async def test_should_dispatch_drag_drop_events(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/drag-n-drop.html")
+ dataTransfer = await page.evaluate_handle("() => new DataTransfer()")
+ await page.dispatch_event("#source", "dragstart", {"dataTransfer": dataTransfer})
+ await page.dispatch_event("#target", "drop", {"dataTransfer": dataTransfer})
+ assert await page.evaluate(
+ """() => {
+ return source.parentElement === target;
+ }"""
+ )
+
+
+async def test_should_dispatch_drag_and_drop_events_element_handle(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/drag-n-drop.html")
+ dataTransfer = await page.evaluate_handle("() => new DataTransfer()")
+ source = await page.query_selector("#source")
+ assert source
+ await source.dispatch_event("dragstart", {"dataTransfer": dataTransfer})
+ target = await page.query_selector("#target")
+ assert target
+ await target.dispatch_event("drop", {"dataTransfer": dataTransfer})
+ assert await page.evaluate(
+ """() => {
+ return source.parentElement === target;
+ }"""
+ )
+
+
+async def test_should_dispatch_click_event_element_handle(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = await page.query_selector("button")
+ assert button
+ await button.dispatch_event("click")
+ assert await page.evaluate("() => result") == "Clicked"
diff --git a/tests/async/test_download.py b/tests/async/test_download.py
new file mode 100644
index 0000000..04d0f2e
--- /dev/null
+++ b/tests/async/test_download.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
+from asyncio.futures import Future
+from pathlib import Path
+from typing import Callable, Generator
+
+import pytest
+
+from playwright.async_api import Browser, Download, Error, Page
+from tests.server import Server, TestServerRequest
+from tests.utils import TARGET_CLOSED_ERROR_MESSAGE
+
+
+def assert_file_content(path: Path, content: str) -> None:
+ with open(path, "r") as fd:
+ assert fd.read() == content
+
+
+@pytest.fixture(autouse=True)
+def after_each_hook(server: Server) -> Generator[None, None, None]:
+ def handle_download(request: TestServerRequest) -> None:
+ request.setHeader("Content-Type", "application/octet-stream")
+ request.setHeader("Content-Disposition", "attachment")
+ request.write(b"Hello world")
+ request.finish()
+
+ def handle_download_with_file_name(request: TestServerRequest) -> None:
+ request.setHeader("Content-Type", "application/octet-stream")
+ request.setHeader("Content-Disposition", "attachment; filename=file.txt")
+ request.write(b"Hello world")
+ request.finish()
+
+ server.set_route("/download", handle_download)
+ server.set_route("/downloadWithFilename", handle_download_with_file_name)
+ yield
+
+
+async def test_should_report_downloads_with_accept_downloads_false(
+ page: Page, server: Server
+) -> None:
+ await page.set_content(f'
download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ assert download.page is page
+ assert download.url == f"{server.PREFIX}/downloadWithFilename"
+ assert download.suggested_filename == "file.txt"
+ assert (
+ repr(download)
+ == f"
"
+ )
+ assert await download.path()
+ assert await download.failure() is None
+
+
+async def test_should_report_downloads_with_accept_downloads_true(
+ browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ path = await download.path()
+ assert os.path.isfile(path)
+ assert_file_content(path, "Hello world")
+ await page.close()
+
+
+async def test_should_save_to_user_specified_path(
+ tmpdir: Path, browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ user_path = tmpdir / "download.txt"
+ await download.save_as(user_path)
+ assert user_path.exists()
+ assert user_path.read_text("utf-8") == "Hello world"
+ await page.close()
+
+
+async def test_should_save_to_user_specified_path_without_updating_original_path(
+ tmpdir: Path, browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ user_path = tmpdir / "download.txt"
+ await download.save_as(user_path)
+ assert user_path.exists()
+ assert user_path.read_text("utf-8") == "Hello world"
+
+ originalPath = Path(await download.path())
+ assert originalPath.exists()
+ assert originalPath.read_text("utf-8") == "Hello world"
+ await page.close()
+
+
+async def test_should_save_to_two_different_paths_with_multiple_save_as_calls(
+ tmpdir: Path, browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ user_path = tmpdir / "download.txt"
+ await download.save_as(user_path)
+ assert user_path.exists()
+ assert user_path.read_text("utf-8") == "Hello world"
+
+ anotheruser_path = tmpdir / "download (2).txt"
+ await download.save_as(anotheruser_path)
+ assert anotheruser_path.exists()
+ assert anotheruser_path.read_text("utf-8") == "Hello world"
+ await page.close()
+
+
+async def test_should_save_to_overwritten_filepath(
+ tmpdir: Path, browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ user_path = tmpdir / "download.txt"
+ await download.save_as(user_path)
+ assert len(list(Path(tmpdir).glob("*.*"))) == 1
+ await download.save_as(user_path)
+ assert len(list(Path(tmpdir).glob("*.*"))) == 1
+ assert user_path.exists()
+ assert user_path.read_text("utf-8") == "Hello world"
+ await page.close()
+
+
+async def test_should_create_subdirectories_when_saving_to_non_existent_user_specified_path(
+ tmpdir: Path, browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ nested_path = tmpdir / "these" / "are" / "directories" / "download.txt"
+ await download.save_as(nested_path)
+ assert nested_path.exists()
+ assert nested_path.read_text("utf-8") == "Hello world"
+ await page.close()
+
+
+async def test_should_error_when_saving_with_downloads_disabled(
+ tmpdir: Path, browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=False)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ user_path = tmpdir / "download.txt"
+ with pytest.raises(Error) as exc:
+ await download.save_as(user_path)
+ assert (
+ "Pass 'accept_downloads=True' when you are creating your browser context"
+ in exc.value.message
+ )
+ assert (
+ "Pass 'accept_downloads=True' when you are creating your browser context."
+ == await download.failure()
+ )
+ await page.close()
+
+
+async def test_should_error_when_saving_after_deletion(
+ tmpdir: Path, browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ user_path = tmpdir / "download.txt"
+ await download.delete()
+ with pytest.raises(Error) as exc:
+ await download.save_as(user_path)
+ assert TARGET_CLOSED_ERROR_MESSAGE in exc.value.message
+ await page.close()
+
+
+async def test_should_report_non_navigation_downloads(browser: Browser, server: Server) -> None:
+ # Mac WebKit embedder does not download in this case, although Safari does.
+ def handle_download(request: TestServerRequest) -> None:
+ request.setHeader("Content-Type", "application/octet-stream")
+ request.write(b"Hello world")
+ request.finish()
+
+ server.set_route("/download", handle_download)
+
+ page = await browser.new_page(accept_downloads=True)
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ assert download.suggested_filename == "file.txt"
+ path = await download.path()
+ assert os.path.exists(path)
+ assert_file_content(path, "Hello world")
+ await page.close()
+
+
+async def test_report_download_path_within_page_on_download_handler_for_files(
+ browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ on_download_path: Future[Path] = asyncio.Future()
+
+ async def on_download(download: Download) -> None:
+ on_download_path.set_result(await download.path())
+
+ page.once(
+ "download",
+ lambda res: asyncio.create_task(on_download(res)),
+ )
+ await page.set_content(f'download ')
+ await page.click("a")
+ path = await on_download_path
+ assert_file_content(path, "Hello world")
+ await page.close()
+
+
+async def test_download_report_download_path_within_page_on_handle_for_blobs(
+ browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ on_download_path: "asyncio.Future[Path]" = asyncio.Future()
+
+ async def on_download(download: Download) -> None:
+ on_download_path.set_result(await download.path())
+
+ page.once(
+ "download",
+ lambda res: asyncio.create_task(on_download(res)),
+ )
+
+ await page.goto(server.PREFIX + "/download-blob.html")
+ await page.click("a")
+ path = await on_download_path
+ assert_file_content(path, "Hello world")
+ await page.close()
+
+
+@pytest.mark.only_browser("chromium")
+async def test_should_report_alt_click_downloads(browser: Browser, server: Server) -> None:
+ # Firefox does not download on alt-click by default.
+ # Our WebKit embedder does not download on alt-click, although Safari does.
+ def handle_download(request: TestServerRequest) -> None:
+ request.setHeader("Content-Type", "application/octet-stream")
+ request.write(b"Hello world")
+ request.finish()
+
+ server.set_route("/download", handle_download)
+
+ page = await browser.new_page(accept_downloads=True)
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a", modifiers=["Alt"])
+ download = await download_info.value
+ path = await download.path()
+ assert os.path.exists(path)
+ assert_file_content(path, "Hello world")
+ await page.close()
+
+
+async def test_should_report_new_window_downloads(browser: Browser, server: Server) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ path = await download.path()
+ assert os.path.exists(path)
+ await page.close()
+
+
+async def test_should_delete_file(browser: Browser, server: Server) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ path = await download.path()
+ assert os.path.exists(path)
+ await download.delete()
+ assert os.path.exists(path) is False
+ await page.close()
+
+
+async def test_should_delete_downloads_on_context_destruction(
+ browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download1 = await download_info.value
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download2 = await download_info.value
+ path1 = await download1.path()
+ path2 = await download2.path()
+ assert os.path.exists(path1)
+ assert os.path.exists(path2)
+ await page.context.close()
+ assert os.path.exists(path1) is False
+ assert os.path.exists(path2) is False
+
+
+async def test_should_delete_downloads_on_browser_gone(
+ browser_factory: "Callable[..., asyncio.Future[Browser]]", server: Server
+) -> None:
+ browser = await browser_factory()
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download1 = await download_info.value
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download2 = await download_info.value
+ path1 = await download1.path()
+ path2 = await download2.path()
+ assert os.path.exists(path1)
+ assert os.path.exists(path2)
+ await browser.close()
+ assert os.path.exists(path1) is False
+ assert os.path.exists(path2) is False
+ assert os.path.exists(os.path.join(path1, "..")) is False
+
+
+async def test_download_cancel_should_work(browser: Browser, server: Server) -> None:
+ def handle_download(request: TestServerRequest) -> None:
+ request.setHeader("Content-Type", "application/octet-stream")
+ request.setHeader("Content-Disposition", "attachment")
+ # Chromium requires a large enough payload to trigger the download event soon enough
+ request.write(b"a" * 4096)
+ request.write(b"foo")
+
+ server.set_route("/downloadWithDelay", handle_download)
+ page = await browser.new_page(accept_downloads=True)
+ await page.set_content(f'download ')
+ async with page.expect_download() as download_info:
+ await page.click("a")
+ download = await download_info.value
+ await download.cancel()
+ assert await download.failure() == "canceled"
+ await page.close()
diff --git a/tests/async/test_element_handle.py.disabled b/tests/async/test_element_handle.py.disabled
new file mode 100644
index 0000000..ce46f04
--- /dev/null
+++ b/tests/async/test_element_handle.py.disabled
@@ -0,0 +1,754 @@
+# 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
+from typing import Optional, cast
+
+import pytest
+from playwright.async_api import Browser, ElementHandle, Error, FloatRect, Page
+
+from tests.server import Server
+
+from .utils import Utils
+
+
+async def test_bounding_box(page: Page, server: Server) -> None:
+ await page.set_viewport_size({"width": 500, "height": 500})
+ await page.goto(server.PREFIX + "/grid.html")
+ element_handle = await page.query_selector(".box:nth-of-type(13)")
+ assert element_handle
+ box = await element_handle.bounding_box()
+ assert box == {"x": 100, "y": 50, "width": 50, "height": 50}
+
+
+async def test_bounding_box_handle_nested_frames(page: Page, server: Server) -> None:
+ await page.set_viewport_size({"width": 500, "height": 500})
+ await page.goto(server.PREFIX + "/frames/nested-frames.html")
+ nested_frame = page.frame(name="dos")
+ assert nested_frame
+ element_handle = await nested_frame.query_selector("div")
+ assert element_handle
+ box = await element_handle.bounding_box()
+ assert box == {"x": 24, "y": 224, "width": 268, "height": 18}
+
+
+async def test_bounding_box_return_null_for_invisible_elements(page: Page, server: Server) -> None:
+ await page.set_content('hi
')
+ element = await page.query_selector("div")
+ assert element
+ assert await element.bounding_box() is None
+
+
+async def test_bounding_box_force_a_layout(page: Page, server: Server) -> None:
+ await page.set_viewport_size({"width": 500, "height": 500})
+ await page.set_content('hello
')
+ element_handle = await page.query_selector("div")
+ assert element_handle
+ await page.evaluate('element => element.style.height = "200px"', element_handle)
+ box = await element_handle.bounding_box()
+ assert box == {"x": 8, "y": 8, "width": 100, "height": 200}
+
+
+async def test_bounding_box_with_SVG_nodes(page: Page, server: Server) -> None:
+ await page.set_content(
+ """
+
+ """
+ )
+ element = await page.query_selector("#therect")
+ assert element
+ pw_bounding_box = await element.bounding_box()
+ web_bounding_box = await page.evaluate(
+ """e => {
+ rect = e.getBoundingClientRect()
+ return {x: rect.x, y: rect.y, width: rect.width, height: rect.height}
+ }""",
+ element,
+ )
+ assert pw_bounding_box == web_bounding_box
+
+
+@pytest.mark.skip_browser("firefox")
+async def test_bounding_box_with_page_scale(browser: Browser, server: Server) -> None:
+ context = await browser.new_context(viewport={"width": 400, "height": 400}, is_mobile=True)
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = await page.query_selector("button")
+ assert button
+ await button.evaluate(
+ """button => {
+ document.body.style.margin = '0'
+ button.style.borderWidth = '0'
+ button.style.width = '200px'
+ button.style.height = '20px'
+ button.style.marginLeft = '17px'
+ button.style.marginTop = '23px'
+ }"""
+ )
+
+ box = await button.bounding_box()
+ assert box
+ assert round(box["x"] * 100) == 17 * 100
+ assert round(box["y"] * 100) == 23 * 100
+ assert round(box["width"] * 100) == 200 * 100
+ assert round(box["height"] * 100) == 20 * 100
+ await context.close()
+
+
+async def test_bounding_box_when_inline_box_child_is_outside_of_viewport(
+ page: Page,
+) -> None:
+ await page.set_content(
+ """
+
+ woof doggo
+ """
+ )
+ handle = await page.query_selector("span")
+ assert handle
+ box = await handle.bounding_box()
+ web_bounding_box = await handle.evaluate(
+ """e => {
+ rect = e.getBoundingClientRect();
+ return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
+ }"""
+ )
+
+ def roundbox(b: Optional[FloatRect]) -> FloatRect:
+ assert b
+ return {
+ "x": round(b["x"] * 100),
+ "y": round(b["y"] * 100),
+ "width": round(b["width"] * 100),
+ "height": round(b["height"] * 100),
+ }
+
+ assert roundbox(box) == roundbox(web_bounding_box)
+
+
+async def test_content_frame(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ element_handle = await page.query_selector("#frame1")
+ assert element_handle
+ frame = await element_handle.content_frame()
+ assert frame == page.frames[1]
+
+
+async def test_content_frame_for_non_iframes(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ frame = page.frames[1]
+ element_handle = cast(ElementHandle, await frame.evaluate_handle("document.body"))
+ assert await element_handle.content_frame() is None
+
+
+async def test_content_frame_for_document_element(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ frame = page.frames[1]
+ element_handle = cast(ElementHandle, await frame.evaluate_handle("document.documentElement"))
+ assert await element_handle.content_frame() is None
+
+
+async def test_owner_frame(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ frame = page.frames[1]
+ element_handle = cast(ElementHandle, await frame.evaluate_handle("document.body"))
+ assert await element_handle.owner_frame() == frame
+
+
+async def test_owner_frame_for_cross_process_iframes(
+ page: Page, server: Server, utils: Utils
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.CROSS_PROCESS_PREFIX + "/empty.html")
+ frame = page.frames[1]
+ element_handle = cast(ElementHandle, await frame.evaluate_handle("document.body"))
+ assert await element_handle.owner_frame() == frame
+
+
+async def test_owner_frame_for_document(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ frame = page.frames[1]
+ element_handle = cast(ElementHandle, await frame.evaluate_handle("document"))
+ assert await element_handle.owner_frame() == frame
+
+
+async def test_owner_frame_for_iframe_elements(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ frame = page.main_frame
+ element_handle = cast(
+ ElementHandle, await frame.evaluate_handle('document.querySelector("#frame1")')
+ )
+ assert await element_handle.owner_frame() == frame
+
+
+async def test_owner_frame_for_cross_frame_evaluations(
+ page: Page, server: Server, utils: Utils
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ frame = page.main_frame
+ element_handle = cast(
+ ElementHandle,
+ await frame.evaluate_handle(
+ 'document.querySelector("#frame1").contentWindow.document.body'
+ ),
+ )
+ assert await element_handle.owner_frame() == frame.child_frames[0]
+
+
+async def test_owner_frame_for_detached_elements(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ div_handle = cast(
+ ElementHandle,
+ await page.evaluate_handle(
+ """() => {
+ div = document.createElement('div');
+ document.body.appendChild(div);
+ return div;
+ }"""
+ ),
+ )
+ assert div_handle
+
+ assert await div_handle.owner_frame() == page.main_frame
+ await page.evaluate(
+ """() => {
+ div = document.querySelector('div')
+ document.body.removeChild(div)
+ }"""
+ )
+ assert await div_handle.owner_frame() == page.main_frame
+
+
+async def test_owner_frame_for_adopted_elements(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("url => window.__popup = window.open(url)", server.EMPTY_PAGE)
+ popup = await popup_info.value
+ div_handle = cast(
+ ElementHandle,
+ await page.evaluate_handle(
+ """() => {
+ div = document.createElement('div');
+ document.body.appendChild(div);
+ return div;
+ }"""
+ ),
+ )
+ assert div_handle
+ assert await div_handle.owner_frame() == page.main_frame
+ await popup.wait_for_load_state("domcontentloaded")
+ await page.evaluate(
+ """() => {
+ div = document.querySelector('div');
+ window.__popup.document.body.appendChild(div);
+ }"""
+ )
+ assert await div_handle.owner_frame() == popup.main_frame
+
+
+async def test_click(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = await page.query_selector("button")
+ assert button
+ await button.click()
+ assert await page.evaluate("result") == "Clicked"
+
+
+async def test_click_with_node_removed(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.evaluate('delete window["Node"]')
+ button = await page.query_selector("button")
+ assert button
+ await button.click()
+ assert await page.evaluate("result") == "Clicked"
+
+
+async def test_click_for_shadow_dom_v1(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/shadow.html")
+ button_handle = cast(ElementHandle, await page.evaluate_handle("button"))
+ await button_handle.click()
+ assert await page.evaluate("clicked")
+
+
+async def test_click_for_TextNodes(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ buttonTextNode = cast(
+ ElementHandle,
+ await page.evaluate_handle('document.querySelector("button").firstChild'),
+ )
+ await buttonTextNode.click()
+ assert await page.evaluate("result") == "Clicked"
+
+
+async def test_click_throw_for_detached_nodes(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = await page.query_selector("button")
+ assert button
+ await page.evaluate("button => button.remove()", button)
+ with pytest.raises(Error) as exc_info:
+ await button.click()
+ assert "Element is not attached to the DOM" in exc_info.value.message
+
+
+async def test_click_throw_for_hidden_nodes_with_force(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = await page.query_selector("button")
+ assert button
+ await page.evaluate('button => button.style.display = "none"', button)
+ with pytest.raises(Error) as exc_info:
+ await button.click(force=True)
+ assert "Element is not visible" in exc_info.value.message
+
+
+async def test_click_throw_for_recursively_hidden_nodes_with_force(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = await page.query_selector("button")
+ assert button
+ await page.evaluate('button => button.parentElement.style.display = "none"', button)
+ with pytest.raises(Error) as exc_info:
+ await button.click(force=True)
+ assert "Element is not visible" in exc_info.value.message
+
+
+async def test_click_throw_for__br__elements_with_force(page: Page, server: Server) -> None:
+ await page.set_content("hello goodbye")
+ br = await page.query_selector("br")
+ assert br
+ with pytest.raises(Error) as exc_info:
+ await br.click(force=True)
+ assert "Element is outside of the viewport" in exc_info.value.message
+
+
+async def test_double_click_the_button(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.evaluate(
+ """() => {
+ window.double = false;
+ button = document.querySelector('button');
+ button.addEventListener('dblclick', event => {
+ window.double = true;
+ });
+ }"""
+ )
+ button = await page.query_selector("button")
+ assert button
+ await button.dblclick()
+ assert await page.evaluate("double")
+ assert await page.evaluate("result") == "Clicked"
+
+
+async def test_hover(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/scrollable.html")
+ button = await page.query_selector("#button-6")
+ assert button
+ await button.hover()
+ assert await page.evaluate('document.querySelector("button:hover").id') == "button-6"
+
+
+async def test_hover_when_node_is_removed(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/scrollable.html")
+ await page.evaluate('delete window["Node"]')
+ button = await page.query_selector("#button-6")
+ assert button
+ await button.hover()
+ assert await page.evaluate('document.querySelector("button:hover").id') == "button-6"
+
+
+async def test_scroll(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/offscreenbuttons.html")
+ for i in range(11):
+ button = await page.query_selector(f"#btn{i}")
+ assert button
+ before = await button.evaluate(
+ """button => {
+ return button.getBoundingClientRect().right - window.innerWidth
+ }"""
+ )
+
+ assert before == 10 * i
+ await button.scroll_into_view_if_needed()
+ after = await button.evaluate(
+ """button => {
+ return button.getBoundingClientRect().right - window.innerWidth
+ }"""
+ )
+
+ assert after <= 0
+ await page.evaluate("() => window.scrollTo(0, 0)")
+
+
+async def test_scroll_should_throw_for_detached_element(page: Page, server: Server) -> None:
+ await page.set_content("Hello
")
+ div = await page.query_selector("div")
+ assert div
+ await div.evaluate("div => div.remove()")
+ with pytest.raises(Error) as exc_info:
+ await div.scroll_into_view_if_needed()
+ assert "Element is not attached to the DOM" in exc_info.value.message
+
+
+async def waiting_helper(page: Page, after: str) -> None:
+ div = await page.query_selector("div")
+ assert div
+ done = []
+
+ async def scroll() -> None:
+ done.append(False)
+ await div.scroll_into_view_if_needed()
+ done.append(True)
+
+ promise = asyncio.create_task(scroll())
+ await asyncio.sleep(0) # execute scheduled tasks, but don't await them
+ await page.evaluate("() => new Promise(f => setTimeout(f, 1000))")
+ assert done == [False]
+ await div.evaluate(after)
+ await promise
+ assert done == [False, True]
+
+
+async def test_should_wait_for_display_none_to_become_visible(page: Page) -> None:
+ await page.set_content('Hello
')
+ await waiting_helper(page, 'div => div.style.display = "block"')
+
+
+async def test_should_work_for_visibility_hidden_element(page: Page) -> None:
+ await page.set_content('Hello
')
+ div = await page.query_selector("div")
+ assert div
+ await div.scroll_into_view_if_needed()
+
+
+async def test_should_work_for_zero_sized_element(page: Page) -> None:
+ await page.set_content('Hello
')
+ div = await page.query_selector("div")
+ assert div
+ await div.scroll_into_view_if_needed()
+
+
+async def test_should_wait_for_nested_display_none_to_become_visible(
+ page: Page,
+) -> None:
+ await page.set_content('Hello
')
+ await waiting_helper(page, 'div => div.parentElement.style.display = "block"')
+
+
+async def test_should_timeout_waiting_for_visible(page: Page) -> None:
+ await page.set_content('Hello
')
+ div = await page.query_selector("div")
+ assert div
+ with pytest.raises(Error) as exc_info:
+ await div.scroll_into_view_if_needed(timeout=3000)
+ assert "element is not visible" in exc_info.value.message
+ assert "retrying scroll into view action" in exc_info.value.message
+
+
+async def test_fill_input(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ handle = await page.query_selector("input")
+ assert handle
+ await handle.fill("some value")
+ assert await page.evaluate("result") == "some value"
+
+
+async def test_fill_input_when_Node_is_removed(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.evaluate('delete window["Node"]')
+ handle = await page.query_selector("input")
+ assert handle
+ await handle.fill("some value")
+ assert await page.evaluate("result") == "some value"
+
+
+async def test_select_textarea(
+ page: Page, server: Server, is_firefox: bool, is_webkit: bool
+) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ textarea = await page.query_selector("textarea")
+ assert textarea
+ await textarea.evaluate('textarea => textarea.value = "some value"')
+ await textarea.select_text()
+ if is_firefox or is_webkit:
+ assert await textarea.evaluate("el => el.selectionStart") == 0
+ assert await textarea.evaluate("el => el.selectionEnd") == 10
+ else:
+ assert await page.evaluate("() => window.getSelection().toString()") == "some value"
+
+
+async def test_select_input(page: Page, server: Server, is_firefox: bool, is_webkit: bool) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ input = await page.query_selector("input")
+ assert input
+ await input.evaluate('input => input.value = "some value"')
+ await input.select_text()
+ if is_firefox or is_webkit:
+ assert await input.evaluate("el => el.selectionStart") == 0
+ assert await input.evaluate("el => el.selectionEnd") == 10
+ else:
+ assert await page.evaluate("() => window.getSelection().toString()") == "some value"
+
+
+async def test_select_text_select_plain_div(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ div = await page.query_selector("div.plain")
+ assert div
+ await div.select_text()
+ assert await page.evaluate("() => window.getSelection().toString()") == "Plain div"
+
+
+async def test_select_text_timeout_waiting_for_invisible_element(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ textarea = await page.query_selector("textarea")
+ assert textarea
+ await textarea.evaluate('e => e.style.display = "none"')
+ with pytest.raises(Error) as exc_info:
+ await textarea.select_text(timeout=3000)
+ assert "element is not visible" in exc_info.value.message
+
+
+async def test_select_text_wait_for_visible(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ textarea = await page.query_selector("textarea")
+ assert textarea
+ await textarea.evaluate('textarea => textarea.value = "some value"')
+ await textarea.evaluate('e => e.style.display = "none"')
+ done = []
+
+ async def select_text() -> None:
+ done.append(False)
+ await textarea.select_text(timeout=3000)
+ done.append(True)
+
+ promise = asyncio.create_task(select_text())
+ await asyncio.sleep(0) # execute scheduled tasks, but don't await them
+ await page.evaluate("() => new Promise(f => setTimeout(f, 1000))")
+ await textarea.evaluate('e => e.style.display = "block"')
+ await promise
+ assert done == [False, True]
+
+
+async def test_a_nice_preview(page: Page, server: Server) -> None:
+ await page.goto(f"{server.PREFIX}/dom.html")
+ outer = await page.query_selector("#outer")
+ inner = await page.query_selector("#inner")
+ assert inner
+ check = await page.query_selector("#check")
+ text = await inner.evaluate_handle("e => e.firstChild")
+ await page.evaluate("1") # Give them a chance to calculate the preview.
+ assert str(outer) == 'JSHandle@…
'
+ assert str(inner) == 'JSHandle@Text,↵more text
'
+ assert str(text) == "JSHandle@#text=Text,↵more text"
+ assert str(check) == 'JSHandle@ '
+
+
+async def test_get_attribute(page: Page, server: Server) -> None:
+ await page.goto(f"{server.PREFIX}/dom.html")
+ handle = await page.query_selector("#outer")
+ assert handle
+ assert await handle.get_attribute("name") == "value"
+ assert await page.get_attribute("#outer", "name") == "value"
+
+
+async def test_inner_html(page: Page, server: Server) -> None:
+ await page.goto(f"{server.PREFIX}/dom.html")
+ handle = await page.query_selector("#outer")
+ assert handle
+ assert await handle.inner_html() == 'Text,\nmore text
'
+ assert await page.inner_html("#outer") == 'Text,\nmore text
'
+
+
+async def test_inner_text(page: Page, server: Server) -> None:
+ await page.goto(f"{server.PREFIX}/dom.html")
+ handle = await page.query_selector("#inner")
+ assert handle
+ assert await handle.inner_text() == "Text, more text"
+ assert await page.inner_text("#inner") == "Text, more text"
+
+
+async def test_inner_text_should_throw(page: Page, server: Server) -> None:
+ await page.set_content("text ")
+ with pytest.raises(Error) as exc_info1:
+ await page.inner_text("svg")
+ assert " Node is not an HTMLElement" in exc_info1.value.message
+
+ handle = await page.query_selector("svg")
+ assert handle
+ with pytest.raises(Error) as exc_info2:
+ await handle.inner_text()
+ assert " Node is not an HTMLElement" in exc_info2.value.message
+
+
+async def test_text_content(page: Page, server: Server) -> None:
+ await page.goto(f"{server.PREFIX}/dom.html")
+ handle = await page.query_selector("#inner")
+ assert handle
+ assert await handle.text_content() == "Text,\nmore text"
+ assert await page.text_content("#inner") == "Text,\nmore text"
+
+
+async def test_check_the_box(page: Page) -> None:
+ await page.set_content(' ')
+ input = await page.query_selector("input")
+ assert input
+ await input.check()
+ assert await page.evaluate("checkbox.checked")
+
+
+async def test_uncheck_the_box(page: Page) -> None:
+ await page.set_content(' ')
+ input = await page.query_selector("input")
+ assert input
+ await input.uncheck()
+ assert await page.evaluate("checkbox.checked") is False
+
+
+async def test_select_single_option(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ select = await page.query_selector("select")
+ assert select
+ await select.select_option(value="blue")
+ assert await page.evaluate("result.onInput") == ["blue"]
+ assert await page.evaluate("result.onChange") == ["blue"]
+
+
+async def test_focus_a_button(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = await page.query_selector("button")
+ assert button
+ assert await button.evaluate("button => document.activeElement === button") is False
+ await button.focus()
+ assert await button.evaluate("button => document.activeElement === button")
+
+
+async def test_is_visible_and_is_hidden_should_work(page: Page) -> None:
+ await page.set_content("Hi
")
+ div = await page.query_selector("div")
+ assert div
+ assert await div.is_visible()
+ assert await div.is_hidden() is False
+ assert await page.is_visible("div")
+ assert await page.is_hidden("div") is False
+ span = await page.query_selector("span")
+ assert span
+ assert await span.is_visible() is False
+ assert await span.is_hidden()
+ assert await page.is_visible("span") is False
+ assert await page.is_hidden("span")
+
+
+async def test_is_enabled_and_is_disabled_should_work(page: Page) -> None:
+ await page.set_content(
+ """
+ button1
+ button2
+ div
+ """
+ )
+ div = await page.query_selector("div")
+ assert div
+ assert await div.is_enabled()
+ assert await div.is_disabled() is False
+ assert await page.is_enabled("div")
+ assert await page.is_disabled("div") is False
+ button1 = await page.query_selector(":text('button1')")
+ assert button1
+ assert await button1.is_enabled() is False
+ assert await button1.is_disabled()
+ assert await page.is_enabled(":text('button1')") is False
+ assert await page.is_disabled(":text('button1')")
+ button2 = await page.query_selector(":text('button2')")
+ assert button2
+ assert await button2.is_enabled()
+ assert await button2.is_disabled() is False
+ assert await page.is_enabled(":text('button2')")
+ assert await page.is_disabled(":text('button2')") is False
+
+
+async def test_is_editable_should_work(page: Page) -> None:
+ await page.set_content(" ")
+ await page.eval_on_selector("textarea", "t => t.readOnly = true")
+ input1 = await page.query_selector("#input1")
+ assert input1
+ assert await input1.is_editable() is False
+ assert await page.is_editable("#input1") is False
+ input2 = await page.query_selector("#input2")
+ assert input2
+ assert await input2.is_editable()
+ assert await page.is_editable("#input2")
+ textarea = await page.query_selector("textarea")
+ assert textarea
+ assert await textarea.is_editable() is False
+ assert await page.is_editable("textarea") is False
+
+
+async def test_is_checked_should_work(page: Page) -> None:
+ await page.set_content('Not a checkbox
')
+ handle = await page.query_selector("input")
+ assert handle
+ assert await handle.is_checked()
+ assert await page.is_checked("input")
+ await handle.evaluate("input => input.checked = false")
+ assert await handle.is_checked() is False
+ assert await page.is_checked("input") is False
+ with pytest.raises(Error) as exc_info:
+ await page.is_checked("div")
+ assert "Not a checkbox or radio button" in exc_info.value.message
+
+
+async def test_input_value(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ element = await page.query_selector("input")
+ assert element
+ await element.fill("my-text-content")
+ assert await element.input_value() == "my-text-content"
+
+ await element.fill("")
+ assert await element.input_value() == ""
+
+
+async def test_set_checked(page: Page) -> None:
+ await page.set_content("` `")
+ input = await page.query_selector("input")
+ assert input
+ await input.set_checked(True)
+ assert await page.evaluate("checkbox.checked")
+ await input.set_checked(False)
+ assert await page.evaluate("checkbox.checked") is False
+
+
+async def test_should_allow_disposing_twice(page: Page) -> None:
+ await page.set_content("")
+ element = await page.query_selector("section")
+ assert element
+ await element.dispose()
+ await element.dispose()
diff --git a/tests/async/test_element_handle_wait_for_element_state.py b/tests/async/test_element_handle_wait_for_element_state.py
new file mode 100644
index 0000000..2ad39f4
--- /dev/null
+++ b/tests/async/test_element_handle_wait_for_element_state.py
@@ -0,0 +1,156 @@
+# 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
+from typing import List
+
+import pytest
+
+from playwright.async_api import ElementHandle, Error, Page
+from tests.server import Server
+
+
+async def give_it_a_chance_to_resolve(page: Page) -> None:
+ for i in range(5):
+ await page.evaluate(
+ "() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))"
+ )
+
+
+async def wait_for_state(div: ElementHandle, state: str, done: List[bool]) -> None:
+ await div.wait_for_element_state(state) # type: ignore
+ done[0] = True
+
+
+async def wait_for_state_to_throw(div: ElementHandle, state: str) -> pytest.ExceptionInfo[Error]:
+ with pytest.raises(Error) as exc_info:
+ await div.wait_for_element_state(state) # type: ignore
+ return exc_info
+
+
+async def test_should_wait_for_visible(page: Page) -> None:
+ await page.set_content('content
')
+ div = await page.query_selector("div")
+ assert div
+ done = [False]
+ promise = asyncio.create_task(wait_for_state(div, "visible", done))
+ await give_it_a_chance_to_resolve(page)
+ assert done[0] is False
+ assert div
+ await div.evaluate('div => div.style.display = "block"')
+ await promise
+
+
+async def test_should_wait_for_already_visible(page: Page) -> None:
+ await page.set_content("content
")
+ div = await page.query_selector("div")
+ assert div
+ await div.wait_for_element_state("visible")
+
+
+async def test_should_timeout_waiting_for_visible(page: Page) -> None:
+ await page.set_content('content
')
+ div = await page.query_selector("div")
+ assert div
+ with pytest.raises(Error) as exc_info:
+ await div.wait_for_element_state("visible", timeout=1000)
+ assert "Timeout 1000ms exceeded" in exc_info.value.message
+
+
+async def test_should_throw_waiting_for_visible_when_detached(page: Page) -> None:
+ await page.set_content('content
')
+ div = await page.query_selector("div")
+ assert div
+ promise = asyncio.create_task(wait_for_state_to_throw(div, "visible"))
+ await div.evaluate("div => div.remove()")
+ exc_info = await promise
+ assert "Element is not attached to the DOM" in exc_info.value.message
+
+
+async def test_should_wait_for_hidden(page: Page) -> None:
+ await page.set_content("content
")
+ div = await page.query_selector("div")
+ assert div
+ done = [False]
+ promise = asyncio.create_task(wait_for_state(div, "hidden", done))
+ await give_it_a_chance_to_resolve(page)
+ assert done[0] is False
+ await div.evaluate('div => div.style.display = "none"')
+ await promise
+
+
+async def test_should_wait_for_already_hidden(page: Page) -> None:
+ await page.set_content("
")
+ div = await page.query_selector("div")
+ assert div
+ await div.wait_for_element_state("hidden")
+
+
+async def test_should_wait_for_hidden_when_detached(page: Page) -> None:
+ await page.set_content("content
")
+ div = await page.query_selector("div")
+ assert div
+ done = [False]
+ promise = asyncio.create_task(wait_for_state(div, "hidden", done))
+ await give_it_a_chance_to_resolve(page)
+ assert done[0] is False
+ assert div
+ await div.evaluate("div => div.remove()")
+ await promise
+
+
+async def test_should_wait_for_enabled_button(page: Page, server: Server) -> None:
+ await page.set_content("Target ")
+ span = await page.query_selector("text=Target")
+ assert span
+ done = [False]
+ promise = asyncio.create_task(wait_for_state(span, "enabled", done))
+ await give_it_a_chance_to_resolve(page)
+ assert done[0] is False
+ await span.evaluate("span => span.parentElement.disabled = false")
+ await promise
+
+
+async def test_should_throw_waiting_for_enabled_when_detached(page: Page) -> None:
+ await page.set_content("Target ")
+ button = await page.query_selector("button")
+ assert button
+ promise = asyncio.create_task(wait_for_state_to_throw(button, "enabled"))
+ await button.evaluate("button => button.remove()")
+ exc_info = await promise
+ assert "Element is not attached to the DOM" in exc_info.value.message
+
+
+async def test_should_wait_for_disabled_button(page: Page) -> None:
+ await page.set_content("Target ")
+ span = await page.query_selector("text=Target")
+ assert span
+ done = [False]
+ promise = asyncio.create_task(wait_for_state(span, "disabled", done))
+ await give_it_a_chance_to_resolve(page)
+ assert done[0] is False
+ await span.evaluate("span => span.parentElement.disabled = true")
+ await promise
+
+
+async def test_should_wait_for_editable_input(page: Page, server: Server) -> None:
+ await page.set_content(" ")
+ input = await page.query_selector("input")
+ assert input
+ done = [False]
+ promise = asyncio.create_task(wait_for_state(input, "editable", done))
+ await give_it_a_chance_to_resolve(page)
+ assert done[0] is False
+ await input.evaluate("input => input.readOnly = false")
+ await promise
diff --git a/tests/async/test_emulation_focus.py b/tests/async/test_emulation_focus.py
new file mode 100644
index 0000000..782b8ac
--- /dev/null
+++ b/tests/async/test_emulation_focus.py
@@ -0,0 +1,173 @@
+# 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
+from typing import Callable
+
+import pytest
+from playwright.async_api import Page
+
+from tests.server import Server
+
+from .utils import Utils
+
+
+async def test_should_think_that_it_is_focused_by_default(page: Page) -> None:
+ assert await page.evaluate("document.hasFocus()")
+
+
+async def test_should_think_that_all_pages_are_focused(page: Page) -> None:
+ page2 = await page.context.new_page()
+ assert await page.evaluate("document.hasFocus()")
+ assert await page2.evaluate("document.hasFocus()")
+ await page2.close()
+
+
+async def test_should_focus_popups_by_default(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("url => { window.open(url); }", server.EMPTY_PAGE)
+ popup = await popup_info.value
+ assert await popup.evaluate("document.hasFocus()")
+ assert await page.evaluate("document.hasFocus()")
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_provide_target_for_keyboard_events(page: Page, server: Server) -> None:
+ page2 = await page.context.new_page()
+ await asyncio.gather(
+ page.goto(server.PREFIX + "/input/textarea.html"),
+ page2.goto(server.PREFIX + "/input/textarea.html"),
+ )
+ await asyncio.gather(
+ page.focus("input"),
+ page2.focus("input"),
+ )
+ text = "first"
+ text2 = "second"
+ await asyncio.gather(
+ page.keyboard.type(text),
+ page2.keyboard.type(text2),
+ )
+ results = await asyncio.gather(
+ page.evaluate("result"),
+ page2.evaluate("result"),
+ )
+ assert results == [text, text2]
+
+
+async def test_should_not_affect_mouse_event_target_page(page: Page, server: Server) -> None:
+ page2 = await page.context.new_page()
+ click_counter = """() => {
+ document.onclick = () => window.click_count = (window.click_count || 0) + 1;
+ }"""
+ await asyncio.gather(
+ page.evaluate(click_counter),
+ page2.evaluate(click_counter),
+ page.focus("body"),
+ page2.focus("body"),
+ )
+ await asyncio.gather(
+ page.mouse.click(1, 1),
+ page2.mouse.click(1, 1),
+ )
+ counters = await asyncio.gather(
+ page.evaluate("window.click_count"),
+ page2.evaluate("window.click_count"),
+ )
+ assert counters == [1, 1]
+
+
+async def test_should_change_document_activeElement(page: Page, server: Server) -> None:
+ page2 = await page.context.new_page()
+ await asyncio.gather(
+ page.goto(server.PREFIX + "/input/textarea.html"),
+ page2.goto(server.PREFIX + "/input/textarea.html"),
+ )
+ await asyncio.gather(
+ page.focus("input"),
+ page2.focus("textarea"),
+ )
+ active = await asyncio.gather(
+ page.evaluate("document.activeElement.tagName"),
+ page2.evaluate("document.activeElement.tagName"),
+ )
+ assert active == ["INPUT", "TEXTAREA"]
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_not_affect_screenshots(
+ page: Page, server: Server, assert_to_be_golden: Callable[[bytes, str], None]
+) -> None:
+ # Firefox headed produces a different image.
+ page2 = await page.context.new_page()
+ await asyncio.gather(
+ page.set_viewport_size({"width": 500, "height": 500}),
+ page.goto(server.PREFIX + "/grid.html"),
+ page2.set_viewport_size({"width": 50, "height": 50}),
+ page2.goto(server.PREFIX + "/grid.html"),
+ )
+ await asyncio.gather(
+ page.focus("body"),
+ page2.focus("body"),
+ )
+ screenshots = await asyncio.gather(
+ page.screenshot(),
+ page2.screenshot(),
+ )
+ assert_to_be_golden(screenshots[0], "screenshot-sanity.png")
+ assert_to_be_golden(screenshots[1], "grid-cell-0.png")
+
+
+async def test_should_change_focused_iframe(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ [frame1, frame2] = await asyncio.gather(
+ utils.attach_frame(page, "frame1", server.PREFIX + "/input/textarea.html"),
+ utils.attach_frame(page, "frame2", server.PREFIX + "/input/textarea.html"),
+ )
+ logger = """() => {
+ self._events = [];
+ const element = document.querySelector('input');
+ element.onfocus = element.onblur = (e) => self._events.push(e.type);
+ }"""
+ await asyncio.gather(
+ frame1.evaluate(logger),
+ frame2.evaluate(logger),
+ )
+ focused = await asyncio.gather(
+ frame1.evaluate("document.hasFocus()"),
+ frame2.evaluate("document.hasFocus()"),
+ )
+ assert focused == [False, False]
+ await frame1.focus("input")
+ events = await asyncio.gather(
+ frame1.evaluate("self._events"),
+ frame2.evaluate("self._events"),
+ )
+ assert events == [["focus"], []]
+ focused = await asyncio.gather(
+ frame1.evaluate("document.hasFocus()"),
+ frame2.evaluate("document.hasFocus()"),
+ )
+ assert focused == [True, False]
+ await frame2.focus("input")
+ events = await asyncio.gather(
+ frame1.evaluate("self._events"),
+ frame2.evaluate("self._events"),
+ )
+ assert events == [["focus", "blur"], ["focus"]]
+ focused = await asyncio.gather(
+ frame1.evaluate("document.hasFocus()"),
+ frame2.evaluate("document.hasFocus()"),
+ )
+ assert focused == [False, True]
diff --git a/tests/async/test_expect_misc.py b/tests/async/test_expect_misc.py
new file mode 100644
index 0000000..8dd0639
--- /dev/null
+++ b/tests/async/test_expect_misc.py
@@ -0,0 +1,76 @@
+# 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 pytest
+
+from playwright.async_api import Page, TimeoutError, expect
+from tests.server import Server
+
+
+async def test_to_be_in_viewport_should_work(page: Page, server: Server) -> None:
+ await page.set_content(
+ """
+
+ foo
+ """
+ )
+ await expect(page.locator("#big")).to_be_in_viewport()
+ await expect(page.locator("#small")).not_to_be_in_viewport()
+ await page.locator("#small").scroll_into_view_if_needed()
+ await expect(page.locator("#small")).to_be_in_viewport()
+ await expect(page.locator("#small")).to_be_in_viewport(ratio=1)
+
+
+async def test_to_be_in_viewport_should_respect_ratio_option(page: Page, server: Server) -> None:
+ await page.set_content(
+ """
+
+
+ """
+ )
+ await expect(page.locator("div")).to_be_in_viewport()
+ await expect(page.locator("div")).to_be_in_viewport(ratio=0.1)
+ await expect(page.locator("div")).to_be_in_viewport(ratio=0.2)
+
+ await expect(page.locator("div")).to_be_in_viewport(ratio=0.25)
+ # In this test, element's ratio is 0.25.
+ await expect(page.locator("div")).not_to_be_in_viewport(ratio=0.26)
+
+ await expect(page.locator("div")).not_to_be_in_viewport(ratio=0.3)
+ await expect(page.locator("div")).not_to_be_in_viewport(ratio=0.7)
+ await expect(page.locator("div")).not_to_be_in_viewport(ratio=0.8)
+
+
+async def test_to_be_in_viewport_should_have_good_stack(page: Page, server: Server) -> None:
+ with pytest.raises(AssertionError) as exc_info:
+ await expect(page.locator("body")).not_to_be_in_viewport(timeout=100)
+ assert 'unexpected value "viewport ratio' in str(exc_info.value)
+
+
+async def test_to_be_in_viewport_should_report_intersection_even_if_fully_covered_by_other_element(
+ page: Page, server: Server
+) -> None:
+ await page.set_content(
+ """
+ hello
+ None:
+ with pytest.raises(TimeoutError) as exc_info:
+ await page.wait_for_selector("#not-found", timeout=1)
+ assert exc_info.value.name == "TimeoutError"
diff --git a/tests/async/test_fetch_browser_context.py b/tests/async/test_fetch_browser_context.py
new file mode 100644
index 0000000..26eecc6
--- /dev/null
+++ b/tests/async/test_fetch_browser_context.py
@@ -0,0 +1,314 @@
+# 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 base64
+import json
+from typing import Any, Callable, cast
+from urllib.parse import parse_qs
+
+import pytest
+from playwright.async_api import Browser, BrowserContext, Error, FilePayload, Page
+
+from tests.server import Server, TestServerRequest
+from tests.utils import must
+
+
+async def test_get_should_work(context: BrowserContext, server: Server) -> None:
+ response = await context.request.get(server.PREFIX + "/simple.json")
+ assert response.url == server.PREFIX + "/simple.json"
+ assert response.status == 200
+ assert response.status_text == "OK"
+ assert response.ok is True
+ assert response.headers["content-type"] == "application/json"
+ assert {
+ "name": "Content-Type",
+ "value": "application/json",
+ } in response.headers_array
+ assert await response.text() == '{"foo": "bar"}\n'
+
+
+async def test_fetch_should_work(context: BrowserContext, server: Server) -> None:
+ response = await context.request.fetch(server.PREFIX + "/simple.json")
+ assert response.url == server.PREFIX + "/simple.json"
+ assert response.status == 200
+ assert response.status_text == "OK"
+ assert response.ok is True
+ assert response.headers["content-type"] == "application/json"
+ assert {
+ "name": "Content-Type",
+ "value": "application/json",
+ } in response.headers_array
+ assert await response.text() == '{"foo": "bar"}\n'
+
+
+async def test_should_throw_on_network_error(context: BrowserContext, server: Server) -> None:
+ server.set_route("/test", lambda request: request.loseConnection())
+ with pytest.raises(Error, match="socket hang up"):
+ await context.request.fetch(server.PREFIX + "/test")
+
+
+async def test_should_add_session_cookies_to_request(
+ context: BrowserContext, server: Server
+) -> None:
+ await context.add_cookies(
+ [
+ {
+ "name": "username",
+ "value": "John Doe",
+ "url": server.EMPTY_PAGE,
+ "expires": -1,
+ "httpOnly": False,
+ "secure": False,
+ "sameSite": "Lax",
+ }
+ ]
+ )
+ [server_req, response] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ context.request.get(server.EMPTY_PAGE),
+ )
+ assert server_req.getHeader("Cookie") == "username=John Doe"
+
+
+@pytest.mark.parametrize("method", ["fetch", "delete", "get", "head", "patch", "post", "put"])
+async def test_should_support_query_params(
+ context: BrowserContext, server: Server, method: str
+) -> None:
+ expected_params = {"p1": "v1", "парам2": "знач2"}
+ [server_req, _] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ getattr(context.request, method)(server.EMPTY_PAGE + "?p1=foo", params=expected_params),
+ )
+ assert server_req.args["p1".encode()][0].decode() == "v1"
+ assert len(server_req.args["p1".encode()]) == 1
+ assert server_req.args["парам2".encode()][0].decode() == "знач2"
+
+
+@pytest.mark.parametrize("method", ["fetch", "delete", "get", "head", "patch", "post", "put"])
+async def test_should_support_fail_on_status_code(
+ context: BrowserContext, server: Server, method: str
+) -> None:
+ with pytest.raises(Error, match="404 Not Found"):
+ await getattr(context.request, method)(
+ server.PREFIX + "/this-does-clearly-not-exist.html",
+ fail_on_status_code=True,
+ )
+
+
+@pytest.mark.parametrize("method", ["fetch", "delete", "get", "head", "patch", "post", "put"])
+async def test_should_support_ignore_https_errors_option(
+ context: BrowserContext, https_server: Server, method: str
+) -> None:
+ response = await getattr(context.request, method)(
+ https_server.EMPTY_PAGE, ignore_https_errors=True
+ )
+ assert response.ok
+ assert response.status == 200
+
+
+async def test_should_not_add_context_cookie_if_cookie_header_passed_as_parameter(
+ context: BrowserContext, server: Server
+) -> None:
+ await context.add_cookies(
+ [
+ {
+ "name": "username",
+ "value": "John Doe",
+ "url": server.EMPTY_PAGE,
+ "expires": -1,
+ "httpOnly": False,
+ "secure": False,
+ "sameSite": "Lax",
+ }
+ ]
+ )
+ [server_req, _] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ context.request.get(server.EMPTY_PAGE, headers={"Cookie": "foo=bar"}),
+ )
+ assert server_req.getHeader("Cookie") == "foo=bar"
+
+
+async def test_should_support_http_credentials_send_immediately_for_browser_context(
+ context_factory: "Callable[..., asyncio.Future[BrowserContext]]", server: Server
+) -> None:
+ context = await context_factory(
+ http_credentials={
+ "username": "user",
+ "password": "pass",
+ "origin": server.PREFIX.upper(),
+ "send": "always",
+ }
+ )
+ # First request
+ server_request, response = await asyncio.gather(
+ server.wait_for_request("/empty.html"), context.request.get(server.EMPTY_PAGE)
+ )
+ expected_auth = "Basic " + base64.b64encode(b"user:pass").decode()
+ assert server_request.getHeader("authorization") == expected_auth
+ assert response.status == 200
+
+ # Second request
+ server_request, response = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ context.request.get(server.CROSS_PROCESS_PREFIX + "/empty.html"),
+ )
+ # Not sent to another origin.
+ assert server_request.getHeader("authorization") is None
+ assert response.status == 200
+
+
+async def test_support_http_credentials_send_immediately_for_browser_new_page(
+ server: Server, browser: Browser
+) -> None:
+ page = await browser.new_page(
+ http_credentials={
+ "username": "user",
+ "password": "pass",
+ "origin": server.PREFIX.upper(),
+ "send": "always",
+ }
+ )
+ server_request, response = await asyncio.gather(
+ server.wait_for_request("/empty.html"), page.request.get(server.EMPTY_PAGE)
+ )
+ assert (
+ server_request.getHeader("authorization")
+ == "Basic " + base64.b64encode(b"user:pass").decode()
+ )
+ assert response.status == 200
+
+ server_request, response = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ page.request.get(server.CROSS_PROCESS_PREFIX + "/empty.html"),
+ )
+ # Not sent to another origin.
+ assert server_request.getHeader("authorization") is None
+ assert response.status == 200
+
+ await page.close()
+
+
+@pytest.mark.parametrize("method", ["delete", "patch", "post", "put"])
+async def test_should_support_post_data(
+ context: BrowserContext, method: str, server: Server
+) -> None:
+ async def support_post_data(fetch_data: Any, request_post_data: Any) -> None:
+ [request, response] = await asyncio.gather(
+ server.wait_for_request("/simple.json"),
+ getattr(context.request, method)(server.PREFIX + "/simple.json", data=fetch_data),
+ )
+ assert request.method.decode() == method.upper()
+ assert request.post_body == request_post_data
+ assert response.status == 200
+ assert response.url == server.PREFIX + "/simple.json"
+ assert request.getHeader("Content-Length") == str(len(must(request.post_body)))
+
+ await support_post_data("My request", "My request".encode())
+ await support_post_data(b"My request", "My request".encode())
+ await support_post_data(["my", "request"], json.dumps(["my", "request"]).encode())
+ await support_post_data({"my": "request"}, json.dumps({"my": "request"}).encode())
+ with pytest.raises(Error, match="Unsupported 'data' type:
"):
+ await support_post_data(lambda: None, None)
+
+
+async def test_should_support_application_x_www_form_urlencoded(
+ context: BrowserContext, server: Server
+) -> None:
+ [request, response] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ context.request.post(
+ server.PREFIX + "/empty.html",
+ form={
+ "firstName": "John",
+ "lastName": "Doe",
+ "file": "f.js",
+ },
+ ),
+ )
+ assert request.method == b"POST"
+ assert request.getHeader("Content-Type") == "application/x-www-form-urlencoded"
+ assert request.post_body
+ body = request.post_body.decode()
+ assert request.getHeader("Content-Length") == str(len(body))
+ params = parse_qs(request.post_body)
+ assert params[b"firstName"] == [b"John"]
+ assert params[b"lastName"] == [b"Doe"]
+ assert params[b"file"] == [b"f.js"]
+
+
+async def test_should_support_multipart_form_data(context: BrowserContext, server: Server) -> None:
+ file: FilePayload = {
+ "name": "f.js",
+ "mimeType": "text/javascript",
+ "buffer": b"var x = 10;\r\n;console.log(x);",
+ }
+ [request, _] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ context.request.post(
+ server.PREFIX + "/empty.html",
+ multipart={
+ "firstName": "John",
+ "lastName": "Doe",
+ "file": file,
+ },
+ ),
+ )
+ assert request.method == b"POST"
+ assert cast(str, request.getHeader("Content-Type")).startswith("multipart/form-data; ")
+ assert must(request.getHeader("Content-Length")) == str(len(must(request.post_body)))
+ assert request.args[b"firstName"] == [b"John"]
+ assert request.args[b"lastName"] == [b"Doe"]
+ assert request.args[b"file"][0] == file["buffer"]
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_add_default_headers(
+ context: BrowserContext, page: Page, server: Server
+) -> None:
+ [request, response] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ context.request.get(server.EMPTY_PAGE),
+ )
+ assert request.getHeader("Accept") == "*/*"
+ assert request.getHeader("Accept-Encoding") == "gzip,deflate,br"
+ assert request.getHeader("User-Agent") == await page.evaluate("() => navigator.userAgent")
+
+
+async def test_should_work_after_context_dispose(context: BrowserContext, server: Server) -> None:
+ await context.close(reason="Test ended.")
+ with pytest.raises(Error, match="Test ended."):
+ await context.request.get(server.EMPTY_PAGE)
+
+
+async def test_should_retry_ECONNRESET(context: BrowserContext, server: Server) -> None:
+ request_count = 0
+
+ def _handle_request(req: TestServerRequest) -> None:
+ nonlocal request_count
+ request_count += 1
+ if request_count <= 3:
+ assert req.transport
+ req.transport.abortConnection()
+ return
+ req.setHeader("content-type", "text/plain")
+ req.write(b"Hello!")
+ req.finish()
+
+ server.set_route("/test", _handle_request)
+ response = await context.request.fetch(server.PREFIX + "/test", max_retries=3)
+ assert response.status == 200
+ assert await response.text() == "Hello!"
+ assert request_count == 4
diff --git a/tests/async/test_fetch_global.py b/tests/async/test_fetch_global.py
new file mode 100644
index 0000000..6a5e2d3
--- /dev/null
+++ b/tests/async/test_fetch_global.py
@@ -0,0 +1,472 @@
+# 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 base64
+import json
+import sys
+from pathlib import Path
+from typing import Any
+from urllib.parse import urlparse
+
+import pytest
+
+from playwright.async_api import APIResponse, Error, Playwright, StorageState
+from tests.server import Server, TestServerRequest
+
+
+@pytest.mark.parametrize("method", ["fetch", "delete", "get", "head", "patch", "post", "put"])
+async def test_should_work(playwright: Playwright, method: str, server: Server) -> None:
+ request = await playwright.request.new_context()
+ response: APIResponse = await getattr(request, method)(server.PREFIX + "/simple.json")
+ assert response.status == 200
+ assert response.status_text == "OK"
+ assert response.ok is True
+ assert response.url == server.PREFIX + "/simple.json"
+ assert response.headers["content-type"] == "application/json"
+ assert {
+ "name": "Content-Type",
+ "value": "application/json",
+ } in response.headers_array
+ assert await response.text() == ("" if method == "head" else '{"foo": "bar"}\n')
+
+
+async def test_should_dispose_global_request(playwright: Playwright, server: Server) -> None:
+ request = await playwright.request.new_context()
+ response = await request.get(server.PREFIX + "/simple.json")
+ assert await response.json() == {"foo": "bar"}
+ await response.dispose()
+ with pytest.raises(Error, match="Response has been disposed"):
+ await response.body()
+
+
+async def test_should_dispose_with_custom_error_message(
+ playwright: Playwright, server: Server
+) -> None:
+ request = await playwright.request.new_context()
+ await request.dispose(reason="My reason")
+ with pytest.raises(Error, match="My reason"):
+ await request.get(server.EMPTY_PAGE)
+
+
+async def test_should_support_global_user_agent_option(
+ playwright: Playwright, server: Server
+) -> None:
+ api_request_context = await playwright.request.new_context(user_agent="My Agent")
+ response = await api_request_context.get(server.PREFIX + "/empty.html")
+ [request, _] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ api_request_context.get(server.EMPTY_PAGE),
+ )
+ assert response.ok is True
+ assert response.url == server.EMPTY_PAGE
+ assert request.getHeader("user-agent") == "My Agent"
+
+
+async def test_should_support_global_timeout_option(playwright: Playwright, server: Server) -> None:
+ request = await playwright.request.new_context(timeout=100)
+ server.set_route("/empty.html", lambda req: None)
+ with pytest.raises(Error, match="Request timed out after 100ms"):
+ await request.get(server.EMPTY_PAGE)
+
+
+async def test_should_propagate_extra_http_headers_with_redirects(
+ playwright: Playwright, server: Server
+) -> None:
+ server.set_redirect("/a/redirect1", "/b/c/redirect2")
+ server.set_redirect("/b/c/redirect2", "/simple.json")
+ request = await playwright.request.new_context(extra_http_headers={"My-Secret": "Value"})
+ [req1, req2, req3, _] = await asyncio.gather(
+ server.wait_for_request("/a/redirect1"),
+ server.wait_for_request("/b/c/redirect2"),
+ server.wait_for_request("/simple.json"),
+ request.get(f"{server.PREFIX}/a/redirect1"),
+ )
+ assert req1.getHeader("my-secret") == "Value"
+ assert req2.getHeader("my-secret") == "Value"
+ assert req3.getHeader("my-secret") == "Value"
+
+
+async def test_should_support_global_http_credentials_option(
+ playwright: Playwright, server: Server
+) -> None:
+ server.set_auth("/empty.html", "user", "pass")
+ request1 = await playwright.request.new_context()
+ response1 = await request1.get(server.EMPTY_PAGE)
+ assert response1.status == 401
+ await response1.dispose()
+
+ request2 = await playwright.request.new_context(
+ http_credentials={"username": "user", "password": "pass"}
+ )
+ response2 = await request2.get(server.EMPTY_PAGE)
+ assert response2.status == 200
+ assert response2.ok is True
+ await response2.dispose()
+
+
+async def test_should_return_error_with_wrong_credentials(
+ playwright: Playwright, server: Server
+) -> None:
+ server.set_auth("/empty.html", "user", "pass")
+ request = await playwright.request.new_context(
+ http_credentials={"username": "user", "password": "wrong"}
+ )
+ response = await request.get(server.EMPTY_PAGE)
+ assert response.status == 401
+ assert response.ok is False
+
+
+async def test_should_work_with_correct_credentials_and_matching_origin(
+ playwright: Playwright, server: Server
+) -> None:
+ server.set_auth("/empty.html", "user", "pass")
+ request = await playwright.request.new_context(
+ http_credentials={
+ "username": "user",
+ "password": "pass",
+ "origin": server.PREFIX,
+ }
+ )
+ response = await request.get(server.EMPTY_PAGE)
+ assert response.status == 200
+ await response.dispose()
+
+
+async def test_should_work_with_correct_credentials_and_matching_origin_case_insensitive(
+ playwright: Playwright, server: Server
+) -> None:
+ server.set_auth("/empty.html", "user", "pass")
+ request = await playwright.request.new_context(
+ http_credentials={
+ "username": "user",
+ "password": "pass",
+ "origin": server.PREFIX.upper(),
+ }
+ )
+ response = await request.get(server.EMPTY_PAGE)
+ assert response.status == 200
+ await response.dispose()
+
+
+async def test_should_return_error_with_correct_credentials_and_mismatching_scheme(
+ playwright: Playwright, server: Server
+) -> None:
+ server.set_auth("/empty.html", "user", "pass")
+ request = await playwright.request.new_context(
+ http_credentials={
+ "username": "user",
+ "password": "pass",
+ "origin": server.PREFIX.replace("http://", "https://"),
+ }
+ )
+ response = await request.get(server.EMPTY_PAGE)
+ assert response.status == 401
+ await response.dispose()
+
+
+async def test_should_return_error_with_correct_credentials_and_mismatching_hostname(
+ playwright: Playwright, server: Server
+) -> None:
+ server.set_auth("/empty.html", "user", "pass")
+ hostname = urlparse(server.PREFIX).hostname
+ assert hostname
+ origin = server.PREFIX.replace(hostname, "mismatching-hostname")
+ request = await playwright.request.new_context(
+ http_credentials={"username": "user", "password": "pass", "origin": origin}
+ )
+ response = await request.get(server.EMPTY_PAGE)
+ assert response.status == 401
+ await response.dispose()
+
+
+async def test_should_return_error_with_correct_credentials_and_mismatching_port(
+ playwright: Playwright, server: Server
+) -> None:
+ server.set_auth("/empty.html", "user", "pass")
+ origin = server.PREFIX.replace(str(server.PORT), str(server.PORT + 1))
+ request = await playwright.request.new_context(
+ http_credentials={"username": "user", "password": "pass", "origin": origin}
+ )
+ response = await request.get(server.EMPTY_PAGE)
+ assert response.status == 401
+ await response.dispose()
+
+
+async def test_support_http_credentials_send_immediately(
+ playwright: Playwright, server: Server
+) -> None:
+ request = await playwright.request.new_context(
+ http_credentials={
+ "username": "user",
+ "password": "pass",
+ "origin": server.PREFIX.upper(),
+ "send": "always",
+ }
+ )
+ server_request, response = await asyncio.gather(
+ server.wait_for_request("/empty.html"), request.get(server.EMPTY_PAGE)
+ )
+ assert (
+ server_request.getHeader("authorization")
+ == "Basic " + base64.b64encode(b"user:pass").decode()
+ )
+ assert response.status == 200
+
+ server_request, response = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ request.get(server.CROSS_PROCESS_PREFIX + "/empty.html"),
+ )
+ # Not sent to another origin.
+ assert server_request.getHeader("authorization") is None
+ assert response.status == 200
+
+
+async def test_should_support_global_ignore_https_errors_option(
+ playwright: Playwright, https_server: Server
+) -> None:
+ request = await playwright.request.new_context(ignore_https_errors=True)
+ response = await request.get(https_server.EMPTY_PAGE)
+ assert response.status == 200
+ assert response.ok is True
+ assert response.url == https_server.EMPTY_PAGE
+ await response.dispose()
+
+
+async def test_should_resolve_url_relative_to_global_base_url_option(
+ playwright: Playwright, server: Server
+) -> None:
+ request = await playwright.request.new_context(base_url=server.PREFIX)
+ response = await request.get("/empty.html")
+ assert response.status == 200
+ assert response.ok is True
+ assert response.url == server.EMPTY_PAGE
+ await response.dispose()
+
+
+async def test_should_use_playwright_as_a_user_agent(
+ playwright: Playwright, server: Server
+) -> None:
+ request = await playwright.request.new_context()
+ [server_req, _] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ request.get(server.EMPTY_PAGE),
+ )
+ assert str(server_req.getHeader("User-Agent")).startswith("Playwright/")
+ await request.dispose()
+
+
+async def test_should_return_empty_body(playwright: Playwright, server: Server) -> None:
+ request = await playwright.request.new_context()
+ response = await request.get(server.EMPTY_PAGE)
+ body = await response.body()
+ assert len(body) == 0
+ assert await response.text() == ""
+ await request.dispose()
+ with pytest.raises(Error, match="Response has been disposed"):
+ await response.body()
+
+
+async def test_storage_state_should_round_trip_through_file(
+ playwright: Playwright, tmpdir: Path
+) -> None:
+ expected: StorageState = {
+ "cookies": [
+ {
+ "name": "a",
+ "value": "b",
+ "domain": "a.b.one.com",
+ "path": "/",
+ "expires": -1,
+ "httpOnly": False,
+ "secure": False,
+ "sameSite": "Lax",
+ }
+ ],
+ "origins": [],
+ }
+ request = await playwright.request.new_context(storage_state=expected)
+ path = tmpdir / "storage-state.json"
+ actual = await request.storage_state(path=path)
+ assert actual == expected
+
+ written = path.read_text("utf8")
+ assert json.loads(written) == expected
+
+ request2 = await playwright.request.new_context(storage_state=path)
+ state2 = await request2.storage_state()
+ assert state2 == expected
+
+
+serialization_data = [
+ [{"foo": "bar"}],
+ [["foo", "bar", 2021]],
+ ["foo"],
+ [True],
+ [2021],
+]
+
+
+@pytest.mark.parametrize("serialization", serialization_data)
+async def test_should_json_stringify_body_when_content_type_is_application_json(
+ playwright: Playwright, server: Server, serialization: Any
+) -> None:
+ request = await playwright.request.new_context()
+ [req, _] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ request.post(
+ server.EMPTY_PAGE,
+ headers={"content-type": "application/json"},
+ data=serialization,
+ ),
+ )
+ body = req.post_body
+ assert body
+ assert body.decode() == json.dumps(serialization)
+ await request.dispose()
+
+
+@pytest.mark.parametrize("serialization", serialization_data)
+async def test_should_not_double_stringify_body_when_content_type_is_application_json(
+ playwright: Playwright, server: Server, serialization: Any
+) -> None:
+ request = await playwright.request.new_context()
+ stringified_value = json.dumps(serialization)
+ [req, _] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ request.post(
+ server.EMPTY_PAGE,
+ headers={"content-type": "application/json"},
+ data=stringified_value,
+ ),
+ )
+
+ body = req.post_body
+ assert body
+ assert body.decode() == stringified_value
+ await request.dispose()
+
+
+async def test_should_accept_already_serialized_data_as_bytes_when_content_type_is_application_json(
+ playwright: Playwright, server: Server
+) -> None:
+ request = await playwright.request.new_context()
+ stringified_value = json.dumps({"foo": "bar"}).encode()
+ [req, _] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ request.post(
+ server.EMPTY_PAGE,
+ headers={"content-type": "application/json"},
+ data=stringified_value,
+ ),
+ )
+ body = req.post_body
+ assert body == stringified_value
+ await request.dispose()
+
+
+async def test_should_contain_default_user_agent(playwright: Playwright, server: Server) -> None:
+ request = await playwright.request.new_context()
+ [server_request, _] = await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ request.get(server.EMPTY_PAGE),
+ )
+ user_agent = server_request.getHeader("user-agent")
+ assert user_agent
+ assert "python" in user_agent
+ assert f"{sys.version_info.major}.{sys.version_info.minor}" in user_agent
+
+
+async def test_should_throw_an_error_when_max_redirects_is_exceeded(
+ playwright: Playwright, server: Server
+) -> None:
+ server.set_redirect("/a/redirect1", "/b/c/redirect2")
+ server.set_redirect("/b/c/redirect2", "/b/c/redirect3")
+ server.set_redirect("/b/c/redirect3", "/b/c/redirect4")
+ server.set_redirect("/b/c/redirect4", "/simple.json")
+
+ request = await playwright.request.new_context()
+ for method in ["GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH"]:
+ for max_redirects in [1, 2, 3]:
+ with pytest.raises(Error) as exc_info:
+ await request.fetch(
+ server.PREFIX + "/a/redirect1",
+ method=method,
+ max_redirects=max_redirects,
+ )
+ assert "Max redirect count exceeded" in str(exc_info)
+
+
+async def test_should_not_follow_redirects_when_max_redirects_is_set_to_0(
+ playwright: Playwright, server: Server
+) -> None:
+ server.set_redirect("/a/redirect1", "/b/c/redirect2")
+ server.set_redirect("/b/c/redirect2", "/simple.json")
+
+ request = await playwright.request.new_context()
+ for method in ["GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH"]:
+ response = await request.fetch(
+ server.PREFIX + "/a/redirect1", method=method, max_redirects=0
+ )
+ assert response.headers["location"] == "/b/c/redirect2"
+ assert response.status == 302
+
+
+async def test_should_throw_an_error_when_max_redirects_is_less_than_0(
+ playwright: Playwright,
+ server: Server,
+) -> None:
+ request = await playwright.request.new_context()
+ for method in ["GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH"]:
+ with pytest.raises(AssertionError) as exc_info:
+ await request.fetch(server.PREFIX + "/a/redirect1", method=method, max_redirects=-1)
+ assert "'max_redirects' must be greater than or equal to '0'" in str(exc_info)
+
+
+async def test_should_serialize_request_data(playwright: Playwright, server: Server) -> None:
+ request = await playwright.request.new_context()
+ server.set_route("/echo", lambda req: (req.write(req.post_body), req.finish()))
+ for data, expected in [
+ ({"foo": None}, '{"foo": null}'),
+ ([], "[]"),
+ ({}, "{}"),
+ ("", ""),
+ ]:
+ response = await request.post(server.PREFIX + "/echo", data=data)
+ assert response.status == 200
+ assert await response.text() == expected
+ await request.dispose()
+
+
+async def test_should_retry_ECONNRESET(playwright: Playwright, server: Server) -> None:
+ request_count = 0
+
+ def _handle_request(req: TestServerRequest) -> None:
+ nonlocal request_count
+ request_count += 1
+ if request_count <= 3:
+ assert req.transport
+ req.transport.abortConnection()
+ return
+ req.setHeader("content-type", "text/plain")
+ req.write(b"Hello!")
+ req.finish()
+
+ server.set_route("/test", _handle_request)
+ request = await playwright.request.new_context()
+ response = await request.fetch(server.PREFIX + "/test", max_retries=3)
+ assert response.status == 200
+ assert await response.text() == "Hello!"
+ assert request_count == 4
+ await request.dispose()
diff --git a/tests/async/test_fill.py.disabled b/tests/async/test_fill.py.disabled
new file mode 100644
index 0000000..4dd6db3
--- /dev/null
+++ b/tests/async/test_fill.py.disabled
@@ -0,0 +1,31 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from playwright.async_api import Page
+from tests.server import Server
+
+
+async def test_fill_textarea(page: Page, server: Server) -> None:
+ await page.goto(f"{server.PREFIX}/input/textarea.html")
+ await page.fill("textarea", "some value")
+ assert await page.evaluate("result") == "some value"
+
+
+#
+
+
+async def test_fill_input(page: Page, server: Server) -> None:
+ await page.goto(f"{server.PREFIX}/input/textarea.html")
+ await page.fill("input", "some value")
+ assert await page.evaluate("result") == "some value"
diff --git a/tests/async/test_focus.py.disabled b/tests/async/test_focus.py.disabled
new file mode 100644
index 0000000..72698ea
--- /dev/null
+++ b/tests/async/test_focus.py.disabled
@@ -0,0 +1,110 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from playwright.async_api import Page
+
+
+async def test_should_work(page: Page) -> None:
+ await page.set_content("
")
+ assert await page.evaluate("() => document.activeElement.nodeName") == "BODY"
+ await page.focus("#d1")
+ assert await page.evaluate("() => document.activeElement.id") == "d1"
+
+
+async def test_should_emit_focus_event(page: Page) -> None:
+ await page.set_content("
")
+ focused = []
+ await page.expose_function("focusEvent", lambda: focused.append(True))
+ await page.evaluate("() => d1.addEventListener('focus', focusEvent)")
+ await page.focus("#d1")
+ assert focused == [True]
+
+
+async def test_should_emit_blur_event(page: Page) -> None:
+ await page.set_content(
+ "DIV1
DIV2
"
+ )
+ await page.focus("#d1")
+ focused = []
+ blurred = []
+ await page.expose_function("focusEvent", lambda: focused.append(True))
+ await page.expose_function("blurEvent", lambda: blurred.append(True))
+ await page.evaluate("() => d1.addEventListener('blur', blurEvent)")
+ await page.evaluate("() => d2.addEventListener('focus', focusEvent)")
+ await page.focus("#d2")
+ assert focused == [True]
+ assert blurred == [True]
+
+
+async def test_should_traverse_focus(page: Page) -> None:
+ await page.set_content(' ')
+ focused = []
+ await page.expose_function("focusEvent", lambda: focused.append(True))
+ await page.evaluate("() => i2.addEventListener('focus', focusEvent)")
+
+ await page.focus("#i1")
+ await page.keyboard.type("First")
+ await page.keyboard.press("Tab")
+ await page.keyboard.type("Last")
+
+ assert focused == [True]
+ assert await page.eval_on_selector("#i1", "e => e.value") == "First"
+ assert await page.eval_on_selector("#i2", "e => e.value") == "Last"
+
+
+async def test_should_traverse_focus_in_all_directions(page: Page) -> None:
+ await page.set_content(' ')
+ await page.keyboard.press("Tab")
+ assert await page.evaluate("() => document.activeElement.value") == "1"
+ await page.keyboard.press("Tab")
+ assert await page.evaluate("() => document.activeElement.value") == "2"
+ await page.keyboard.press("Tab")
+ assert await page.evaluate("() => document.activeElement.value") == "3"
+ await page.keyboard.press("Shift+Tab")
+ assert await page.evaluate("() => document.activeElement.value") == "2"
+ await page.keyboard.press("Shift+Tab")
+ assert await page.evaluate("() => document.activeElement.value") == "1"
+
+
+@pytest.mark.only_platform("darwin")
+@pytest.mark.only_browser("webkit")
+async def test_should_traverse_only_form_elements(page: Page) -> None:
+ await page.set_content(
+ """
+
+ button
+ link
+
+ """
+ )
+ await page.keyboard.press("Tab")
+ assert await page.evaluate("() => document.activeElement.id") == "input-1"
+ await page.keyboard.press("Tab")
+ assert await page.evaluate("() => document.activeElement.id") == "input-2"
+ await page.keyboard.press("Shift+Tab")
+ assert await page.evaluate("() => document.activeElement.id") == "input-1"
+ await page.keyboard.press("Alt+Tab")
+ assert await page.evaluate("() => document.activeElement.id") == "button"
+ await page.keyboard.press("Alt+Tab")
+ assert await page.evaluate("() => document.activeElement.id") == "link"
+ await page.keyboard.press("Alt+Tab")
+ assert await page.evaluate("() => document.activeElement.id") == "input-2"
+ await page.keyboard.press("Alt+Shift+Tab")
+ assert await page.evaluate("() => document.activeElement.id") == "link"
+ await page.keyboard.press("Alt+Shift+Tab")
+ assert await page.evaluate("() => document.activeElement.id") == "button"
+ await page.keyboard.press("Alt+Shift+Tab")
+ assert await page.evaluate("() => document.activeElement.id") == "input-1"
diff --git a/tests/async/test_frames.py b/tests/async/test_frames.py
new file mode 100644
index 0000000..8deb70c
--- /dev/null
+++ b/tests/async/test_frames.py
@@ -0,0 +1,280 @@
+# 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
+from typing import Optional
+
+import pytest
+
+from playwright.async_api import Error, Page
+from tests.server import Server
+
+from .utils import Utils
+
+
+async def test_evaluate_handle(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ main_frame = page.main_frame
+ assert main_frame.page == page
+ window_handle = await main_frame.evaluate_handle("window")
+ assert window_handle
+
+
+async def test_frame_element(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ assert frame1
+ await utils.attach_frame(page, "frame2", server.EMPTY_PAGE)
+ frame3 = await utils.attach_frame(page, "frame3", server.EMPTY_PAGE)
+ assert frame3
+ frame1handle1 = await page.query_selector("#frame1")
+ assert frame1handle1
+ frame1handle2 = await frame1.frame_element()
+ frame3handle1 = await page.query_selector("#frame3")
+ assert frame3handle1
+ frame3handle2 = await frame3.frame_element()
+ assert await frame1handle1.evaluate("(a, b) => a === b", frame1handle2)
+ assert await frame3handle1.evaluate("(a, b) => a === b", frame3handle2)
+ assert await frame1handle1.evaluate("(a, b) => a === b", frame3handle1) is False
+
+
+async def test_frame_element_with_content_frame(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ frame = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ handle = await frame.frame_element()
+ content_frame = await handle.content_frame()
+ assert content_frame == frame
+
+
+async def test_frame_element_throw_when_detached(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ await page.eval_on_selector("#frame1", "e => e.remove()")
+ error: Optional[Error] = None
+ try:
+ await frame1.frame_element()
+ except Error as e:
+ error = e
+ assert error
+ assert error.message == "Frame.frame_element: Frame has been detached."
+
+
+async def test_evaluate_throw_for_detached_frames(page: Page, server: Server, utils: Utils) -> None:
+ frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ assert frame1
+ await utils.detach_frame(page, "frame1")
+ error: Optional[Error] = None
+ try:
+ await frame1.evaluate("7 * 8")
+ except Error as e:
+ error = e
+ assert error
+ assert "Frame was detached" in error.message
+
+
+async def test_evaluate_isolated_between_frames(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ assert len(page.frames) == 2
+ [frame1, frame2] = page.frames
+ assert frame1 != frame2
+
+ await asyncio.gather(frame1.evaluate("window.a = 1"), frame2.evaluate("window.a = 2"))
+ [a1, a2] = await asyncio.gather(frame1.evaluate("window.a"), frame2.evaluate("window.a"))
+ assert a1 == 1
+ assert a2 == 2
+
+
+async def test_should_handle_nested_frames(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.PREFIX + "/frames/nested-frames.html")
+ assert utils.dump_frames(page.main_frame) == [
+ "http://localhost:/frames/nested-frames.html",
+ " http://localhost:/frames/frame.html (aframe)",
+ " http://localhost:/frames/two-frames.html (2frames)",
+ " http://localhost:/frames/frame.html (dos)",
+ " http://localhost:/frames/frame.html (uno)",
+ ]
+
+
+async def test_should_send_events_when_frames_are_manipulated_dynamically(
+ page: Page, server: Server, utils: Utils
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ # validate frameattached events
+ attached_frames = []
+ page.on("frameattached", lambda frame: attached_frames.append(frame))
+ await utils.attach_frame(page, "frame1", "./assets/frame.html")
+ assert len(attached_frames) == 1
+ assert "/assets/frame.html" in attached_frames[0].url
+
+ # validate framenavigated events
+ navigated_frames = []
+ page.on("framenavigated", lambda frame: navigated_frames.append(frame))
+ await page.evaluate(
+ """() => {
+ frame = document.getElementById('frame1')
+ frame.src = './empty.html'
+ return new Promise(x => frame.onload = x)
+ }"""
+ )
+
+ assert len(navigated_frames) == 1
+ assert navigated_frames[0].url == server.EMPTY_PAGE
+
+ # validate framedetached events
+ detached_frames = []
+ page.on("framedetached", lambda frame: detached_frames.append(frame))
+ await utils.detach_frame(page, "frame1")
+ assert len(detached_frames) == 1
+ assert detached_frames[0].is_detached()
+
+
+async def test_framenavigated_when_navigating_on_anchor_urls(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_event("framenavigated"):
+ await page.goto(server.EMPTY_PAGE + "#foo")
+ assert page.url == server.EMPTY_PAGE + "#foo"
+
+
+async def test_persist_main_frame_on_cross_process_navigation(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ main_frame = page.main_frame
+ await page.goto(server.CROSS_PROCESS_PREFIX + "/empty.html")
+ assert page.main_frame == main_frame
+
+
+async def test_should_not_send_attach_detach_events_for_main_frame(
+ page: Page, server: Server
+) -> None:
+ has_events = []
+ page.on("frameattached", lambda frame: has_events.append(True))
+ page.on("framedetached", lambda frame: has_events.append(True))
+ await page.goto(server.EMPTY_PAGE)
+ assert has_events == []
+
+
+async def test_detach_child_frames_on_navigation(page: Page, server: Server) -> None:
+ attached_frames = []
+ detached_frames = []
+ navigated_frames = []
+ page.on("frameattached", lambda frame: attached_frames.append(frame))
+ page.on("framedetached", lambda frame: detached_frames.append(frame))
+ page.on("framenavigated", lambda frame: navigated_frames.append(frame))
+ await page.goto(server.PREFIX + "/frames/nested-frames.html")
+ assert len(attached_frames) == 4
+ assert len(detached_frames) == 0
+ assert len(navigated_frames) == 5
+
+ attached_frames = []
+ detached_frames = []
+ navigated_frames = []
+ await page.goto(server.EMPTY_PAGE)
+ assert len(attached_frames) == 0
+ assert len(detached_frames) == 4
+ assert len(navigated_frames) == 1
+
+
+async def test_framesets(page: Page, server: Server) -> None:
+ attached_frames = []
+ detached_frames = []
+ navigated_frames = []
+ page.on("frameattached", lambda frame: attached_frames.append(frame))
+ page.on("framedetached", lambda frame: detached_frames.append(frame))
+ page.on("framenavigated", lambda frame: navigated_frames.append(frame))
+ await page.goto(server.PREFIX + "/frames/frameset.html")
+ assert len(attached_frames) == 4
+ assert len(detached_frames) == 0
+ assert len(navigated_frames) == 5
+
+ attached_frames = []
+ detached_frames = []
+ navigated_frames = []
+ await page.goto(server.EMPTY_PAGE)
+ assert len(attached_frames) == 0
+ assert len(detached_frames) == 4
+ assert len(navigated_frames) == 1
+
+
+async def test_frame_from_inside_shadow_dom(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/shadow.html")
+ await page.evaluate(
+ """async url => {
+ frame = document.createElement('iframe');
+ frame.src = url;
+ document.body.shadowRoot.appendChild(frame);
+ await new Promise(x => frame.onload = x);
+ }""",
+ server.EMPTY_PAGE,
+ )
+ assert len(page.frames) == 2
+ assert page.frames[1].url == server.EMPTY_PAGE
+
+
+async def test_frame_name(page: Page, server: Server, utils: Utils) -> None:
+ await utils.attach_frame(page, "theFrameId", server.EMPTY_PAGE)
+ await page.evaluate(
+ """url => {
+ frame = document.createElement('iframe');
+ frame.name = 'theFrameName';
+ frame.src = url;
+ document.body.appendChild(frame);
+ return new Promise(x => frame.onload = x);
+ }""",
+ server.EMPTY_PAGE,
+ )
+ assert page.frames[0].name == ""
+ assert page.frames[1].name == "theFrameId"
+ assert page.frames[2].name == "theFrameName"
+
+
+async def test_frame_parent(page: Page, server: Server, utils: Utils) -> None:
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame2", server.EMPTY_PAGE)
+ assert page.frames[0].parent_frame is None
+ assert page.frames[1].parent_frame == page.main_frame
+ assert page.frames[2].parent_frame == page.main_frame
+
+
+async def test_should_report_different_frame_instance_when_frame_re_attaches(
+ page: Page, server: Server, utils: Utils
+) -> None:
+ frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ await page.evaluate(
+ """() => {
+ window.frame = document.querySelector('#frame1')
+ window.frame.remove()
+ }"""
+ )
+
+ assert frame1.is_detached()
+ async with page.expect_event("frameattached") as frame2_info:
+ await page.evaluate("() => document.body.appendChild(window.frame)")
+
+ frame2 = await frame2_info.value
+ assert frame2.is_detached() is False
+ assert frame1 != frame2
+
+
+async def test_strict_mode(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(
+ """
+ Hello
+ Hello
+ """
+ )
+ with pytest.raises(Error):
+ await page.text_content("button", strict=True)
+ with pytest.raises(Error):
+ await page.query_selector("button", strict=True)
diff --git a/tests/async/test_geolocation.py b/tests/async/test_geolocation.py
new file mode 100644
index 0000000..3e4832d
--- /dev/null
+++ b/tests/async/test_geolocation.py
@@ -0,0 +1,136 @@
+# 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 pytest
+from playwright.async_api import Browser, BrowserContext, Error, Page
+
+from tests.server import Server
+
+
+async def test_should_work(page: Page, server: Server, context: BrowserContext) -> None:
+ await context.grant_permissions(["geolocation"])
+ await page.goto(server.EMPTY_PAGE)
+ await context.set_geolocation({"latitude": 10, "longitude": 10})
+ 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_throw_when_invalid_longitude(context: BrowserContext) -> None:
+ with pytest.raises(Error) as exc:
+ await context.set_geolocation({"latitude": 10, "longitude": 200})
+ assert (
+ "geolocation.longitude: precondition -180 <= LONGITUDE <= 180 failed." in exc.value.message
+ )
+
+
+async def test_should_isolate_contexts(
+ page: Page, server: Server, context: BrowserContext, browser: Browser
+) -> None:
+ await context.grant_permissions(["geolocation"])
+ await context.set_geolocation({"latitude": 10, "longitude": 10})
+ await page.goto(server.EMPTY_PAGE)
+
+ context2 = await browser.new_context(
+ permissions=["geolocation"], geolocation={"latitude": 20, "longitude": 20}
+ )
+
+ page2 = await context2.new_page()
+ await page2.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}
+
+ geolocation2 = await page2.evaluate(
+ """() => new Promise(resolve => navigator.geolocation.getCurrentPosition(position => {
+ resolve({latitude: position.coords.latitude, longitude: position.coords.longitude})
+ }))"""
+ )
+ assert geolocation2 == {"latitude": 20, "longitude": 20}
+
+ await context2.close()
+
+
+async def test_should_use_context_options(browser: Browser, server: Server) -> None:
+ context = await browser.new_context(
+ geolocation={"latitude": 10, "longitude": 10}, permissions=["geolocation"]
+ )
+ page = await context.new_page()
+ 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}
+ await context.close()
+
+
+async def test_watch_position_should_be_notified(
+ page: Page, server: Server, context: BrowserContext
+) -> None:
+ await context.grant_permissions(["geolocation"])
+ await page.goto(server.EMPTY_PAGE)
+ messages = []
+ page.on("console", lambda message: messages.append(message.text))
+
+ await context.set_geolocation({"latitude": 0, "longitude": 0})
+ await page.evaluate(
+ """() => {
+ navigator.geolocation.watchPosition(pos => {
+ const coords = pos.coords;
+ console.log(`lat=${coords.latitude} lng=${coords.longitude}`);
+ }, err => {});
+ }"""
+ )
+
+ async with page.expect_console_message(lambda m: "lat=0 lng=10" in m.text):
+ await context.set_geolocation({"latitude": 0, "longitude": 10})
+
+ async with page.expect_console_message(lambda m: "lat=20 lng=30" in m.text):
+ await context.set_geolocation({"latitude": 20, "longitude": 30})
+
+ async with page.expect_console_message(lambda m: "lat=40 lng=50" in m.text):
+ await context.set_geolocation({"latitude": 40, "longitude": 50})
+
+ all_messages = "|".join(messages)
+ assert "lat=0 lng=10" in all_messages
+ assert "lat=20 lng=30" in all_messages
+ assert "lat=40 lng=50" in all_messages
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_use_context_options_for_popup(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ await context.grant_permissions(["geolocation"])
+ await context.set_geolocation({"latitude": 10, "longitude": 10})
+ async with page.expect_popup() as popup_info:
+ await page.evaluate(
+ "url => window._popup = window.open(url)",
+ server.PREFIX + "/geolocation.html",
+ )
+ popup = await popup_info.value
+ await popup.wait_for_load_state()
+ geolocation = await popup.evaluate("() => window.geolocationPromise")
+ assert geolocation == {"latitude": 10, "longitude": 10}
diff --git a/tests/async/test_har.py b/tests/async/test_har.py
new file mode 100644
index 0000000..7ad7f0d
--- /dev/null
+++ b/tests/async/test_har.py
@@ -0,0 +1,750 @@
+# 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 json
+import os
+import re
+import zipfile
+from pathlib import Path
+from typing import Awaitable, Callable, cast
+
+import pytest
+from playwright.async_api import Browser, BrowserContext, Error, Page, Route, expect
+
+from tests.server import Server, TestServerRequest
+from tests.utils import must
+
+
+async def test_should_work(browser: Browser, server: Server, tmpdir: Path) -> None:
+ path = os.path.join(tmpdir, "log.har")
+ context = await browser.new_context(record_har_path=path)
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ await context.close()
+ with open(path) as f:
+ data = json.load(f)
+ assert "log" in data
+
+
+async def test_should_omit_content(browser: Browser, server: Server, tmpdir: Path) -> None:
+ path = os.path.join(tmpdir, "log.har")
+ context = await browser.new_context(
+ record_har_path=path,
+ record_har_content="omit",
+ )
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/har.html")
+ await context.close()
+ with open(path) as f:
+ data = json.load(f)
+ assert "log" in data
+ log = data["log"]
+ content1 = log["entries"][0]["response"]["content"]
+ assert "text" not in content1
+ assert "encoding" not in content1
+
+
+async def test_should_omit_content_legacy(browser: Browser, server: Server, tmpdir: Path) -> None:
+ path = os.path.join(tmpdir, "log.har")
+ context = await browser.new_context(record_har_path=path, record_har_omit_content=True)
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/har.html")
+ await context.close()
+ with open(path) as f:
+ data = json.load(f)
+ assert "log" in data
+ log = data["log"]
+ content1 = log["entries"][0]["response"]["content"]
+ assert "text" not in content1
+ assert "encoding" not in content1
+
+
+async def test_should_attach_content(browser: Browser, server: Server, tmpdir: Path) -> None:
+ path = os.path.join(tmpdir, "log.har.zip")
+ context = await browser.new_context(
+ record_har_path=path,
+ record_har_content="attach",
+ )
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/har.html")
+ await page.evaluate("() => fetch('/pptr.png').then(r => r.arrayBuffer())")
+ await context.close()
+ with zipfile.ZipFile(path) as z:
+ with z.open("har.har") as har:
+ entries = json.load(har)["log"]["entries"]
+
+ assert "encoding" not in entries[0]["response"]["content"]
+ assert entries[0]["response"]["content"]["mimeType"] == "text/html; charset=utf-8"
+ assert (
+ "75841480e2606c03389077304342fac2c58ccb1b"
+ in entries[0]["response"]["content"]["_file"]
+ )
+ assert entries[0]["response"]["content"]["size"] >= 96
+ assert entries[0]["response"]["content"]["compression"] == 0
+
+ assert "encoding" not in entries[1]["response"]["content"]
+ assert entries[1]["response"]["content"]["mimeType"] == "text/css; charset=utf-8"
+ assert (
+ "79f739d7bc88e80f55b9891a22bf13a2b4e18adb"
+ in entries[1]["response"]["content"]["_file"]
+ )
+ assert entries[1]["response"]["content"]["size"] >= 37
+ assert entries[1]["response"]["content"]["compression"] == 0
+
+ assert "encoding" not in entries[2]["response"]["content"]
+ assert entries[2]["response"]["content"]["mimeType"] == "image/png"
+ assert (
+ "a4c3a18f0bb83f5d9fe7ce561e065c36205762fa"
+ in entries[2]["response"]["content"]["_file"]
+ )
+ assert entries[2]["response"]["content"]["size"] >= 6000
+ assert entries[2]["response"]["content"]["compression"] == 0
+
+ with z.open("75841480e2606c03389077304342fac2c58ccb1b.html") as f:
+ assert b"HAR Page" in f.read()
+
+ with z.open("79f739d7bc88e80f55b9891a22bf13a2b4e18adb.css") as f:
+ assert b"pink" in f.read()
+
+ with z.open("a4c3a18f0bb83f5d9fe7ce561e065c36205762fa.png") as f:
+ assert len(f.read()) == entries[2]["response"]["content"]["size"]
+
+
+async def test_should_not_omit_content(browser: Browser, server: Server, tmpdir: Path) -> None:
+ path = os.path.join(tmpdir, "log.har")
+ context = await browser.new_context(record_har_path=path, record_har_omit_content=False)
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/har.html")
+ await context.close()
+ with open(path) as f:
+ data = json.load(f)
+ content1 = data["log"]["entries"][0]["response"]["content"]
+ assert "text" in content1
+
+
+async def test_should_include_content(browser: Browser, server: Server, tmpdir: Path) -> None:
+ path = os.path.join(tmpdir, "log.har")
+ context = await browser.new_context(record_har_path=path)
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/har.html")
+ await context.close()
+ with open(path) as f:
+ data = json.load(f)
+ assert "log" in data
+ log = data["log"]
+
+ content1 = log["entries"][0]["response"]["content"]
+ assert content1["mimeType"] == "text/html; charset=utf-8"
+ assert "HAR Page" in content1["text"]
+
+
+async def test_should_default_to_full_mode(browser: Browser, server: Server, tmpdir: Path) -> None:
+ path = os.path.join(tmpdir, "log.har")
+ context = await browser.new_context(
+ record_har_path=path,
+ )
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/har.html")
+ await context.close()
+ with open(path) as f:
+ data = json.load(f)
+ assert "log" in data
+ log = data["log"]
+ assert log["entries"][0]["request"]["bodySize"] >= 0
+
+
+async def test_should_support_minimal_mode(browser: Browser, server: Server, tmpdir: Path) -> None:
+ path = os.path.join(tmpdir, "log.har")
+ context = await browser.new_context(
+ record_har_path=path,
+ record_har_mode="minimal",
+ )
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/har.html")
+ await context.close()
+ with open(path) as f:
+ data = json.load(f)
+ assert "log" in data
+ log = data["log"]
+ assert log["entries"][0]["request"]["bodySize"] == -1
+
+
+async def test_should_filter_by_glob(browser: Browser, server: Server, tmpdir: str) -> None:
+ path = os.path.join(tmpdir, "log.har")
+ context = await browser.new_context(
+ base_url=server.PREFIX,
+ record_har_path=path,
+ record_har_url_filter="/*.css",
+ ignore_https_errors=True,
+ )
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/har.html")
+ await context.close()
+ with open(path) as f:
+ data = json.load(f)
+ assert "log" in data
+ log = data["log"]
+ assert len(log["entries"]) == 1
+ assert log["entries"][0]["request"]["url"].endswith("one-style.css")
+
+
+async def test_should_filter_by_regexp(browser: Browser, server: Server, tmpdir: str) -> None:
+ path = os.path.join(tmpdir, "log.har")
+ context = await browser.new_context(
+ base_url=server.PREFIX,
+ record_har_path=path,
+ record_har_url_filter=re.compile("HAR.X?HTML", re.I),
+ ignore_https_errors=True,
+ )
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/har.html")
+ await context.close()
+ with open(path) as f:
+ data = json.load(f)
+ assert "log" in data
+ log = data["log"]
+ assert len(log["entries"]) == 1
+ assert log["entries"][0]["request"]["url"].endswith("har.html")
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_context_route_from_har_matching_the_method_and_following_redirects(
+ context: BrowserContext, assetdir: Path
+) -> None:
+ await context.route_from_har(har=assetdir / "har-fulfill.har")
+ page = await context.new_page()
+ await page.goto("http://no.playwright/")
+ # HAR contains a redirect for the script that should be followed automatically.
+ assert await page.evaluate("window.value") == "foo"
+ # HAR contains a POST for the css file that should not be used.
+ await expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)")
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_page_route_from_har_matching_the_method_and_following_redirects(
+ page: Page, assetdir: Path
+) -> None:
+ await page.route_from_har(har=assetdir / "har-fulfill.har")
+ await page.goto("http://no.playwright/")
+ # HAR contains a redirect for the script that should be followed automatically.
+ assert await page.evaluate("window.value") == "foo"
+ # HAR contains a POST for the css file that should not be used.
+ await expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)")
+
+
+async def test_fallback_continue_should_continue_when_not_found_in_har(
+ context: BrowserContext, server: Server, assetdir: Path
+) -> None:
+ await context.route_from_har(har=assetdir / "har-fulfill.har", not_found="fallback")
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/one-style.html")
+ await expect(page.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
+
+
+async def test_by_default_should_abort_requests_not_found_in_har(
+ context: BrowserContext,
+ server: Server,
+ assetdir: Path,
+ is_chromium: bool,
+ is_webkit: bool,
+) -> None:
+ await context.route_from_har(har=assetdir / "har-fulfill.har")
+ page = await context.new_page()
+
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.EMPTY_PAGE)
+ assert exc_info.value
+ if is_chromium:
+ assert "net::ERR_FAILED" in exc_info.value.message
+ elif is_webkit:
+ assert "Blocked by Web Inspector" in exc_info.value.message
+ else:
+ assert "NS_ERROR_FAILURE" in exc_info.value.message
+
+
+async def test_fallback_continue_should_continue_requests_on_bad_har(
+ context: BrowserContext, server: Server, tmpdir: Path
+) -> None:
+ path_to_invalid_har = tmpdir / "invalid.har"
+ with path_to_invalid_har.open("w") as f:
+ json.dump({"log": {}}, f)
+ await context.route_from_har(har=path_to_invalid_har, not_found="fallback")
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/one-style.html")
+ await expect(page.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_only_handle_requests_matching_url_filter(
+ context: BrowserContext, server: Server, assetdir: Path
+) -> None:
+ await context.route_from_har(
+ har=assetdir / "har-fulfill.har", not_found="fallback", url="**/*.js"
+ )
+ page = await context.new_page()
+
+ async def handler(route: Route) -> None:
+ assert route.request.url == "http://no.playwright/"
+ await route.fulfill(
+ status=200,
+ content_type="text/html",
+ body='hello
',
+ )
+
+ await context.route("http://no.playwright/", handler)
+ await page.goto("http://no.playwright/")
+ assert await page.evaluate("window.value") == "foo"
+ await expect(page.locator("body")).to_have_css("background-color", "rgba(0, 0, 0, 0)")
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_only_handle_requests_matching_url_filter_no_fallback(
+ context: BrowserContext, server: Server, assetdir: Path
+) -> None:
+ await context.route_from_har(har=assetdir / "har-fulfill.har", url="**/*.js")
+ page = await context.new_page()
+
+ async def handler(route: Route) -> None:
+ assert route.request.url == "http://no.playwright/"
+ await route.fulfill(
+ status=200,
+ content_type="text/html",
+ body='hello
',
+ )
+
+ await context.route("http://no.playwright/", handler)
+ await page.goto("http://no.playwright/")
+ assert await page.evaluate("window.value") == "foo"
+ await expect(page.locator("body")).to_have_css("background-color", "rgba(0, 0, 0, 0)")
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_only_handle_requests_matching_url_filter_no_fallback_page(
+ page: Page, server: Server, assetdir: Path
+) -> None:
+ await page.route_from_har(har=assetdir / "har-fulfill.har", url="**/*.js")
+
+ async def handler(route: Route) -> None:
+ assert route.request.url == "http://no.playwright/"
+ await route.fulfill(
+ status=200,
+ content_type="text/html",
+ body='hello
',
+ )
+
+ await page.route("http://no.playwright/", handler)
+ await page.goto("http://no.playwright/")
+ assert await page.evaluate("window.value") == "foo"
+ await expect(page.locator("body")).to_have_css("background-color", "rgba(0, 0, 0, 0)")
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_support_regex_filter(
+ context: BrowserContext, server: Server, assetdir: Path
+) -> None:
+ await context.route_from_har(
+ har=assetdir / "har-fulfill.har",
+ url=re.compile(r".*(\.js|.*\.css|no.playwright\/)"),
+ )
+ page = await context.new_page()
+ await page.goto("http://no.playwright/")
+ assert await page.evaluate("window.value") == "foo"
+ await expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)")
+
+
+async def test_should_change_document_url_after_redirected_navigation(
+ context: BrowserContext, server: Server, assetdir: Path
+) -> None:
+ await context.route_from_har(har=assetdir / "har-redirect.har")
+ page = await context.new_page()
+
+ async with page.expect_navigation() as navigation_info:
+ await asyncio.gather(
+ page.wait_for_url("https://www.theverge.com/"),
+ page.goto("https://theverge.com/"),
+ )
+
+ response = await navigation_info.value
+ await expect(page).to_have_url("https://www.theverge.com/")
+ assert response.request.url == "https://www.theverge.com/"
+ assert await page.evaluate("window.location.href") == "https://www.theverge.com/"
+
+
+async def test_should_change_document_url_after_redirected_navigation_on_click(
+ context: BrowserContext, server: Server, assetdir: Path
+) -> None:
+ await context.route_from_har(
+ har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/")
+ )
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content('click me ')
+ async with page.expect_navigation() as navigation_info:
+ await asyncio.gather(
+ page.wait_for_url("https://www.theverge.com/"),
+ page.click("text=click me"),
+ )
+
+ response = await navigation_info.value
+ await expect(page).to_have_url("https://www.theverge.com/")
+ assert response.request.url == "https://www.theverge.com/"
+ assert await page.evaluate("window.location.href") == "https://www.theverge.com/"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_go_back_to_redirected_navigation(
+ context: BrowserContext, server: Server, assetdir: Path
+) -> None:
+ await context.route_from_har(
+ har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/")
+ )
+ page = await context.new_page()
+ await page.goto("https://theverge.com/")
+ await page.goto(server.EMPTY_PAGE)
+ await expect(page).to_have_url(server.EMPTY_PAGE)
+
+ response = await page.go_back()
+ assert response
+ await expect(page).to_have_url("https://www.theverge.com/")
+ assert response.request.url == "https://www.theverge.com/"
+ assert await page.evaluate("window.location.href") == "https://www.theverge.com/"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_go_forward_to_redirected_navigation(
+ context: BrowserContext, server: Server, assetdir: Path
+) -> None:
+ await context.route_from_har(
+ har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/")
+ )
+ page = await context.new_page()
+ await page.goto("https://theverge.com/")
+ await page.goto(server.EMPTY_PAGE)
+ await expect(page).to_have_url(server.EMPTY_PAGE)
+ await page.goto("https://theverge.com/")
+ await expect(page).to_have_url("https://www.theverge.com/")
+ await page.go_back()
+ await expect(page).to_have_url(server.EMPTY_PAGE)
+ response = await page.go_forward()
+ assert response
+ await expect(page).to_have_url("https://www.theverge.com/")
+ assert response.request.url == "https://www.theverge.com/"
+ assert await page.evaluate("window.location.href") == "https://www.theverge.com/"
+
+
+async def test_should_reload_redirected_navigation(
+ context: BrowserContext, server: Server, assetdir: Path
+) -> None:
+ await context.route_from_har(
+ har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/")
+ )
+ page = await context.new_page()
+ await page.goto("https://theverge.com/")
+ await expect(page).to_have_url("https://www.theverge.com/")
+ response = await page.reload()
+ assert response
+ await expect(page).to_have_url("https://www.theverge.com/")
+ assert response.request.url == "https://www.theverge.com/"
+ assert await page.evaluate("window.location.href") == "https://www.theverge.com/"
+
+
+async def test_should_fulfill_from_har_with_content_in_a_file(
+ context: BrowserContext, server: Server, assetdir: Path
+) -> None:
+ await context.route_from_har(har=assetdir / "har-sha1.har")
+ page = await context.new_page()
+ await page.goto("http://no.playwright/")
+ assert await page.content() == "Hello, world"
+
+
+async def test_should_round_trip_har_zip(
+ browser: Browser, server: Server, assetdir: Path, tmpdir: Path
+) -> None:
+ har_path = tmpdir / "har.zip"
+ context_1 = await browser.new_context(record_har_mode="minimal", record_har_path=har_path)
+ page_1 = await context_1.new_page()
+ await page_1.goto(server.PREFIX + "/one-style.html")
+ await context_1.close()
+
+ context_2 = await browser.new_context()
+ await context_2.route_from_har(har=har_path, not_found="abort")
+ page_2 = await context_2.new_page()
+ await page_2.goto(server.PREFIX + "/one-style.html")
+ assert "hello, world!" in await page_2.content()
+ await expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
+
+
+async def test_should_round_trip_har_with_post_data(
+ browser: Browser, server: Server, assetdir: Path, tmpdir: Path
+) -> None:
+ server.set_route("/echo", lambda req: (req.write(req.post_body), req.finish()))
+ fetch_function = """
+ async (body) => {
+ const response = await fetch('/echo', { method: 'POST', body });
+ return await response.text();
+ };
+ """
+ har_path = tmpdir / "har.zip"
+ context_1 = await browser.new_context(record_har_mode="minimal", record_har_path=har_path)
+ page_1 = await context_1.new_page()
+ await page_1.goto(server.EMPTY_PAGE)
+
+ assert await page_1.evaluate(fetch_function, "1") == "1"
+ assert await page_1.evaluate(fetch_function, "2") == "2"
+ assert await page_1.evaluate(fetch_function, "3") == "3"
+ await context_1.close()
+
+ context_2 = await browser.new_context()
+ await context_2.route_from_har(har=har_path, not_found="abort")
+ page_2 = await context_2.new_page()
+ await page_2.goto(server.EMPTY_PAGE)
+ assert await page_2.evaluate(fetch_function, "1") == "1"
+ assert await page_2.evaluate(fetch_function, "2") == "2"
+ assert await page_2.evaluate(fetch_function, "3") == "3"
+ with pytest.raises(Exception):
+ await page_2.evaluate(fetch_function, "4")
+
+
+async def test_should_disambiguate_by_header(
+ browser: Browser, server: Server, tmpdir: Path
+) -> None:
+ server.set_route(
+ "/echo",
+ lambda req: (req.write(cast(str, req.getHeader("baz")).encode()), req.finish()),
+ )
+ fetch_function = """
+ async (bazValue) => {
+ const response = await fetch('/echo', {
+ method: 'POST',
+ body: '',
+ headers: {
+ foo: 'foo-value',
+ bar: 'bar-value',
+ baz: bazValue,
+ }
+ });
+ return await response.text();
+ };
+ """
+ har_path = tmpdir / "har.zip"
+ context_1 = await browser.new_context(record_har_mode="minimal", record_har_path=har_path)
+ page_1 = await context_1.new_page()
+ await page_1.goto(server.EMPTY_PAGE)
+
+ assert await page_1.evaluate(fetch_function, "baz1") == "baz1"
+ assert await page_1.evaluate(fetch_function, "baz2") == "baz2"
+ assert await page_1.evaluate(fetch_function, "baz3") == "baz3"
+ await context_1.close()
+
+ context_2 = await browser.new_context()
+ await context_2.route_from_har(har=har_path)
+ page_2 = await context_2.new_page()
+ await page_2.goto(server.EMPTY_PAGE)
+ assert await page_2.evaluate(fetch_function, "baz1") == "baz1"
+ assert await page_2.evaluate(fetch_function, "baz2") == "baz2"
+ assert await page_2.evaluate(fetch_function, "baz3") == "baz3"
+ assert await page_2.evaluate(fetch_function, "baz4") == "baz1"
+
+
+async def test_should_produce_extracted_zip(browser: Browser, server: Server, tmpdir: Path) -> None:
+ har_path = tmpdir / "har.har"
+ context = await browser.new_context(
+ record_har_mode="minimal", record_har_path=har_path, record_har_content="attach"
+ )
+ page_1 = await context.new_page()
+ await page_1.goto(server.PREFIX + "/one-style.html")
+ await context.close()
+
+ assert har_path.exists()
+ with har_path.open() as r:
+ content = r.read()
+ assert "log" in content
+ assert "background-color" not in r.read()
+
+ context_2 = await browser.new_context()
+ await context_2.route_from_har(har_path, not_found="abort")
+ page_2 = await context_2.new_page()
+ await page_2.goto(server.PREFIX + "/one-style.html")
+ assert "hello, world!" in await page_2.content()
+ await expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
+
+
+async def test_should_update_har_zip_for_context(
+ browser: Browser, server: Server, tmpdir: Path
+) -> None:
+ har_path = tmpdir / "har.zip"
+ context = await browser.new_context()
+ await context.route_from_har(har_path, update=True)
+ page_1 = await context.new_page()
+ await page_1.goto(server.PREFIX + "/one-style.html")
+ await context.close()
+
+ assert har_path.exists()
+
+ context_2 = await browser.new_context()
+ await context_2.route_from_har(har_path, not_found="abort")
+ page_2 = await context_2.new_page()
+ await page_2.goto(server.PREFIX + "/one-style.html")
+ assert "hello, world!" in await page_2.content()
+ await expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
+
+
+async def test_page_unroute_all_should_stop_page_route_from_har(
+ context_factory: Callable[[], Awaitable[BrowserContext]],
+ server: Server,
+ assetdir: Path,
+) -> None:
+ har_path = assetdir / "har-fulfill.har"
+ context1 = await context_factory()
+ page1 = await context1.new_page()
+ # The har file contains requests for another domain, so the router
+ # is expected to abort all requests.
+ await page1.route_from_har(har_path, not_found="abort")
+ with pytest.raises(Error) as exc_info:
+ await page1.goto(server.EMPTY_PAGE)
+ assert exc_info.value
+ await page1.unroute_all(behavior="wait")
+ response = must(await page1.goto(server.EMPTY_PAGE))
+ assert response.ok
+
+
+async def test_context_unroute_call_should_stop_context_route_from_har(
+ context_factory: Callable[[], Awaitable[BrowserContext]],
+ server: Server,
+ assetdir: Path,
+) -> None:
+ har_path = assetdir / "har-fulfill.har"
+ context1 = await context_factory()
+ page1 = await context1.new_page()
+ # The har file contains requests for another domain, so the router
+ # is expected to abort all requests.
+ await context1.route_from_har(har_path, not_found="abort")
+ with pytest.raises(Error) as exc_info:
+ await page1.goto(server.EMPTY_PAGE)
+ assert exc_info.value
+ await context1.unroute_all(behavior="wait")
+ response = must(await page1.goto(server.EMPTY_PAGE))
+ assert must(response).ok
+
+
+async def test_should_update_har_zip_for_page(
+ browser: Browser, server: Server, tmpdir: Path
+) -> None:
+ har_path = tmpdir / "har.zip"
+ context = await browser.new_context()
+ page_1 = await context.new_page()
+ await page_1.route_from_har(har_path, update=True)
+ await page_1.goto(server.PREFIX + "/one-style.html")
+ await context.close()
+
+ assert har_path.exists()
+
+ context_2 = await browser.new_context()
+ page_2 = await context_2.new_page()
+ await page_2.route_from_har(har_path, not_found="abort")
+ await page_2.goto(server.PREFIX + "/one-style.html")
+ assert "hello, world!" in await page_2.content()
+ await expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
+
+
+async def test_should_update_har_zip_for_page_with_different_options(
+ browser: Browser, server: Server, tmpdir: Path
+) -> None:
+ har_path = tmpdir / "har.zip"
+ context1 = await browser.new_context()
+ page1 = await context1.new_page()
+ await page1.route_from_har(har_path, update=True, update_content="embed", update_mode="full")
+ await page1.goto(server.PREFIX + "/one-style.html")
+ await context1.close()
+
+ context2 = await browser.new_context()
+ page2 = await context2.new_page()
+ await page2.route_from_har(har_path, not_found="abort")
+ await page2.goto(server.PREFIX + "/one-style.html")
+ assert "hello, world!" in await page2.content()
+ await expect(page2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
+ await context2.close()
+
+
+async def test_should_update_extracted_har_zip_for_page(
+ browser: Browser, server: Server, tmpdir: Path
+) -> None:
+ har_path = tmpdir / "har.har"
+ context = await browser.new_context()
+ page_1 = await context.new_page()
+ await page_1.route_from_har(har_path, update=True)
+ await page_1.goto(server.PREFIX + "/one-style.html")
+ await context.close()
+
+ assert har_path.exists()
+ with har_path.open() as r:
+ content = r.read()
+ assert "log" in content
+ assert "background-color" not in r.read()
+
+ context_2 = await browser.new_context()
+ page_2 = await context_2.new_page()
+ await page_2.route_from_har(har_path, not_found="abort")
+ await page_2.goto(server.PREFIX + "/one-style.html")
+ assert "hello, world!" in await page_2.content()
+ await expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
+
+
+async def test_should_ignore_aborted_requests(
+ context_factory: Callable[[], Awaitable[BrowserContext]],
+ server: Server,
+ tmpdir: Path,
+) -> None:
+ path = tmpdir / "test.har"
+ server.set_route("/x", lambda request: request.loseConnection())
+ context1 = await context_factory()
+ await context1.route_from_har(har=path, update=True)
+ page1 = await context1.new_page()
+ await page1.goto(server.EMPTY_PAGE)
+ req_promise = asyncio.create_task(server.wait_for_request("/x"))
+ eval_task = asyncio.create_task(
+ page1.evaluate("url => fetch(url).catch(e => 'cancelled')", server.PREFIX + "/x")
+ )
+ await req_promise
+ req = await eval_task
+ assert req == "cancelled"
+ await context1.close()
+
+ server.reset()
+
+ def _handle_route(req: TestServerRequest) -> None:
+ req.setHeader("Content-Type", "text/plain")
+ req.write(b"test")
+ req.finish()
+
+ server.set_route("/x", _handle_route)
+ context2 = await context_factory()
+ await context2.route_from_har(path)
+ page2 = await context2.new_page()
+ await page2.goto(server.EMPTY_PAGE)
+ eval_task = asyncio.create_task(
+ page2.evaluate("url => fetch(url).catch(e => 'cancelled')", server.PREFIX + "/x")
+ )
+
+ async def _timeout() -> str:
+ await asyncio.sleep(1)
+ return "timeout"
+
+ done, _ = await asyncio.wait(
+ [eval_task, asyncio.create_task(_timeout())],
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+ assert next(iter(done)).result() == "timeout"
+ eval_task.cancel()
diff --git a/tests/async/test_headful.py b/tests/async/test_headful.py
new file mode 100644
index 0000000..d0bcd7f
--- /dev/null
+++ b/tests/async/test_headful.py
@@ -0,0 +1,178 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from pathlib import Path
+from typing import Dict
+
+import pytest
+from playwright.async_api import BrowserType
+
+from tests.server import Server
+
+
+async def test_should_have_default_url_when_launching_browser(
+ browser_type: BrowserType, launch_arguments: Dict, tmpdir: Path
+) -> None:
+ browser_context = await browser_type.launch_persistent_context(
+ 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_should_close_browser_with_beforeunload_page(
+ browser_type: BrowserType, launch_arguments: Dict, server: Server, tmpdir: Path
+) -> None:
+ browser_context = await browser_type.launch_persistent_context(
+ tmpdir, **{**launch_arguments, "headless": False}
+ )
+ page = await browser_context.new_page()
+ 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: BrowserType, launch_arguments: Dict, server: Server
+) -> None:
+ browser = await browser_type.launch(**{**launch_arguments, "headless": False})
+ browser_context = await browser.new_context()
+ await browser_context.new_page()
+ await browser_context.close()
+ browser_context = await browser.new_context()
+ await browser_context.new_page()
+ await browser_context.close()
+ await browser.close()
+
+
+async def test_should_click_background_tab(
+ browser_type: BrowserType, launch_arguments: Dict, server: Server
+) -> None:
+ browser = await browser_type.launch(**{**launch_arguments, "headless": False})
+ page = await browser.new_page()
+ await page.set_content(
+ f'Hello 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: BrowserType, launch_arguments: Dict, server: Server
+) -> None:
+ browser = await browser_type.launch(**{**launch_arguments, "headless": False})
+ page = await browser.new_page()
+ 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: BrowserType,
+ launch_arguments: Dict,
+ server: Server,
+ is_chromium: bool,
+ is_firefox: bool,
+) -> None:
+ browser = await browser_type.launch(**{**launch_arguments, "headless": False})
+ page = await browser.new_page()
+ 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.wait_for_timeout(2000)
+ allows_third_party = is_firefox
+ assert document_cookie == ("username=John Doe" if allows_third_party else "")
+ cookies = await page.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": "Lax" if is_chromium else "None",
+ "secure": False,
+ "value": "John Doe",
+ }
+ ]
+ else:
+ assert cookies == []
+
+ await browser.close()
+
+
+async def test_should_not_override_viewport_size_when_passed_null(
+ browser_type: BrowserType, launch_arguments: Dict, server: Server
+) -> None:
+ # Our WebKit embedder does not respect window features.
+ browser = await browser_type.launch(**{**launch_arguments, "headless": False})
+ context = await browser.new_context(no_viewport=True)
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await 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);
+ }"""
+ )
+ popup = await popup_info.value
+ await popup.wait_for_load_state()
+ await popup.wait_for_function(
+ """() => window.outerWidth === 500 && window.outerHeight === 450"""
+ )
+ await context.close()
+ await browser.close()
+
+
+async def test_page_bring_to_front_should_work(
+ browser_type: BrowserType, launch_arguments: Dict
+) -> None:
+ browser = await browser_type.launch(**{**launch_arguments, "headless": False})
+ page1 = await browser.new_page()
+ await page1.set_content("Page1")
+ page2 = await browser.new_page()
+ await page2.set_content("Page2")
+
+ await page1.bring_to_front()
+ assert await page1.evaluate("document.visibilityState") == "visible"
+ assert await page2.evaluate("document.visibilityState") == "visible"
+
+ await page2.bring_to_front()
+ assert await page1.evaluate("document.visibilityState") == "visible"
+ assert await page2.evaluate("document.visibilityState") == "visible"
+ await browser.close()
diff --git a/tests/async/test_ignore_https_errors.py b/tests/async/test_ignore_https_errors.py
new file mode 100644
index 0000000..f583fec
--- /dev/null
+++ b/tests/async/test_ignore_https_errors.py
@@ -0,0 +1,37 @@
+# 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 pytest
+
+from playwright.async_api import Browser, Error
+from tests.server import Server
+
+
+async def test_ignore_https_error_should_work(browser: Browser, https_server: Server) -> None:
+ context = await browser.new_context(ignore_https_errors=True)
+ page = await context.new_page()
+ response = await page.goto(https_server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+ await context.close()
+
+
+async def test_ignore_https_error_should_work_negative_case(
+ browser: Browser, https_server: Server
+) -> None:
+ context = await browser.new_context()
+ page = await context.new_page()
+ with pytest.raises(Error):
+ await page.goto(https_server.EMPTY_PAGE)
+ await context.close()
diff --git a/tests/async/test_input.py b/tests/async/test_input.py
new file mode 100644
index 0000000..b4a371a
--- /dev/null
+++ b/tests/async/test_input.py
@@ -0,0 +1,500 @@
+# 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 re
+import shutil
+import sys
+from pathlib import Path
+from typing import Any
+
+import pytest
+from flaky import flaky
+from playwright._impl._path_utils import get_file_dirname
+from playwright.async_api import Error, FilePayload, Page
+
+from tests.server import Server
+from tests.utils import chromium_version_less_than, must
+
+_dirname = get_file_dirname()
+FILE_TO_UPLOAD = _dirname / ".." / "assets/file-to-upload.txt"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_upload_the_file(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/fileupload.html")
+ file_path = os.path.relpath(FILE_TO_UPLOAD, os.getcwd())
+ input = await page.query_selector("input")
+ assert input
+ await input.set_input_files(file_path)
+ assert await page.evaluate("e => e.files[0].name", input) == "file-to-upload.txt"
+ assert (
+ await page.evaluate(
+ """e => {
+ reader = new FileReader()
+ promise = new Promise(fulfill => reader.onload = fulfill)
+ reader.readAsText(e.files[0])
+ return promise.then(() => reader.result)
+ }""",
+ input,
+ )
+ == "contents of the file\n"
+ )
+
+
+async def test_should_work(page: Page, assetdir: Path) -> None:
+ await page.set_content(" ")
+ await page.set_input_files("input", assetdir / "file-to-upload.txt")
+ assert await page.eval_on_selector("input", "input => input.files.length") == 1
+ assert (
+ await page.eval_on_selector("input", "input => input.files[0].name") == "file-to-upload.txt"
+ )
+
+
+async def test_should_set_from_memory(page: Page) -> None:
+ await page.set_content(" ")
+ file: FilePayload = {
+ "name": "test.txt",
+ "mimeType": "text/plain",
+ "buffer": b"this is a test",
+ }
+ await page.set_input_files(
+ "input",
+ files=[file],
+ )
+ assert await page.eval_on_selector("input", "input => input.files.length") == 1
+ assert await page.eval_on_selector("input", "input => input.files[0].name") == "test.txt"
+
+
+async def test_should_emit_event(page: Page) -> None:
+ await page.set_content(" ")
+ fc_done: asyncio.Future = asyncio.Future()
+ page.once("filechooser", lambda file_chooser: fc_done.set_result(file_chooser))
+ await page.click("input")
+ file_chooser = await fc_done
+ assert file_chooser
+ assert (
+ repr(file_chooser)
+ == f""
+ )
+
+
+async def test_should_work_when_file_input_is_attached_to_dom(page: Page) -> None:
+ await page.set_content(" ")
+ async with page.expect_file_chooser() as fc_info:
+ await page.click("input")
+ file_chooser = await fc_info.value
+ assert file_chooser
+
+
+async def test_should_work_when_file_input_is_not_attached_to_DOM(page: Page) -> None:
+ async with page.expect_file_chooser() as fc_info:
+ await page.evaluate(
+ """() => {
+ el = document.createElement('input')
+ el.type = 'file'
+ el.click()
+ }"""
+ )
+ file_chooser = await fc_info.value
+ assert file_chooser
+
+
+async def test_should_return_the_same_file_chooser_when_there_are_many_watchdogs_simultaneously(
+ page: Page,
+) -> None:
+ await page.set_content(" ")
+ results = await asyncio.gather(
+ page.wait_for_event("filechooser"),
+ page.wait_for_event("filechooser"),
+ page.eval_on_selector("input", "input => input.click()"),
+ )
+ assert results[0] == results[1]
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_accept_single_file(page: Page) -> None:
+ await page.set_content(' ')
+ async with page.expect_file_chooser() as fc_info:
+ await page.click("input")
+ file_chooser = await fc_info.value
+ assert file_chooser.page == page
+ assert file_chooser.element
+ await file_chooser.set_files(FILE_TO_UPLOAD)
+ assert await page.eval_on_selector("input", "input => input.files.length") == 1
+ assert (
+ await page.eval_on_selector("input", "input => input.files[0].name") == "file-to-upload.txt"
+ )
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_be_able_to_read_selected_file(page: Page) -> None:
+ page.once("filechooser", lambda file_chooser: file_chooser.set_files(FILE_TO_UPLOAD))
+ await page.set_content(" ")
+ content = await page.eval_on_selector(
+ "input",
+ """async picker => {
+ picker.click();
+ await new Promise(x => picker.oninput = x);
+ const reader = new FileReader();
+ const promise = new Promise(fulfill => reader.onload = fulfill);
+ reader.readAsText(picker.files[0]);
+ return promise.then(() => reader.result);
+ }""",
+ )
+ assert content == "contents of the file\n"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_be_able_to_reset_selected_files_with_empty_file_list(
+ page: Page,
+) -> None:
+ await page.set_content(" ")
+ page.once("filechooser", lambda file_chooser: file_chooser.set_files(FILE_TO_UPLOAD))
+ file_length = 0
+ async with page.expect_file_chooser():
+ file_length = await page.eval_on_selector(
+ "input",
+ """async picker => {
+ picker.click();
+ await new Promise(x => picker.oninput = x);
+ return picker.files.length;
+ }""",
+ )
+ assert file_length == 1
+
+ page.once("filechooser", lambda file_chooser: file_chooser.set_files([]))
+ async with page.expect_file_chooser():
+ file_length = await page.eval_on_selector(
+ "input",
+ """async picker => {
+ picker.click();
+ await new Promise(x => picker.oninput = x);
+ return picker.files.length;
+ }""",
+ )
+ assert file_length == 0
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_not_accept_multiple_files_for_single_file_input(
+ page: Page, assetdir: Path
+) -> None:
+ await page.set_content(" ")
+ async with page.expect_file_chooser() as fc_info:
+ await page.click("input")
+ file_chooser = await fc_info.value
+ with pytest.raises(Exception) as exc_info:
+ await file_chooser.set_files(
+ [
+ os.path.realpath(assetdir / "file-to-upload.txt"),
+ os.path.realpath(assetdir / "pptr.png"),
+ ]
+ )
+ assert exc_info.value
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_emit_input_and_change_events(page: Page) -> None:
+ events = []
+ await page.expose_function("eventHandled", lambda e: events.append(e))
+ await page.set_content(
+ """
+
+
+ """
+ )
+ await must(await page.query_selector("input")).set_input_files(FILE_TO_UPLOAD)
+ assert len(events) == 2
+ assert events[0]["type"] == "input"
+ assert events[1]["type"] == "change"
+
+
+async def test_should_work_for_single_file_pick(page: Page) -> None:
+ await page.set_content(" ")
+ async with page.expect_file_chooser() as fc_info:
+ await page.click("input")
+ file_chooser = await fc_info.value
+ assert file_chooser.is_multiple() is False
+
+
+async def test_should_work_for_multiple(page: Page) -> None:
+ await page.set_content(" ")
+ async with page.expect_file_chooser() as fc_info:
+ await page.click("input")
+ file_chooser = await fc_info.value
+ assert file_chooser.is_multiple()
+
+
+async def test_should_work_for_webkitdirectory(page: Page) -> None:
+ await page.set_content(" ")
+ async with page.expect_file_chooser() as fc_info:
+ await page.click("input")
+ file_chooser = await fc_info.value
+ assert file_chooser.is_multiple()
+
+
+def _assert_wheel_event(expected: Any, received: Any, browser_name: str) -> None:
+ # Chromium reports deltaX/deltaY scaled by host device scale factor.
+ # https://bugs.chromium.org/p/chromium/issues/detail?id=1324819
+ # https://github.com/microsoft/playwright/issues/7362
+ # Different bots have different scale factors (usually 1 or 2), so we just ignore the values
+ # instead of guessing the host scale factor.
+ if sys.platform == "darwin" and browser_name == "chromium":
+ del expected["deltaX"]
+ del expected["deltaY"]
+ del received["deltaX"]
+ del received["deltaY"]
+ assert received == expected
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_wheel_should_work(page: Page, browser_name: str) -> None:
+ await page.set_content(
+ """
+
+ """
+ )
+ await page.mouse.move(50, 60)
+ await _listen_for_wheel_events(page, "div")
+ await page.mouse.wheel(0, 100)
+ _assert_wheel_event(
+ await page.evaluate("window.lastEvent"),
+ {
+ "deltaX": 0,
+ "deltaY": 100,
+ "clientX": 50,
+ "clientY": 60,
+ "deltaMode": 0,
+ "ctrlKey": False,
+ "shiftKey": False,
+ "altKey": False,
+ "metaKey": False,
+ },
+ browser_name,
+ )
+ await page.wait_for_function("window.scrollY === 100")
+
+
+async def _listen_for_wheel_events(page: Page, selector: str) -> None:
+ await page.evaluate(
+ """
+ selector => {
+ document.querySelector(selector).addEventListener('wheel', (e) => {
+ window['lastEvent'] = {
+ deltaX: e.deltaX,
+ deltaY: e.deltaY,
+ clientX: e.clientX,
+ clientY: e.clientY,
+ deltaMode: e.deltaMode,
+ ctrlKey: e.ctrlKey,
+ shiftKey: e.shiftKey,
+ altKey: e.altKey,
+ metaKey: e.metaKey,
+ };
+ }, { passive: false });
+ }
+ """,
+ selector,
+ )
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+@flaky
+async def test_should_upload_large_file(page: Page, server: Server, tmp_path: Path) -> None:
+ await page.goto(server.PREFIX + "/input/fileupload.html")
+ large_file_path = tmp_path / "200MB.zip"
+ data = b"A" * 1024
+ with large_file_path.open("wb") as f:
+ for i in range(0, 200 * 1024 * 1024, len(data)):
+ f.write(data)
+ input = page.locator('input[type="file"]')
+ events = await input.evaluate_handle(
+ """
+ e => {
+ const events = [];
+ e.addEventListener('input', () => events.push('input'));
+ e.addEventListener('change', () => events.push('change'));
+ return events;
+ }
+ """
+ )
+
+ await input.set_input_files(large_file_path)
+ assert await input.evaluate("e => e.files[0].name") == "200MB.zip"
+ assert await events.evaluate("e => e") == ["input", "change"]
+
+ [request, _] = await asyncio.gather(
+ server.wait_for_request("/upload"),
+ page.click("input[type=submit]"),
+ )
+
+ contents = request.args[b"file1"][0]
+ assert len(contents) == 200 * 1024 * 1024
+ assert contents[:1024] == data
+ # flake8: noqa: E203
+ assert contents[len(contents) - 1024 :] == data
+ assert request.post_body
+ match = re.search(
+ rb'^.*Content-Disposition: form-data; name="(?P.*)"; filename="(?P.*)".*$',
+ request.post_body,
+ re.MULTILINE,
+ )
+ assert match
+ assert match.group("name") == b"file1"
+ assert match.group("filename") == b"200MB.zip"
+
+
+async def test_set_input_files_should_preserve_last_modified_timestamp(
+ page: Page,
+ assetdir: Path,
+) -> None:
+ await page.set_content(" ")
+ input = page.locator("input")
+ files = ["file-to-upload.txt", "file-to-upload-2.txt"]
+ await input.set_input_files([assetdir / file for file in files])
+ assert await input.evaluate("input => [...input.files].map(f => f.name)") == files
+ timestamps = await input.evaluate("input => [...input.files].map(f => f.lastModified)")
+ expected_timestamps = [os.path.getmtime(assetdir / file) * 1000 for file in files]
+
+ # On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even
+ # rounds it to seconds in WebKit: 1696272058110 -> 1696272058000.
+ for i in range(len(timestamps)):
+ assert abs(timestamps[i] - expected_timestamps[i]) < 1000
+
+
+@flaky
+async def test_should_upload_multiple_large_file(
+ page: Page, server: Server, tmp_path: Path
+) -> None:
+ files_count = 10
+ await page.goto(server.PREFIX + "/input/fileupload-multi.html")
+ upload_file = tmp_path / "50MB_1.zip"
+ data = b"A" * 1024
+ with upload_file.open("wb") as f:
+ # 49 is close to the actual limit
+ for i in range(0, 49 * 1024):
+ f.write(data)
+ input = page.locator('input[type="file"]')
+ upload_files = [upload_file]
+ for i in range(2, files_count + 1):
+ dst_file = tmp_path / f"50MB_{i}.zip"
+ shutil.copy(upload_file, dst_file)
+ upload_files.append(dst_file)
+ async with page.expect_file_chooser() as fc_info:
+ await input.click()
+ file_chooser = await fc_info.value
+ await file_chooser.set_files(upload_files)
+ files_len = await page.evaluate('document.getElementsByTagName("input")[0].files.length')
+ assert file_chooser.is_multiple()
+ assert files_len == files_count
+ for path in upload_files:
+ path.unlink()
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_upload_a_folder(
+ page: Page,
+ server: Server,
+ tmp_path: Path,
+ browser_name: str,
+ browser_version: str,
+ headless: bool,
+) -> None:
+ await page.goto(server.PREFIX + "/input/folderupload.html")
+ input = await page.query_selector("input")
+ assert input
+ dir = tmp_path / "file-upload-test"
+ dir.mkdir()
+ (dir / "file1.txt").write_text("file1 content")
+ (dir / "file2").write_text("file2 content")
+ (dir / "sub-dir").mkdir()
+ (dir / "sub-dir" / "really.txt").write_text("sub-dir file content")
+ await input.set_input_files(dir)
+ assert set(await input.evaluate("e => [...e.files].map(f => f.webkitRelativePath)")) == set(
+ [
+ "file-upload-test/file1.txt",
+ "file-upload-test/file2",
+ # https://issues.chromium.org/issues/345393164
+ *(
+ []
+ if browser_name == "chromium"
+ and headless
+ and chromium_version_less_than(browser_version, "127.0.6533.0")
+ else ["file-upload-test/sub-dir/really.txt"]
+ ),
+ ]
+ )
+ webkit_relative_paths = await input.evaluate("e => [...e.files].map(f => f.webkitRelativePath)")
+ for i, webkit_relative_path in enumerate(webkit_relative_paths):
+ content = await input.evaluate(
+ """(e, i) => {
+ const reader = new FileReader();
+ const promise = new Promise(fulfill => reader.onload = fulfill);
+ reader.readAsText(e.files[i]);
+ return promise.then(() => reader.result);
+ }""",
+ i,
+ )
+ assert content == (dir / ".." / webkit_relative_path).read_text()
+
+
+async def test_should_upload_a_folder_and_throw_for_multiple_directories(
+ page: Page, server: Server, tmp_path: Path
+) -> None:
+ await page.goto(server.PREFIX + "/input/folderupload.html")
+ input = page.locator("input")
+ dir = tmp_path / "file-upload-test"
+ dir.mkdir()
+ (dir / "folder1").mkdir()
+ (dir / "folder1" / "file1.txt").write_text("file1 content")
+ (dir / "folder2").mkdir()
+ (dir / "folder2" / "file2.txt").write_text("file2 content")
+ with pytest.raises(Error) as exc_info:
+ await input.set_input_files([dir / "folder1", dir / "folder2"])
+ assert "Multiple directories are not supported" in exc_info.value.message
+
+
+async def test_should_throw_if_a_directory_and_files_are_passed(
+ page: Page, server: Server, tmp_path: Path
+) -> None:
+ await page.goto(server.PREFIX + "/input/folderupload.html")
+ input = page.locator("input")
+ dir = tmp_path / "file-upload-test"
+ dir.mkdir()
+ (dir / "file1.txt").write_text("file1 content")
+ with pytest.raises(Error) as exc_info:
+ await input.set_input_files([dir, dir / "file1.txt"])
+ assert "File paths must be all files or a single directory" in exc_info.value.message
+
+
+async def test_should_throw_when_upload_a_folder_in_a_normal_file_upload_input(
+ page: Page, server: Server, tmp_path: Path
+) -> None:
+ await page.goto(server.PREFIX + "/input/fileupload.html")
+ input = await page.query_selector("input")
+ assert input
+ dir = tmp_path / "file-upload-test"
+ dir.mkdir()
+ (dir / "file1.txt").write_text("file1 content")
+ with pytest.raises(Error) as exc_info:
+ await input.set_input_files(dir)
+ assert (
+ "File input does not support directories, pass individual files instead"
+ in exc_info.value.message
+ )
diff --git a/tests/async/test_issues.py b/tests/async/test_issues.py
new file mode 100644
index 0000000..5b63b02
--- /dev/null
+++ b/tests/async/test_issues.py
@@ -0,0 +1,53 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from asyncio import FIRST_COMPLETED, CancelledError, create_task, wait
+from typing import Dict
+
+import pytest
+
+from playwright.async_api import Browser, BrowserType, Page, Playwright
+
+
+@pytest.mark.only_browser("chromium")
+async def test_issue_189(browser_type: BrowserType, launch_arguments: Dict) -> None:
+ browser = await browser_type.launch(**launch_arguments, ignore_default_args=["--mute-audio"])
+ page = await browser.new_page()
+ assert await page.evaluate("1 + 1") == 2
+ await browser.close()
+
+
+@pytest.mark.only_browser("chromium")
+async def test_issue_195(playwright: Playwright, browser: Browser) -> None:
+ iphone_11 = playwright.devices["iPhone 11"]
+ context = await browser.new_context(**iphone_11)
+ await context.close()
+
+
+async def test_connection_task_cancel(page: Page) -> None:
+ await page.set_content(" ")
+ done, pending = await wait(
+ {
+ create_task(page.wait_for_selector("input")),
+ create_task(page.wait_for_selector("#will-never-resolve")),
+ },
+ return_when=FIRST_COMPLETED,
+ )
+ assert len(done) == 1
+ assert len(pending) == 1
+ for task in pending:
+ task.cancel()
+ with pytest.raises(CancelledError):
+ await task
+ assert list(pending)[0].cancelled()
diff --git a/tests/async/test_jshandle.py b/tests/async/test_jshandle.py
new file mode 100644
index 0000000..8181a54
--- /dev/null
+++ b/tests/async/test_jshandle.py
@@ -0,0 +1,231 @@
+# 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 json
+import math
+from datetime import datetime, timezone
+from typing import Any, Dict
+
+import pytest
+from playwright.async_api import Page
+
+
+async def test_jshandle_evaluate_work(page: Page) -> None:
+ window_handle = await page.evaluate_handle("window")
+ assert window_handle
+ assert repr(window_handle) == f""
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_jshandle_evaluate_accept_object_handle_as_argument(page: Page) -> None:
+ navigator_handle = await page.evaluate_handle("navigator")
+ text = await page.evaluate("e => e.userAgent", navigator_handle)
+ assert "Mozilla" in text
+
+
+async def test_jshandle_evaluate_accept_handle_to_primitive_types(page: Page) -> None:
+ handle = await page.evaluate_handle("5")
+ is_five = await page.evaluate("e => Object.is(e, 5)", handle)
+ assert is_five
+
+
+async def test_jshandle_evaluate_accept_nested_handle(page: Page) -> None:
+ foo = await page.evaluate_handle('({ x: 1, y: "foo" })')
+ result = await page.evaluate("({ foo }) => foo", {"foo": foo})
+ assert result == {"x": 1, "y": "foo"}
+
+
+async def test_jshandle_evaluate_accept_nested_window_handle(page: Page) -> None:
+ foo = await page.evaluate_handle("window")
+ result = await page.evaluate("({ foo }) => foo === window", {"foo": foo})
+ assert result
+
+
+async def test_jshandle_evaluate_accept_multiple_nested_handles(page: Page) -> None:
+ foo = await page.evaluate_handle('({ x: 1, y: "foo" })')
+ bar = await page.evaluate_handle("5")
+ baz = await page.evaluate_handle('["baz"]')
+ result = await page.evaluate(
+ "x => JSON.stringify(x)",
+ {"a1": {"foo": foo}, "a2": {"bar": bar, "arr": [{"baz": baz}]}},
+ )
+ assert json.loads(result) == {
+ "a1": {"foo": {"x": 1, "y": "foo"}},
+ "a2": {"bar": 5, "arr": [{"baz": ["baz"]}]},
+ }
+
+
+async def test_jshandle_evaluate_should_work_for_circular_objects(page: Page) -> None:
+ a: Dict[str, Any] = {"x": 1}
+ a["y"] = a
+ result = await page.evaluate("a => { a.y.x += 1; return a; }", a)
+ assert result["x"] == 2
+ assert result["y"]["x"] == 2
+ assert result == result["y"]
+
+
+async def test_jshandle_evaluate_accept_same_nested_object_multiple_times(
+ page: Page,
+) -> None:
+ foo = {"x": 1}
+ assert await page.evaluate("x => x", {"foo": foo, "bar": [foo], "baz": {"foo": foo}}) == {
+ "foo": {"x": 1},
+ "bar": [{"x": 1}],
+ "baz": {"foo": {"x": 1}},
+ }
+
+
+async def test_jshandle_evaluate_accept_object_handle_to_unserializable_value(
+ page: Page,
+) -> None:
+ handle = await page.evaluate_handle("() => Infinity")
+ assert await page.evaluate("e => Object.is(e, Infinity)", handle)
+
+
+async def test_jshandle_evaluate_pass_configurable_args(page: Page) -> None:
+ result = await page.evaluate(
+ """arg => {
+ if (arg.foo !== 42)
+ throw new Error('Not a 42');
+ arg.foo = 17;
+ if (arg.foo !== 17)
+ throw new Error('Not 17');
+ delete arg.foo;
+ if (arg.foo === 17)
+ throw new Error('Still 17');
+ return arg;
+ }""",
+ {"foo": 42},
+ )
+ assert result == {}
+
+
+async def test_jshandle_properties_get_property(page: Page) -> None:
+ handle1 = await page.evaluate_handle(
+ """() => ({
+ one: 1,
+ two: 2,
+ three: 3
+ })"""
+ )
+ handle2 = await handle1.get_property("two")
+ assert await handle2.json_value() == 2
+
+
+async def test_jshandle_properties_work_with_undefined_null_and_empty(
+ page: Page,
+) -> None:
+ handle = await page.evaluate_handle(
+ """() => ({
+ undefined: undefined,
+ null: null,
+ })"""
+ )
+ undefined_handle = await handle.get_property("undefined")
+ assert await undefined_handle.json_value() is None
+ null_handle = await handle.get_property("null")
+ assert await null_handle.json_value() is None
+ empty_handle = await handle.get_property("empty")
+ assert await empty_handle.json_value() is None
+
+
+async def test_jshandle_properties_work_with_unserializable_values(page: Page) -> None:
+ handle = await page.evaluate_handle(
+ """() => ({
+ infinity: Infinity,
+ negInfinity: -Infinity,
+ nan: NaN,
+ negZero: -0
+ })"""
+ )
+ infinity_handle = await handle.get_property("infinity")
+ assert await infinity_handle.json_value() == float("inf")
+ neg_infinity_handle = await handle.get_property("negInfinity")
+ assert await neg_infinity_handle.json_value() == float("-inf")
+ nan_handle = await handle.get_property("nan")
+ assert math.isnan(await nan_handle.json_value()) is True
+ neg_zero_handle = await handle.get_property("negZero")
+ assert await neg_zero_handle.json_value() == float("-0")
+
+
+async def test_jshandle_properties_get_properties(page: Page) -> None:
+ handle = await page.evaluate_handle('() => ({ foo: "bar" })')
+ properties = await handle.get_properties()
+ assert "foo" in properties
+ foo = properties["foo"]
+ assert await foo.json_value() == "bar"
+
+
+async def test_jshandle_properties_return_empty_map_for_non_objects(page: Page) -> None:
+ handle = await page.evaluate_handle("123")
+ properties = await handle.get_properties()
+ assert properties == {}
+
+
+async def test_jshandle_json_value_work(page: Page) -> None:
+ handle = await page.evaluate_handle('() => ({foo: "bar"})')
+ json = await handle.json_value()
+ assert json == {"foo": "bar"}
+
+
+async def test_jshandle_json_value_work_with_dates(page: Page) -> None:
+ handle = await page.evaluate_handle('() => new Date("2020-05-27T01:31:38.506Z")')
+ json = await handle.json_value()
+ assert json == datetime.fromisoformat("2020-05-27T01:31:38.506").replace(tzinfo=timezone.utc)
+
+
+async def test_jshandle_json_value_should_work_for_circular_object(page: Page) -> None:
+ handle = await page.evaluate_handle("const a = {}; a.b = a; a")
+ a: Dict[str, Any] = {}
+ a["b"] = a
+ result = await handle.json_value()
+ # Node test looks like the below, but assert isn't smart enough to handle this:
+ # assert await handle.json_value() == a
+ assert result["b"] == result
+
+
+async def test_jshandle_as_element_work(page: Page) -> None:
+ handle = await page.evaluate_handle("document.body")
+ element = handle.as_element()
+ assert element is not None
+
+
+async def test_jshandle_as_element_return_none_for_non_elements(page: Page) -> None:
+ handle = await page.evaluate_handle("2")
+ element = handle.as_element()
+ assert element is None
+
+
+async def test_jshandle_to_string_work_for_primitives(page: Page) -> None:
+ number_handle = await page.evaluate_handle("2")
+ assert str(number_handle) == "2"
+ string_handle = await page.evaluate_handle('"a"')
+ assert str(string_handle) == "a"
+
+
+async def test_jshandle_to_string_work_for_complicated_objects(
+ page: Page, browser_name: str
+) -> None:
+ handle = await page.evaluate_handle("window")
+ if browser_name != "firefox":
+ assert str(handle) == "Window"
+ else:
+ assert str(handle) == "JSHandle@object"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_jshandle_to_string_work_for_promises(page: Page) -> None:
+ handle = await page.evaluate_handle("({b: Promise.resolve(123)})")
+ b_handle = await handle.get_property("b")
+ assert str(b_handle) == "Promise"
diff --git a/tests/async/test_keyboard.py b/tests/async/test_keyboard.py
new file mode 100644
index 0000000..7ba5e75
--- /dev/null
+++ b/tests/async/test_keyboard.py
@@ -0,0 +1,525 @@
+# 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 pytest
+from playwright.async_api import Error, JSHandle, Page
+
+from tests.server import Server
+
+from .utils import Utils
+
+
+async def captureLastKeydown(page: Page) -> JSHandle:
+ lastEvent = await page.evaluate_handle(
+ """() => {
+ const lastEvent = {
+ repeat: false,
+ location: -1,
+ code: '',
+ key: '',
+ metaKey: false,
+ keyIdentifier: 'unsupported'
+ };
+ document.addEventListener('keydown', e => {
+ lastEvent.repeat = e.repeat;
+ lastEvent.location = e.location;
+ lastEvent.key = e.key;
+ lastEvent.code = e.code;
+ lastEvent.metaKey = e.metaKey;
+ // keyIdentifier only exists in WebKit, and isn't in TypeScript's lib.
+ lastEvent.keyIdentifier = 'keyIdentifier' in e && e.keyIdentifier;
+ }, true);
+ return lastEvent;
+ }"""
+ )
+ return lastEvent
+
+
+async def test_keyboard_type_into_a_textarea(page: Page) -> None:
+ await page.evaluate(
+ """
+ const textarea = document.createElement('textarea');
+ document.body.appendChild(textarea);
+ textarea.focus();
+ """
+ )
+ text = "Hello world. I am the text that was typed!"
+ await page.keyboard.type(text)
+ assert await page.evaluate('document.querySelector("textarea").value') == text
+
+
+async def test_keyboard_move_with_the_arrow_keys(page: Page, server: Server) -> None:
+ await page.goto(f"{server.PREFIX}/input/textarea.html")
+ await page.type("textarea", "Hello World!")
+ assert await page.evaluate("document.querySelector('textarea').value") == "Hello World!"
+ for _ in "World!":
+ await page.keyboard.press("ArrowLeft")
+ await page.keyboard.type("inserted ")
+ assert (
+ await page.evaluate("document.querySelector('textarea').value") == "Hello inserted World!"
+ )
+ await page.keyboard.down("Shift")
+ for _ in "inserted ":
+ await page.keyboard.press("ArrowLeft")
+ await page.keyboard.up("Shift")
+ await page.keyboard.press("Backspace")
+ assert await page.evaluate("document.querySelector('textarea').value") == "Hello World!"
+
+
+async def test_keyboard_send_a_character_with_elementhandle_press(
+ page: Page, server: Server
+) -> None:
+ await page.goto(f"{server.PREFIX}/input/textarea.html")
+ textarea = await page.query_selector("textarea")
+ assert textarea
+ await textarea.press("a")
+ assert await page.evaluate("document.querySelector('textarea').value") == "a"
+ await page.evaluate("() => window.addEventListener('keydown', e => e.preventDefault(), true)")
+ await textarea.press("b")
+ assert await page.evaluate("document.querySelector('textarea').value") == "a"
+
+
+async def test_should_send_a_character_with_send_character(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.focus("textarea")
+ await page.keyboard.insert_text("嗨")
+ assert await page.evaluate('() => document.querySelector("textarea").value') == "嗨"
+ await page.evaluate('() => window.addEventListener("keydown", e => e.preventDefault(), true)')
+ await page.keyboard.insert_text("a")
+ assert await page.evaluate('() => document.querySelector("textarea").value') == "嗨a"
+
+
+async def test_should_only_emit_input_event(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.focus("textarea")
+ events = await page.evaluate_handle(
+ """() => {
+ const events = [];
+ document.addEventListener('keydown', e => events.push(e.type));
+ document.addEventListener('keyup', e => events.push(e.type));
+ document.addEventListener('keypress', e => events.push(e.type));
+ document.addEventListener('input', e => events.push(e.type));
+ return events;
+ }"""
+ )
+
+ await page.keyboard.insert_text("hello world")
+ assert await events.json_value() == ["input"]
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_report_shiftkey(
+ page: Page, server: Server, is_mac: bool, is_firefox: bool
+) -> None:
+ if is_firefox and is_mac:
+ pytest.skip()
+ await page.goto(server.PREFIX + "/input/keyboard.html")
+ keyboard = page.keyboard
+ codeForKey = {"Shift": 16, "Alt": 18, "Control": 17}
+ for modifierKey in codeForKey.keys():
+ await keyboard.down(modifierKey)
+ assert (
+ await page.evaluate("() => getResult()")
+ == "Keydown: "
+ + modifierKey
+ + " "
+ + modifierKey
+ + "Left "
+ + str(codeForKey[modifierKey])
+ + " ["
+ + modifierKey
+ + "]"
+ )
+ await keyboard.down("!")
+ # Shift+! will generate a keypress
+ if modifierKey == "Shift":
+ assert (
+ await page.evaluate("() => getResult()")
+ == "Keydown: ! Digit1 49 ["
+ + modifierKey
+ + "]\nKeypress: ! Digit1 33 33 ["
+ + modifierKey
+ + "]"
+ )
+ else:
+ assert (
+ await page.evaluate("() => getResult()")
+ == "Keydown: ! Digit1 49 [" + modifierKey + "]"
+ )
+
+ await keyboard.up("!")
+ assert (
+ await page.evaluate("() => getResult()") == "Keyup: ! Digit1 49 [" + modifierKey + "]"
+ )
+ await keyboard.up(modifierKey)
+ assert (
+ await page.evaluate("() => getResult()")
+ == "Keyup: "
+ + modifierKey
+ + " "
+ + modifierKey
+ + "Left "
+ + str(codeForKey[modifierKey])
+ + " []"
+ )
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_report_multiple_modifiers(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/keyboard.html")
+ keyboard = page.keyboard
+ await keyboard.down("Control")
+ assert await page.evaluate("() => getResult()") == "Keydown: Control ControlLeft 17 [Control]"
+ await keyboard.down("Alt")
+ assert await page.evaluate("() => getResult()") == "Keydown: Alt AltLeft 18 [Alt Control]"
+ await keyboard.down(";")
+ assert await page.evaluate("() => getResult()") == "Keydown: ; Semicolon 186 [Alt Control]"
+ await keyboard.up(";")
+ assert await page.evaluate("() => getResult()") == "Keyup: ; Semicolon 186 [Alt Control]"
+ await keyboard.up("Control")
+ assert await page.evaluate("() => getResult()") == "Keyup: Control ControlLeft 17 [Alt]"
+ await keyboard.up("Alt")
+ assert await page.evaluate("() => getResult()") == "Keyup: Alt AltLeft 18 []"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_send_proper_codes_while_typing(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/keyboard.html")
+ await page.keyboard.type("!")
+ assert await page.evaluate("() => getResult()") == "\n".join(
+ [
+ "Keydown: ! Digit1 49 []",
+ "Keypress: ! Digit1 33 33 []",
+ "Keyup: ! Digit1 49 []",
+ ]
+ )
+ await page.keyboard.type("^")
+ assert await page.evaluate("() => getResult()") == "\n".join(
+ [
+ "Keydown: ^ Digit6 54 []",
+ "Keypress: ^ Digit6 94 94 []",
+ "Keyup: ^ Digit6 54 []",
+ ]
+ )
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_send_proper_codes_while_typing_with_shift(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/keyboard.html")
+ keyboard = page.keyboard
+ await keyboard.down("Shift")
+ await page.keyboard.type("~")
+ assert await page.evaluate("() => getResult()") == "\n".join(
+ [
+ "Keydown: Shift ShiftLeft 16 [Shift]",
+ "Keydown: ~ Backquote 192 [Shift]", # 192 is ` keyCode
+ "Keypress: ~ Backquote 126 126 [Shift]", # 126 is ~ charCode
+ "Keyup: ~ Backquote 192 [Shift]",
+ ]
+ )
+ await keyboard.up("Shift")
+
+
+async def test_should_not_type_canceled_events(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.focus("textarea")
+ await page.evaluate(
+ """() => {
+ window.addEventListener('keydown', event => {
+ event.stopPropagation();
+ event.stopImmediatePropagation();
+ if (event.key === 'l')
+ event.preventDefault();
+ if (event.key === 'o')
+ event.preventDefault();
+ }, false);
+ }"""
+ )
+
+ await page.keyboard.type("Hello World!")
+ assert await page.eval_on_selector("textarea", "textarea => textarea.value") == "He Wrd!"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_press_plus(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/keyboard.html")
+ await page.keyboard.press("+")
+ assert await page.evaluate("() => getResult()") == "\n".join(
+ [
+ "Keydown: + Equal 187 []", # 192 is ` keyCode
+ "Keypress: + Equal 43 43 []", # 126 is ~ charCode
+ "Keyup: + Equal 187 []",
+ ]
+ )
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_press_shift_plus(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/keyboard.html")
+ await page.keyboard.press("Shift++")
+ assert await page.evaluate("() => getResult()") == "\n".join(
+ [
+ "Keydown: Shift ShiftLeft 16 [Shift]",
+ "Keydown: + Equal 187 [Shift]", # 192 is ` keyCode
+ "Keypress: + Equal 43 43 [Shift]", # 126 is ~ charCode
+ "Keyup: + Equal 187 [Shift]",
+ "Keyup: Shift ShiftLeft 16 []",
+ ]
+ )
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_support_plus_separated_modifiers(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/keyboard.html")
+ await page.keyboard.press("Shift+~")
+ assert await page.evaluate("() => getResult()") == "\n".join(
+ [
+ "Keydown: Shift ShiftLeft 16 [Shift]",
+ "Keydown: ~ Backquote 192 [Shift]", # 192 is ` keyCode
+ "Keypress: ~ Backquote 126 126 [Shift]", # 126 is ~ charCode
+ "Keyup: ~ Backquote 192 [Shift]",
+ "Keyup: Shift ShiftLeft 16 []",
+ ]
+ )
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_suport_multiple_plus_separated_modifiers(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/keyboard.html")
+ await page.keyboard.press("Control+Shift+~")
+ assert await page.evaluate("() => getResult()") == "\n".join(
+ [
+ "Keydown: Control ControlLeft 17 [Control]",
+ "Keydown: Shift ShiftLeft 16 [Control Shift]",
+ "Keydown: ~ Backquote 192 [Control Shift]", # 192 is ` keyCode
+ "Keyup: ~ Backquote 192 [Control Shift]",
+ "Keyup: Shift ShiftLeft 16 [Control]",
+ "Keyup: Control ControlLeft 17 []",
+ ]
+ )
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_shift_raw_codes(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/keyboard.html")
+ await page.keyboard.press("Shift+Digit3")
+ assert await page.evaluate("() => getResult()") == "\n".join(
+ [
+ "Keydown: Shift ShiftLeft 16 [Shift]",
+ "Keydown: # Digit3 51 [Shift]", # 51 is # keyCode
+ "Keypress: # Digit3 35 35 [Shift]", # 35 is # charCode
+ "Keyup: # Digit3 51 [Shift]",
+ "Keyup: Shift ShiftLeft 16 []",
+ ]
+ )
+
+
+async def test_should_specify_repeat_property(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.focus("textarea")
+ lastEvent = await captureLastKeydown(page)
+ await page.keyboard.down("a")
+ assert await lastEvent.evaluate("e => e.repeat") is False
+ await page.keyboard.press("a")
+ assert await lastEvent.evaluate("e => e.repeat")
+
+ await page.keyboard.down("b")
+ assert await lastEvent.evaluate("e => e.repeat") is False
+ await page.keyboard.down("b")
+ assert await lastEvent.evaluate("e => e.repeat")
+
+ await page.keyboard.up("a")
+ await page.keyboard.down("a")
+ assert await lastEvent.evaluate("e => e.repeat") is False
+
+
+async def test_should_type_all_kinds_of_characters(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.focus("textarea")
+ text = "This text goes onto two lines.\nThis character is 嗨."
+ await page.keyboard.type(text)
+ assert await page.eval_on_selector("textarea", "t => t.value") == text
+
+
+async def test_should_specify_location(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ lastEvent = await captureLastKeydown(page)
+ textarea = await page.query_selector("textarea")
+ assert textarea
+
+ await textarea.press("Digit5")
+ assert await lastEvent.evaluate("e => e.location") == 0
+
+ await textarea.press("ControlLeft")
+ assert await lastEvent.evaluate("e => e.location") == 1
+
+ await textarea.press("ControlRight")
+ assert await lastEvent.evaluate("e => e.location") == 2
+
+ await textarea.press("NumpadSubtract")
+ assert await lastEvent.evaluate("e => e.location") == 3
+
+
+async def test_should_press_enter(page: Page) -> None:
+ await page.set_content("")
+ await page.focus("textarea")
+ lastEventHandle = await captureLastKeydown(page)
+
+ async def testEnterKey(key: str, expectedKey: str, expectedCode: str) -> None:
+ await page.keyboard.press(key)
+ lastEvent = await lastEventHandle.json_value()
+ assert lastEvent["key"] == expectedKey
+ assert lastEvent["code"] == expectedCode
+ value = await page.eval_on_selector("textarea", "t => t.value")
+ assert value == "\n"
+ await page.eval_on_selector("textarea", "t => t.value = ''")
+
+ await testEnterKey("Enter", "Enter", "Enter")
+ await testEnterKey("NumpadEnter", "Enter", "NumpadEnter")
+ await testEnterKey("\n", "Enter", "Enter")
+ await testEnterKey("\r", "Enter", "Enter")
+
+
+async def test_should_throw_unknown_keys(page: Page, server: Server) -> None:
+ with pytest.raises(Error) as exc:
+ await page.keyboard.press("NotARealKey")
+ assert exc.value.message == 'Keyboard.press: Unknown key: "NotARealKey"'
+
+ with pytest.raises(Error) as exc:
+ await page.keyboard.press("ё")
+ assert exc.value.message == 'Keyboard.press: Unknown key: "ё"'
+
+ with pytest.raises(Error) as exc:
+ await page.keyboard.press("😊")
+ assert exc.value.message == 'Keyboard.press: Unknown key: "😊"'
+
+
+async def test_should_type_emoji(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.type("textarea", "👹 Tokyo street Japan 🇯🇵")
+ assert (
+ await page.eval_on_selector("textarea", "textarea => textarea.value")
+ == "👹 Tokyo street Japan 🇯🇵"
+ )
+
+
+async def test_should_type_emoji_into_an_iframe(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "emoji-test", server.PREFIX + "/input/textarea.html")
+ frame = page.frames[1]
+ textarea = await frame.query_selector("textarea")
+ assert textarea
+ await textarea.type("👹 Tokyo street Japan 🇯🇵")
+ assert (
+ await frame.eval_on_selector("textarea", "textarea => textarea.value")
+ == "👹 Tokyo street Japan 🇯🇵"
+ )
+
+
+async def test_should_handle_select_all(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ textarea = await page.query_selector("textarea")
+ assert textarea
+ await textarea.type("some text")
+ await page.keyboard.down("ControlOrMeta")
+ await page.keyboard.press("a")
+ await page.keyboard.up("ControlOrMeta")
+ await page.keyboard.press("Backspace")
+ assert await page.eval_on_selector("textarea", "textarea => textarea.value") == ""
+
+
+async def test_should_be_able_to_prevent_select_all(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ textarea = await page.query_selector("textarea")
+ assert textarea
+ await textarea.type("some text")
+ await page.eval_on_selector(
+ "textarea",
+ """textarea => {
+ textarea.addEventListener('keydown', event => {
+ if (event.key === 'a' && (event.metaKey || event.ctrlKey))
+ event.preventDefault();
+ }, false);
+ }""",
+ )
+
+ await page.keyboard.down("ControlOrMeta")
+ await page.keyboard.press("a")
+ await page.keyboard.up("ControlOrMeta")
+ await page.keyboard.press("Backspace")
+ assert await page.eval_on_selector("textarea", "textarea => textarea.value") == "some tex"
+
+
+@pytest.mark.only_platform("darwin")
+@pytest.mark.skip_browser("firefox") # Upstream issue
+async def test_should_support_macos_shortcuts(
+ page: Page, server: Server, is_firefox: bool, is_mac: bool
+) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ textarea = await page.query_selector("textarea")
+ assert textarea
+ await textarea.type("some text")
+ # select one word backwards
+ await page.keyboard.press("Shift+Control+Alt+KeyB")
+ await page.keyboard.press("Backspace")
+ assert await page.eval_on_selector("textarea", "textarea => textarea.value") == "some "
+
+
+async def test_should_press_the_meta_key(page: Page) -> None:
+ lastEvent = await captureLastKeydown(page)
+ await page.keyboard.press("Meta")
+ v = await lastEvent.json_value()
+ metaKey = v["metaKey"]
+ key = v["key"]
+ code = v["code"]
+ assert key == "Meta"
+ assert code == "MetaLeft"
+ assert metaKey
+
+
+async def test_should_work_after_a_cross_origin_navigation(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/empty.html")
+ await page.goto(server.CROSS_PROCESS_PREFIX + "/empty.html")
+ lastEvent = await captureLastKeydown(page)
+ await page.keyboard.press("a")
+ assert await lastEvent.evaluate("l => l.key") == "a"
+
+
+# event.keyIdentifier has been removed from all browsers except WebKit
+@pytest.mark.only_browser("webkit")
+async def test_should_expose_keyIdentifier_in_webkit(page: Page, server: Server) -> None:
+ lastEvent = await captureLastKeydown(page)
+ keyMap = {
+ "ArrowUp": "Up",
+ "ArrowDown": "Down",
+ "ArrowLeft": "Left",
+ "ArrowRight": "Right",
+ "Backspace": "U+0008",
+ "Tab": "U+0009",
+ "Delete": "U+007F",
+ "a": "U+0041",
+ "b": "U+0042",
+ "F12": "F12",
+ }
+ for key, keyIdentifier in keyMap.items():
+ await page.keyboard.press(key)
+ assert await lastEvent.evaluate("e => e.keyIdentifier") == keyIdentifier
+
+
+async def test_should_scroll_with_pagedown(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/scrollable.html")
+ # A click is required for WebKit to send the event into the body.
+ await page.click("body")
+ await page.keyboard.press("PageDown")
+ # We can't wait for the scroll to finish, so just wait for it to start.
+ await page.wait_for_function("() => scrollY > 0")
diff --git a/tests/async/test_launcher.py.disabled b/tests/async/test_launcher.py.disabled
new file mode 100644
index 0000000..d29b209
--- /dev/null
+++ b/tests/async/test_launcher.py.disabled
@@ -0,0 +1,145 @@
+# 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
+from pathlib import Path
+from typing import Dict, Optional
+
+import pytest
+
+from playwright.async_api import BrowserType, Error
+from tests.utils import TARGET_CLOSED_ERROR_MESSAGE
+
+
+async def test_browser_type_launch_should_reject_all_promises_when_browser_is_closed(
+ browser_type: BrowserType, launch_arguments: Dict
+) -> None:
+ browser = await browser_type.launch(**launch_arguments)
+ page = await (await browser.new_context()).new_page()
+ never_resolves = asyncio.create_task(page.evaluate("() => new Promise(r => {})"))
+ await page.close()
+ with pytest.raises(Error) as exc:
+ await never_resolves
+ assert TARGET_CLOSED_ERROR_MESSAGE in exc.value.message
+
+
+@pytest.mark.skip_browser("firefox")
+async def test_browser_type_launch_should_throw_if_page_argument_is_passed(
+ browser_type: BrowserType, launch_arguments: Dict
+) -> None:
+ with pytest.raises(Error) as exc:
+ await browser_type.launch(**launch_arguments, args=["http://example.com"])
+ assert "can not specify page" in exc.value.message
+
+
+async def test_browser_type_launch_should_reject_if_launched_browser_fails_immediately(
+ browser_type: BrowserType, launch_arguments: Dict, assetdir: Path
+) -> None:
+ with pytest.raises(Error):
+ await browser_type.launch(
+ **launch_arguments,
+ executable_path=assetdir / "dummy_bad_browser_executable.js",
+ )
+
+
+async def test_browser_type_launch_should_reject_if_executable_path_is_invalid(
+ browser_type: BrowserType, launch_arguments: Dict
+) -> None:
+ with pytest.raises(Error) as exc:
+ await browser_type.launch(
+ **launch_arguments, executable_path="random-invalid-path"
+ )
+ assert "executable doesn't exist" in exc.value.message
+
+
+async def test_browser_type_executable_path_should_work(
+ browser_type: BrowserType, browser_channel: str
+) -> None:
+ if browser_channel:
+ return
+ executable_path = browser_type.executable_path
+ assert os.path.exists(executable_path)
+ assert os.path.realpath(executable_path) == os.path.realpath(executable_path)
+
+
+async def test_browser_type_name_should_work(
+ browser_type: BrowserType, is_webkit: bool, is_firefox: bool, is_chromium: bool
+) -> None:
+ if is_webkit:
+ assert browser_type.name == "webkit"
+ elif is_firefox:
+ assert browser_type.name == "firefox"
+ elif is_chromium:
+ assert browser_type.name == "chromium"
+ else:
+ raise ValueError("Unknown browser")
+
+
+async def test_browser_close_should_fire_close_event_for_all_contexts(
+ browser_type: BrowserType, launch_arguments: Dict
+) -> None:
+ browser = await browser_type.launch(**launch_arguments)
+ context = await browser.new_context()
+ closed = []
+ context.on("close", lambda _: closed.append(True))
+ await browser.close()
+ assert closed == [True]
+
+
+async def test_browser_close_should_be_callable_twice(
+ browser_type: BrowserType, launch_arguments: Dict
+) -> None:
+ browser = await browser_type.launch(**launch_arguments)
+ await asyncio.gather(
+ browser.close(),
+ browser.close(),
+ )
+ await browser.close()
+
+
+@pytest.mark.only_browser("chromium")
+async def test_browser_launch_should_return_background_pages(
+ browser_type: BrowserType,
+ tmpdir: Path,
+ browser_channel: Optional[str],
+ assetdir: Path,
+ launch_arguments: Dict,
+) -> None:
+ if browser_channel:
+ pytest.skip()
+
+ extension_path = str(assetdir / "simple-extension")
+ context = await browser_type.launch_persistent_context(
+ str(tmpdir),
+ **{
+ **launch_arguments,
+ "headless": False,
+ "args": [
+ f"--disable-extensions-except={extension_path}",
+ f"--load-extension={extension_path}",
+ ],
+ },
+ )
+ background_page = None
+ if len(context.background_pages):
+ background_page = context.background_pages[0]
+ else:
+ background_page = await context.wait_for_event("backgroundpage")
+ assert background_page
+ assert background_page in context.background_pages
+ assert background_page not in context.pages
+ await context.close()
+ assert len(context.background_pages) == 0
+ assert len(context.pages) == 0
diff --git a/tests/async/test_listeners.py b/tests/async/test_listeners.py
new file mode 100644
index 0000000..5185fd4
--- /dev/null
+++ b/tests/async/test_listeners.py
@@ -0,0 +1,32 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from playwright.async_api import Page, Response
+from tests.server import Server
+
+
+async def test_listeners(page: Page, server: Server) -> None:
+ log = []
+
+ def print_response(response: Response) -> None:
+ log.append(response)
+
+ page.on("response", print_response)
+ await page.goto(f"{server.PREFIX}/input/textarea.html")
+ assert len(log) > 0
+ page.remove_listener("response", print_response)
+
+ log = []
+ await page.goto(f"{server.PREFIX}/input/textarea.html")
+ assert len(log) == 0
diff --git a/tests/async/test_locators.py b/tests/async/test_locators.py
new file mode 100644
index 0000000..93aad1c
--- /dev/null
+++ b/tests/async/test_locators.py
@@ -0,0 +1,1039 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import re
+import traceback
+from typing import Callable
+from urllib.parse import urlparse
+
+import pytest
+from playwright._impl._path_utils import get_file_dirname
+from playwright.async_api import Error, Page, expect
+
+from tests.server import Server
+
+_dirname = get_file_dirname()
+FILE_TO_UPLOAD = _dirname / ".." / "assets/file-to-upload.txt"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_click_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = page.locator("button")
+ await button.click()
+ assert await page.evaluate("window['result']") == "Clicked"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_click_should_work_with_node_removed(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.evaluate("delete window['Node']")
+ button = page.locator("button")
+ await button.click()
+ assert await page.evaluate("window['result']") == "Clicked"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_click_should_work_for_text_nodes(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ await page.evaluate(
+ """() => {
+ window['double'] = false;
+ const button = document.querySelector('button');
+ button.addEventListener('dblclick', event => {
+ window['double'] = true;
+ });
+ }"""
+ )
+ button = page.locator("button")
+ await button.dblclick()
+ assert await page.evaluate("double") is True
+ assert await page.evaluate("result") == "Clicked"
+
+
+async def test_locators_should_have_repr(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = page.locator("button")
+ await button.click()
+ assert (
+ str(button)
+ == f" selector='button'>"
+ )
+
+
+async def test_locators_get_attribute_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/dom.html")
+ button = page.locator("#outer")
+ assert await button.get_attribute("name") == "value"
+ assert await button.get_attribute("foo") is None
+
+
+async def test_locators_input_value_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/dom.html")
+ await page.fill("#textarea", "input value")
+ text_area = page.locator("#textarea")
+ assert await text_area.input_value() == "input value"
+
+
+async def test_locators_inner_html_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/dom.html")
+ locator = page.locator("#outer")
+ assert await locator.inner_html() == 'Text,\nmore text
'
+
+
+async def test_locators_inner_text_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/dom.html")
+ locator = page.locator("#inner")
+ assert await locator.inner_text() == "Text, more text"
+
+
+async def test_locators_text_content_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/dom.html")
+ locator = page.locator("#inner")
+ assert await locator.text_content() == "Text,\nmore text"
+
+
+async def test_locators_is_hidden_and_is_visible_should_work(page: Page) -> None:
+ await page.set_content("Hi
")
+
+ div = page.locator("div")
+ assert await div.is_visible() is True
+ assert await div.is_hidden() is False
+
+ span = page.locator("span")
+ assert await span.is_visible() is False
+ assert await span.is_hidden() is True
+
+
+async def test_locators_is_enabled_and_is_disabled_should_work(page: Page) -> None:
+ await page.set_content(
+ """
+ button1
+ button2
+ div
+ """
+ )
+
+ div = page.locator("div")
+ assert await div.is_enabled() is True
+ assert await div.is_disabled() is False
+
+ button1 = page.locator(':text("button1")')
+ assert await button1.is_enabled() is False
+ assert await button1.is_disabled() is True
+
+ button1 = page.locator(':text("button2")')
+ assert await button1.is_enabled() is True
+ assert await button1.is_disabled() is False
+
+
+async def test_locators_is_editable_should_work(page: Page) -> None:
+ await page.set_content(
+ """
+
+ """
+ )
+
+ input1 = page.locator("#input1")
+ assert await input1.is_editable() is False
+
+ input2 = page.locator("#input2")
+ assert await input2.is_editable() is True
+
+
+async def test_locators_is_checked_should_work(page: Page) -> None:
+ await page.set_content(
+ """
+ Not a checkbox
+ """
+ )
+
+ element = page.locator("input")
+ assert await element.is_checked() is True
+ await element.evaluate("e => e.checked = false")
+ assert await element.is_checked() is False
+
+
+async def test_locators_all_text_contents_should_work(page: Page) -> None:
+ await page.set_content(
+ """
+ A
B
C
+ """
+ )
+
+ element = page.locator("div")
+ assert await element.all_text_contents() == ["A", "B", "C"]
+
+
+async def test_locators_all_inner_texts(page: Page) -> None:
+ await page.set_content(
+ """
+ A
B
C
+ """
+ )
+
+ element = page.locator("div")
+ assert await element.all_inner_texts() == ["A", "B", "C"]
+
+
+async def test_locators_should_query_existing_element(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/playground.html")
+ await page.set_content(
+ """"""
+ )
+ html = page.locator("html")
+ second = html.locator(".second")
+ inner = second.locator(".inner")
+ assert await page.evaluate("e => e.textContent", await inner.element_handle()) == "A"
+
+
+async def test_locators_evaluate_handle_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/dom.html")
+ outer = page.locator("#outer")
+ inner = outer.locator("#inner")
+ check = inner.locator("#check")
+ text = await inner.evaluate_handle("e => e.firstChild")
+ await page.evaluate("1 + 1")
+ assert (
+ str(outer)
+ == f" selector='#outer'>"
+ )
+ assert (
+ str(inner)
+ == f" selector='#outer >> #inner'>"
+ )
+ assert str(text) == "JSHandle@#text=Text,↵more text"
+ assert (
+ str(check)
+ == f" selector='#outer >> #inner >> #check'>"
+ )
+
+
+async def test_locators_should_query_existing_elements(page: Page) -> None:
+ await page.set_content("""A
B
""")
+ html = page.locator("html")
+ elements = await html.locator("div").element_handles()
+ assert len(elements) == 2
+ result = []
+ for element in elements:
+ result.append(await page.evaluate("e => e.textContent", element))
+ assert result == ["A", "B"]
+
+
+async def test_locators_return_empty_array_for_non_existing_elements(
+ page: Page,
+) -> None:
+ await page.set_content("""A
B
""")
+ html = page.locator("html")
+ elements = await html.locator("abc").element_handles()
+ assert len(elements) == 0
+ assert elements == []
+
+
+async def test_locators_evaluate_all_should_work(page: Page) -> None:
+ await page.set_content(
+ """"""
+ )
+ tweet = page.locator(".tweet .like")
+ content = await tweet.evaluate_all("nodes => nodes.map(n => n.innerText)")
+ assert content == ["100", "10"]
+
+
+async def test_locators_evaluate_all_should_work_with_missing_selector(
+ page: Page,
+) -> None:
+ await page.set_content("""not-a-child-div
nodes.length")
+ assert nodes_length == 0
+
+
+async def test_locators_hover_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/scrollable.html")
+ button = page.locator("#button-6")
+ await button.hover()
+ assert await page.evaluate("document.querySelector('button:hover').id") == "button-6"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_fill_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ button = page.locator("input")
+ await button.fill("some value")
+ assert await page.evaluate("result") == "some value"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_clear_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ button = page.locator("input")
+ await button.fill("some value")
+ assert await page.evaluate("result") == "some value"
+ await button.clear()
+ assert await page.evaluate("result") == ""
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_check_should_work(page: Page) -> None:
+ await page.set_content(" ")
+ button = page.locator("input")
+ await button.check()
+ assert await page.evaluate("checkbox.checked") is True
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_uncheck_should_work(page: Page) -> None:
+ await page.set_content(" ")
+ button = page.locator("input")
+ await button.uncheck()
+ assert await page.evaluate("checkbox.checked") is False
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_select_option_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ select = page.locator("select")
+ await select.select_option("blue")
+ assert await page.evaluate("result.onInput") == ["blue"]
+ assert await page.evaluate("result.onChange") == ["blue"]
+
+
+async def test_locators_focus_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = page.locator("button")
+ assert await button.evaluate("button => document.activeElement === button") is False
+ await button.focus()
+ assert await button.evaluate("button => document.activeElement === button") is True
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_dispatch_event_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = page.locator("button")
+ await button.dispatch_event("click")
+ assert await page.evaluate("result") == "Clicked"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_should_upload_a_file(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/fileupload.html")
+ input = page.locator("input[type=file]")
+
+ file_path = os.path.relpath(FILE_TO_UPLOAD, os.getcwd())
+ await input.set_input_files(file_path)
+ assert (
+ await page.evaluate("e => e.files[0].name", await input.element_handle())
+ == "file-to-upload.txt"
+ )
+
+
+async def test_locators_should_press(page: Page) -> None:
+ await page.set_content(" ")
+ await page.locator("input").press("h")
+ assert await page.eval_on_selector("input", "input => input.value") == "h"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_should_scroll_into_view(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/offscreenbuttons.html")
+ for i in range(11):
+ button = page.locator(f"#btn{i}")
+ before = await button.evaluate(
+ "button => button.getBoundingClientRect().right - window.innerWidth"
+ )
+ assert before == 10 * i
+ await button.scroll_into_view_if_needed()
+ after = await button.evaluate(
+ "button => button.getBoundingClientRect().right - window.innerWidth"
+ )
+ assert after <= 0
+ await page.evaluate("window.scrollTo(0, 0)")
+
+
+async def test_locators_should_select_textarea(
+ page: Page, server: Server, browser_name: str
+) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ textarea = page.locator("textarea")
+ await textarea.evaluate("textarea => textarea.value = 'some value'")
+ await textarea.select_text()
+ if browser_name == "firefox" or browser_name == "webkit":
+ assert await textarea.evaluate("el => el.selectionStart") == 0
+ assert await textarea.evaluate("el => el.selectionEnd") == 10
+ else:
+ assert await page.evaluate("window.getSelection().toString()") == "some value"
+
+
+async def test_locators_should_type(page: Page) -> None:
+ await page.set_content(" ")
+ await page.locator("input").type("hello")
+ assert await page.eval_on_selector("input", "input => input.value") == "hello"
+
+
+async def test_locators_should_press_sequentially(page: Page) -> None:
+ await page.set_content(" ")
+ await page.locator("input").press_sequentially("hello")
+ assert await page.eval_on_selector("input", "input => input.value") == "hello"
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_should_screenshot(
+ page: Page, server: Server, assert_to_be_golden: Callable[[bytes, str], None]
+) -> None:
+ await page.set_viewport_size(
+ {
+ "width": 500,
+ "height": 500,
+ }
+ )
+ await page.goto(server.PREFIX + "/grid.html")
+ await page.evaluate("window.scrollBy(50, 100)")
+ element = page.locator(".box:nth-of-type(3)")
+ assert_to_be_golden(await element.screenshot(), "screenshot-element-bounding-box.png")
+
+
+async def test_locators_should_return_bounding_box(page: Page, server: Server) -> None:
+ await page.set_viewport_size(
+ {
+ "width": 500,
+ "height": 500,
+ }
+ )
+ await page.goto(server.PREFIX + "/grid.html")
+ element = page.locator(".box:nth-of-type(13)")
+ box = await element.bounding_box()
+ assert box == {
+ "x": 100,
+ "y": 50,
+ "width": 50,
+ "height": 50,
+ }
+
+
+async def test_locators_should_respect_first_and_last(page: Page) -> None:
+ await page.set_content(
+ """
+ """
+ )
+ assert await page.locator("div >> p").count() == 6
+ assert await page.locator("div").locator("p").count() == 6
+ assert await page.locator("div").first.locator("p").count() == 1
+ assert await page.locator("div").last.locator("p").count() == 3
+
+
+async def test_locators_should_respect_nth(page: Page) -> None:
+ await page.set_content(
+ """
+ """
+ )
+ assert await page.locator("div >> p").nth(0).count() == 1
+ assert await page.locator("div").nth(1).locator("p").count() == 2
+ assert await page.locator("div").nth(2).locator("p").count() == 3
+
+
+async def test_locators_should_throw_on_capture_without_nth(page: Page) -> None:
+ await page.set_content(
+ """
+
+ """
+ )
+ with pytest.raises(Error, match="Can't query n-th element"):
+ await page.locator("*css=div >> p").nth(1).click()
+
+
+async def test_locators_should_throw_due_to_strictness(page: Page) -> None:
+ await page.set_content(
+ """
+ A
B
+ """
+ )
+ with pytest.raises(Error, match="strict mode violation"):
+ await page.locator("div").is_visible()
+
+
+async def test_locators_should_throw_due_to_strictness_2(page: Page) -> None:
+ await page.set_content(
+ """
+ One Two
+ """
+ )
+ with pytest.raises(Error, match="strict mode violation"):
+ await page.locator("option").evaluate("e => {}")
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_locators_set_checked(page: Page) -> None:
+ await page.set_content("` `")
+ locator = page.locator("input")
+ await locator.set_checked(True)
+ assert await page.evaluate("checkbox.checked")
+ await locator.set_checked(False)
+ assert await page.evaluate("checkbox.checked") is False
+
+
+async def test_locators_wait_for(page: Page) -> None:
+ await page.set_content("
")
+ locator = page.locator("div")
+ task = locator.wait_for()
+ await page.eval_on_selector("div", "div => div.innerHTML = 'target '")
+ await task
+ assert await locator.text_content() == "target"
+
+
+async def test_should_wait_for_hidden(page: Page) -> None:
+ await page.set_content("target
")
+ locator = page.locator("span")
+ task = locator.wait_for(state="hidden")
+ await page.eval_on_selector("div", "div => div.innerHTML = ''")
+ await task
+
+
+async def test_should_combine_visible_with_other_selectors(page: Page) -> None:
+ await page.set_content(
+ """
+
Hidden data0
+
visible data1
+
Hidden data1
+
visible data2
+
Hidden data1
+
visible data3
+
+ """
+ )
+ locator = page.locator(".item >> visible=true").nth(1)
+ await expect(locator).to_have_text("visible data2")
+ await expect(page.locator(".item >> visible=true >> text=data3")).to_have_text("visible data3")
+
+
+async def test_locator_count_should_work_with_deleted_map_in_main_world(
+ page: Page,
+) -> None:
+ await page.evaluate("Map = 1")
+ await page.locator("#searchResultTableDiv .x-grid3-row").count()
+ await expect(page.locator("#searchResultTableDiv .x-grid3-row")).to_have_count(0)
+
+
+async def test_locator_locator_and_framelocator_locator_should_accept_locator(
+ page: Page,
+) -> None:
+ await page.set_content(
+ """
+
+
+ """
+ )
+
+ input_locator = page.locator("input")
+ assert await input_locator.input_value() == "outer"
+ assert await page.locator("div").locator(input_locator).input_value() == "outer"
+ assert await page.frame_locator("iframe").locator(input_locator).input_value() == "inner"
+ assert (
+ await page.frame_locator("iframe").locator("div").locator(input_locator).input_value()
+ == "inner"
+ )
+
+ div_locator = page.locator("div")
+ assert await div_locator.locator("input").input_value() == "outer"
+ assert (
+ await page.frame_locator("iframe").locator(div_locator).locator("input").input_value()
+ == "inner"
+ )
+
+
+async def route_iframe(page: Page) -> None:
+ await page.route(
+ "**/empty.html",
+ lambda route: route.fulfill(
+ body='',
+ content_type="text/html",
+ ),
+ )
+ await page.route(
+ "**/iframe.html",
+ lambda route: route.fulfill(
+ body="""
+
+ Hello iframe
+
+
+ 1
+ 2
+ """,
+ content_type="text/html",
+ ),
+ )
+ await page.route(
+ "**/iframe-2.html",
+ lambda route: route.fulfill(
+ body="Hello nested iframe ",
+ content_type="text/html",
+ ),
+ )
+
+
+async def test_locators_frame_should_work_with_iframe(page: Page, server: Server) -> None:
+ await route_iframe(page)
+ await page.goto(server.EMPTY_PAGE)
+ button = page.frame_locator("iframe").locator("button")
+ await button.wait_for()
+ assert await button.inner_text() == "Hello iframe"
+ await button.click()
+
+
+async def test_locators_frame_should_work_for_nested_iframe(page: Page, server: Server) -> None:
+ await route_iframe(page)
+ await page.goto(server.EMPTY_PAGE)
+ button = page.frame_locator("iframe").frame_locator("iframe").locator("button")
+ await button.wait_for()
+ assert await button.inner_text() == "Hello nested iframe"
+ await button.click()
+
+
+async def test_locators_frame_should_work_with_locator_frame_locator(
+ page: Page, server: Server
+) -> None:
+ await route_iframe(page)
+ await page.goto(server.EMPTY_PAGE)
+ button = page.locator("body").frame_locator("iframe").locator("button")
+ await button.wait_for()
+ assert await button.inner_text() == "Hello iframe"
+ await button.click()
+
+
+async def test_locator_content_frame_should_work(page: Page, server: Server) -> None:
+ await route_iframe(page)
+ await page.goto(server.EMPTY_PAGE)
+ locator = page.locator("iframe")
+ frame_locator = locator.content_frame
+ button = frame_locator.locator("button")
+ assert await button.inner_text() == "Hello iframe"
+ await expect(button).to_have_text("Hello iframe")
+ await button.click()
+
+
+async def test_frame_locator_owner_should_work(page: Page, server: Server) -> None:
+ await route_iframe(page)
+ await page.goto(server.EMPTY_PAGE)
+ frame_locator = page.frame_locator("iframe")
+ locator = frame_locator.owner
+ await expect(locator).to_be_visible()
+ assert await locator.get_attribute("name") == "frame1"
+
+
+async def route_ambiguous(page: Page) -> None:
+ await page.route(
+ "**/empty.html",
+ lambda route: route.fulfill(
+ body="""
+
+
+
+ """,
+ content_type="text/html",
+ ),
+ )
+ await page.route(
+ "**/iframe-*",
+ lambda route: route.fulfill(
+ body=f"Hello from {urlparse(route.request.url).path[1:]} ",
+ content_type="text/html",
+ ),
+ )
+
+
+async def test_locator_frame_locator_should_throw_on_ambiguity(page: Page, server: Server) -> None:
+ await route_ambiguous(page)
+ await page.goto(server.EMPTY_PAGE)
+ button = page.locator("body").frame_locator("iframe").locator("button")
+ with pytest.raises(
+ Error,
+ match=r'.*strict mode violation: locator\("body"\)\.locator\("iframe"\) resolved to 3 elements.*',
+ ):
+ await button.wait_for()
+
+
+async def test_locator_frame_locator_should_not_throw_on_first_last_nth(
+ page: Page, server: Server
+) -> None:
+ await route_ambiguous(page)
+ await page.goto(server.EMPTY_PAGE)
+ button1 = page.locator("body").frame_locator("iframe").first.locator("button")
+ assert await button1.text_content() == "Hello from iframe-1.html"
+ button2 = page.locator("body").frame_locator("iframe").nth(1).locator("button")
+ assert await button2.text_content() == "Hello from iframe-2.html"
+ button3 = page.locator("body").frame_locator("iframe").last.locator("button")
+ assert await button3.text_content() == "Hello from iframe-3.html"
+
+
+async def test_drag_to(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/drag-n-drop.html")
+ await page.locator("#source").drag_to(page.locator("#target"))
+ assert (
+ await page.eval_on_selector(
+ "#target", "target => target.contains(document.querySelector('#source'))"
+ )
+ is True
+ )
+
+
+async def test_drag_to_with_position(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(
+ """
+
+
+
+
+ """
+ )
+ events_handle = await page.evaluate_handle(
+ """
+ () => {
+ const events = [];
+ document.getElementById('red').addEventListener('mousedown', event => {
+ events.push({
+ type: 'mousedown',
+ x: event.offsetX,
+ y: event.offsetY,
+ });
+ });
+ document.getElementById('blue').addEventListener('mouseup', event => {
+ events.push({
+ type: 'mouseup',
+ x: event.offsetX,
+ y: event.offsetY,
+ });
+ });
+ return events;
+ }
+ """
+ )
+ await page.locator("#red").drag_to(
+ page.locator("#blue"),
+ source_position={"x": 34, "y": 7},
+ target_position={"x": 10, "y": 20},
+ )
+ assert await events_handle.json_value() == [
+ {"type": "mousedown", "x": 34, "y": 7},
+ {"type": "mouseup", "x": 10, "y": 20},
+ ]
+
+
+async def test_locator_query_should_filter_by_text(page: Page, server: Server) -> None:
+ await page.set_content("Foobar
Bar
")
+ await expect(page.locator("div", has_text="Foo")).to_have_text("Foobar")
+
+
+async def test_locator_query_should_filter_by_text_2(page: Page, server: Server) -> None:
+ await page.set_content("foo hello world bar
")
+ await expect(page.locator("div", has_text="hello world")).to_have_text("foo hello world bar")
+
+
+async def test_locator_query_should_filter_by_regex(page: Page, server: Server) -> None:
+ await page.set_content("Foobar
Bar
")
+ await expect(page.locator("div", has_text=re.compile(r"Foo.*"))).to_have_text("Foobar")
+
+
+async def test_locator_query_should_filter_by_text_with_quotes(page: Page, server: Server) -> None:
+ await page.set_content('Hello "world"
Hello world
')
+ await expect(page.locator("div", has_text='Hello "world"')).to_have_text('Hello "world"')
+
+
+async def test_locator_query_should_filter_by_regex_with_quotes(page: Page, server: Server) -> None:
+ await page.set_content('Hello "world"
Hello world
')
+ await expect(page.locator("div", has_text=re.compile('Hello "world"'))).to_have_text(
+ 'Hello "world"'
+ )
+
+
+async def test_locator_query_should_filter_by_regex_and_regexp_flags(
+ page: Page, server: Server
+) -> None:
+ await page.set_content('Hello "world"
Hello world
')
+ await expect(
+ page.locator("div", has_text=re.compile('hElLo "world', re.IGNORECASE))
+ ).to_have_text('Hello "world"')
+
+
+async def test_locator_should_return_page(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/frames/two-frames.html")
+ outer = page.locator("#outer")
+ assert outer.page == page
+
+ inner = outer.locator("#inner")
+ assert inner.page == page
+
+ in_frame = page.frames[1].locator("div")
+ assert in_frame.page == page
+
+
+async def test_locator_should_support_has_locator(page: Page, server: Server) -> None:
+ await page.set_content("hello
world
")
+ await expect(page.locator("div", has=page.locator("text=world"))).to_have_count(1)
+ assert (
+ await page.locator("div", has=page.locator("text=world")).evaluate("e => e.outerHTML")
+ == "world
"
+ )
+ await expect(page.locator("div", has=page.locator('text="hello"'))).to_have_count(1)
+ assert (
+ await page.locator("div", has=page.locator('text="hello"')).evaluate("e => e.outerHTML")
+ == "hello
"
+ )
+ await expect(page.locator("div", has=page.locator("xpath=./span"))).to_have_count(2)
+ await expect(page.locator("div", has=page.locator("span"))).to_have_count(2)
+ await expect(page.locator("div", has=page.locator("span", has_text="wor"))).to_have_count(1)
+ assert (
+ await page.locator("div", has=page.locator("span", has_text="wor")).evaluate(
+ "e => e.outerHTML"
+ )
+ == "world
"
+ )
+ await expect(
+ page.locator(
+ "div",
+ has=page.locator("span"),
+ has_text="wor",
+ )
+ ).to_have_count(1)
+
+
+async def test_locator_should_enforce_same_frame_for_has_locator(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/frames/two-frames.html")
+ child = page.frames[1]
+ with pytest.raises(Error) as exc_info:
+ page.locator("div", has=child.locator("span"))
+ assert 'Inner "has" locator must belong to the same frame.' in exc_info.value.message
+
+
+async def test_locator_should_support_locator_or(page: Page, server: Server) -> None:
+ await page.set_content("hello
world ")
+ await expect(page.locator("div").or_(page.locator("span"))).to_have_count(2)
+ await expect(page.locator("div").or_(page.locator("span"))).to_have_text(["hello", "world"])
+ await expect(
+ page.locator("span").or_(page.locator("article")).or_(page.locator("div"))
+ ).to_have_text(["hello", "world"])
+ await expect(page.locator("article").or_(page.locator("someting"))).to_have_count(0)
+ await expect(page.locator("article").or_(page.locator("div"))).to_have_text("hello")
+ await expect(page.locator("article").or_(page.locator("span"))).to_have_text("world")
+ await expect(page.locator("div").or_(page.locator("article"))).to_have_text("hello")
+ await expect(page.locator("span").or_(page.locator("article"))).to_have_text("world")
+
+
+async def test_locator_should_support_locator_locator_with_and_or(page: Page) -> None:
+ await page.set_content(
+ """
+ one two three
+ four
+ five
+ """
+ )
+
+ await expect(page.locator("div").locator(page.locator("button"))).to_have_text(["three"])
+ await expect(
+ page.locator("div").locator(page.locator("button").or_(page.locator("span")))
+ ).to_have_text(["two", "three"])
+ await expect(page.locator("button").or_(page.locator("span"))).to_have_text(
+ ["two", "three", "four", "five"]
+ )
+
+ await expect(
+ page.locator("div").locator(page.locator("button").and_(page.get_by_role("button")))
+ ).to_have_text(["three"])
+ await expect(page.locator("button").and_(page.get_by_role("button"))).to_have_text(
+ ["three", "five"]
+ )
+
+
+async def test_locator_highlight_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/grid.html")
+ await page.locator(".box").nth(3).highlight()
+ assert await page.locator("x-pw-glass").is_visible()
+
+
+async def test_should_support_locator_that(page: Page) -> None:
+ await page.set_content(
+ ""
+ )
+
+ await expect(page.locator("div").filter(has_text="hello")).to_have_count(1)
+ await expect(page.locator("div", has_text="hello").filter(has_text="hello")).to_have_count(1)
+ await expect(page.locator("div", has_text="hello").filter(has_text="world")).to_have_count(0)
+ await expect(page.locator("section", has_text="hello").filter(has_text="world")).to_have_count(
+ 1
+ )
+ await expect(page.locator("div").filter(has_text="hello").locator("span")).to_have_count(1)
+ await expect(
+ page.locator("div").filter(has=page.locator("span", has_text="world"))
+ ).to_have_count(1)
+ await expect(page.locator("div").filter(has=page.locator("span"))).to_have_count(2)
+ await expect(
+ page.locator("div").filter(
+ has=page.locator("span"),
+ has_text="world",
+ )
+ ).to_have_count(1)
+
+
+async def test_should_filter_by_case_insensitive_regex_in_a_child(page: Page) -> None:
+ await page.set_content('
Title Text ')
+ await expect(page.locator("div", has_text=re.compile(r"^title text$", re.I))).to_have_text(
+ "Title Text"
+ )
+
+
+async def test_should_filter_by_case_insensitive_regex_in_multiple_children(
+ page: Page,
+) -> None:
+ await page.set_content('
Title Text ')
+ await expect(page.locator("div", has_text=re.compile(r"^title text$", re.I))).to_have_class(
+ "test"
+ )
+
+
+async def test_should_filter_by_regex_with_special_symbols(page: Page) -> None:
+ await page.set_content('
First/"and" Second\\ ')
+ await expect(
+ page.locator("div", has_text=re.compile(r'^first\/".*"second\\$', re.S | re.I))
+ ).to_have_class("test")
+
+
+async def test_should_support_locator_filter(page: Page) -> None:
+ await page.set_content(
+ ""
+ )
+
+ await expect(page.locator("div").filter(has_text="hello")).to_have_count(1)
+ await expect(page.locator("div", has_text="hello").filter(has_text="hello")).to_have_count(1)
+ await expect(page.locator("div", has_text="hello").filter(has_text="world")).to_have_count(0)
+ await expect(page.locator("section", has_text="hello").filter(has_text="world")).to_have_count(
+ 1
+ )
+ await expect(page.locator("div").filter(has_text="hello").locator("span")).to_have_count(1)
+ await expect(
+ page.locator("div").filter(has=page.locator("span", has_text="world"))
+ ).to_have_count(1)
+ await expect(page.locator("div").filter(has=page.locator("span"))).to_have_count(2)
+ await expect(
+ page.locator("div").filter(
+ has=page.locator("span"),
+ has_text="world",
+ )
+ ).to_have_count(1)
+ await expect(
+ page.locator("div").filter(has_not=page.locator("span", has_text="world"))
+ ).to_have_count(1)
+ await expect(page.locator("div").filter(has_not=page.locator("section"))).to_have_count(2)
+ await expect(page.locator("div").filter(has_not=page.locator("span"))).to_have_count(0)
+
+ await expect(page.locator("div").filter(has_not_text="hello")).to_have_count(1)
+ await expect(page.locator("div").filter(has_not_text="foo")).to_have_count(2)
+
+
+async def test_locators_should_support_locator_and(page: Page, server: Server) -> None:
+ await page.set_content(
+ """
+ hello
world
+ hello2 world2
+ """
+ )
+ await expect(page.locator("div").and_(page.locator("div"))).to_have_count(2)
+ await expect(page.locator("div").and_(page.get_by_test_id("foo"))).to_have_text(["hello"])
+ await expect(page.locator("div").and_(page.get_by_test_id("bar"))).to_have_text(["world"])
+ await expect(page.get_by_test_id("foo").and_(page.locator("div"))).to_have_text(["hello"])
+ await expect(page.get_by_test_id("bar").and_(page.locator("span"))).to_have_text(["world2"])
+ await expect(
+ page.locator("span").and_(page.get_by_test_id(re.compile("bar|foo")))
+ ).to_have_count(2)
+
+
+async def test_locators_has_does_not_encode_unicode(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ locators = [
+ page.locator("button", has_text="Драматург"),
+ page.locator("button", has_text=re.compile("Драматург")),
+ page.locator("button", has=page.locator("text=Драматург")),
+ ]
+ for locator in locators:
+ with pytest.raises(Error) as exc_info:
+ await locator.click(timeout=1_000)
+ assert "Драматург" in exc_info.value.message
+
+
+async def test_locators_should_focus_and_blur_a_button(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/button.html")
+ button = page.locator("button")
+ assert not await button.evaluate("button => document.activeElement === button")
+
+ focused = False
+ blurred = False
+
+ async def focus_event() -> None:
+ nonlocal focused
+ focused = True
+
+ async def blur_event() -> None:
+ nonlocal blurred
+ blurred = True
+
+ await page.expose_function("focusEvent", focus_event)
+ await page.expose_function("blurEvent", blur_event)
+ await button.evaluate(
+ """button => {
+ button.addEventListener('focus', window['focusEvent']);
+ button.addEventListener('blur', window['blurEvent']);
+ }"""
+ )
+
+ await button.focus()
+ assert focused
+ assert not blurred
+ assert await button.evaluate("button => document.activeElement === button")
+
+ await button.blur()
+ assert focused
+ assert blurred
+ assert not await button.evaluate("button => document.activeElement === button")
+
+
+async def test_locator_all_should_work(page: Page) -> None:
+ await page.set_content("")
+ texts = []
+ for p in await page.locator("p").all():
+ texts.append(await p.text_content())
+ assert texts == ["A", "B", "C"]
+
+
+async def test_locator_click_timeout_error_should_contain_call_log(page: Page) -> None:
+ with pytest.raises(Error) as exc_info:
+ await page.get_by_role("button", name="Hello Python").click(timeout=42)
+ formatted_exception = "".join(
+ traceback.format_exception(type(exc_info.value), value=exc_info.value, tb=None)
+ )
+ assert "Locator.click: Timeout 42ms exceeded." in formatted_exception
+ assert 'waiting for get_by_role("button", name="Hello Python")' in formatted_exception
+ assert (
+ "During handling of the above exception, another exception occurred"
+ not in formatted_exception
+ )
diff --git a/tests/async/test_navigation.py b/tests/async/test_navigation.py
new file mode 100644
index 0000000..d23b77e
--- /dev/null
+++ b/tests/async/test_navigation.py
@@ -0,0 +1,1057 @@
+# 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 re
+import sys
+from pathlib import Path
+from typing import Any, List, Optional
+
+import pytest
+
+from playwright.async_api import (
+ BrowserContext,
+ Error,
+ Page,
+ Request,
+ Response,
+ Route,
+ TimeoutError,
+)
+from tests.server import Server, TestServerRequest
+
+
+async def test_goto_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ assert page.url == server.EMPTY_PAGE
+
+
+async def test_goto_should_work_with_file_URL(page: Page, assetdir: Path) -> None:
+ fileurl = (assetdir / "frames" / "two-frames.html").as_uri()
+ await page.goto(fileurl)
+ assert page.url.lower() == fileurl.lower()
+ assert len(page.frames) == 3
+
+
+async def test_goto_should_use_http_for_no_protocol(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE[7:])
+ assert page.url == server.EMPTY_PAGE
+
+
+async def test_goto_should_work_cross_process(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ assert page.url == server.EMPTY_PAGE
+
+ url = server.CROSS_PROCESS_PREFIX + "/empty.html"
+ request_frames = []
+
+ def on_request(r: Request) -> None:
+ if r.url == url:
+ request_frames.append(r.frame)
+
+ page.on("request", on_request)
+
+ response = await page.goto(url)
+ assert response
+ assert page.url == url
+ assert response.frame == page.main_frame
+ assert request_frames[0] == page.main_frame
+ assert response.url == url
+
+
+async def test_goto_should_capture_iframe_navigation_request(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ assert page.url == server.EMPTY_PAGE
+
+ request_frames = []
+
+ def on_request(r: Request) -> None:
+ if r.url == server.PREFIX + "/frames/frame.html":
+ request_frames.append(r.frame)
+
+ page.on("request", on_request)
+
+ response = await page.goto(server.PREFIX + "/frames/one-frame.html")
+ assert response
+ assert page.url == server.PREFIX + "/frames/one-frame.html"
+ assert response.frame == page.main_frame
+ assert response.url == server.PREFIX + "/frames/one-frame.html"
+
+ assert len(page.frames) == 2
+ assert request_frames[0] == page.frames[1]
+
+
+async def test_goto_should_capture_cross_process_iframe_navigation_request(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ assert page.url == server.EMPTY_PAGE
+
+ request_frames = []
+
+ def on_request(r: Request) -> None:
+ if r.url == server.CROSS_PROCESS_PREFIX + "/frames/frame.html":
+ request_frames.append(r.frame)
+
+ page.on("request", on_request)
+
+ response = await page.goto(server.CROSS_PROCESS_PREFIX + "/frames/one-frame.html")
+ assert response
+ assert page.url == server.CROSS_PROCESS_PREFIX + "/frames/one-frame.html"
+ assert response.frame == page.main_frame
+ assert response.url == server.CROSS_PROCESS_PREFIX + "/frames/one-frame.html"
+
+ assert len(page.frames) == 2
+ assert request_frames[0] == page.frames[1]
+
+
+async def test_goto_should_work_with_anchor_navigation(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ assert page.url == server.EMPTY_PAGE
+ await page.goto(server.EMPTY_PAGE + "#foo")
+ assert page.url == server.EMPTY_PAGE + "#foo"
+ await page.goto(server.EMPTY_PAGE + "#bar")
+ assert page.url == server.EMPTY_PAGE + "#bar"
+
+
+async def test_goto_should_work_with_redirects(page: Page, server: Server) -> None:
+ server.set_redirect("/redirect/1.html", "/redirect/2.html")
+ server.set_redirect("/redirect/2.html", "/empty.html")
+ response = await page.goto(server.PREFIX + "/redirect/1.html")
+ assert response
+ assert response.status == 200
+ assert page.url == server.EMPTY_PAGE
+
+
+async def test_goto_should_navigate_to_about_blank(page: Page, server: Server) -> None:
+ response = await page.goto("about:blank")
+ assert response is None
+
+
+async def test_goto_should_return_response_when_page_changes_its_url_after_load(
+ page: Page, server: Server
+) -> None:
+ response = await page.goto(server.PREFIX + "/historyapi.html")
+ assert response
+ assert response.status == 200
+
+
+@pytest.mark.skip_browser("firefox")
+async def test_goto_should_work_with_subframes_return_204(page: Page, server: Server) -> None:
+ def handle(request: TestServerRequest) -> None:
+ request.setResponseCode(204)
+ request.finish()
+
+ server.set_route("/frames/frame.html", handle)
+
+ await page.goto(server.PREFIX + "/frames/one-frame.html")
+
+
+async def test_goto_should_fail_when_server_returns_204(
+ page: Page, server: Server, is_chromium: bool, is_webkit: bool
+) -> None:
+ # WebKit just loads an empty page.
+ def handle(request: TestServerRequest) -> None:
+ request.setResponseCode(204)
+ request.finish()
+
+ server.set_route("/empty.html", handle)
+
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.EMPTY_PAGE)
+ assert exc_info.value
+ if is_chromium:
+ assert "net::ERR_ABORTED" in exc_info.value.message
+ elif is_webkit:
+ assert "Aborted: 204 No Content" in exc_info.value.message
+ else:
+ assert "NS_BINDING_ABORTED" in exc_info.value.message
+
+
+async def test_goto_should_navigate_to_empty_page_with_domcontentloaded(
+ page: Page, server: Server
+) -> None:
+ response = await page.goto(server.EMPTY_PAGE, wait_until="domcontentloaded")
+ assert response
+ assert response.status == 200
+
+
+async def test_goto_should_work_when_page_calls_history_api_in_beforeunload(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.evaluate(
+ """() => {
+ window.addEventListener('beforeunload', () => history.replaceState(null, 'initial', window.location.href), false)
+ }"""
+ )
+
+ response = await page.goto(server.PREFIX + "/grid.html")
+ assert response
+ assert response.status == 200
+
+
+async def test_goto_should_fail_when_navigating_to_bad_url(
+ page: Page, is_chromium: bool, is_webkit: bool
+) -> None:
+ with pytest.raises(Error) as exc_info:
+ await page.goto("asdfasdf")
+ if is_chromium or is_webkit:
+ assert "Cannot navigate to invalid URL" in exc_info.value.message
+ else:
+ assert "Invalid url" in exc_info.value.message
+
+
+async def test_goto_should_fail_when_navigating_to_bad_ssl(
+ page: Page, https_server: Server, browser_name: str
+) -> None:
+ with pytest.raises(Error) as exc_info:
+ await page.goto(https_server.EMPTY_PAGE)
+ expect_ssl_error(exc_info.value.message, browser_name)
+
+
+async def test_goto_should_fail_when_navigating_to_bad_ssl_after_redirects(
+ page: Page, server: Server, https_server: Server, browser_name: str
+) -> None:
+ server.set_redirect("/redirect/1.html", "/redirect/2.html")
+ server.set_redirect("/redirect/2.html", "/empty.html")
+ with pytest.raises(Error) as exc_info:
+ await page.goto(https_server.PREFIX + "/redirect/1.html")
+ expect_ssl_error(exc_info.value.message, browser_name)
+
+
+async def test_goto_should_not_crash_when_navigating_to_bad_ssl_after_a_cross_origin_navigation(
+ page: Page, server: Server, https_server: Server
+) -> None:
+ await page.goto(server.CROSS_PROCESS_PREFIX + "/empty.html")
+ with pytest.raises(Error):
+ await page.goto(https_server.EMPTY_PAGE)
+
+
+async def test_goto_should_throw_if_networkidle2_is_passed_as_an_option(
+ page: Page, server: Server
+) -> None:
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.EMPTY_PAGE, wait_until="networkidle2") # type: ignore
+ assert (
+ "wait_until: expected one of (load|domcontentloaded|networkidle|commit)"
+ in exc_info.value.message
+ )
+
+
+async def test_goto_should_fail_when_main_resources_failed_to_load(
+ page: Page, is_chromium: bool, is_webkit: bool, is_win: bool
+) -> None:
+ with pytest.raises(Error) as exc_info:
+ await page.goto("http://localhost:44123/non-existing-url")
+ if is_chromium:
+ assert "net::ERR_CONNECTION_REFUSED" in exc_info.value.message
+ elif is_webkit and is_win:
+ assert "Couldn't connect to server" in exc_info.value.message
+ elif is_webkit:
+ assert "Could not connect" in exc_info.value.message
+ else:
+ assert "NS_ERROR_CONNECTION_REFUSED" in exc_info.value.message
+
+
+async def test_goto_should_fail_when_exceeding_maximum_navigation_timeout(
+ page: Page, server: Server
+) -> None:
+ # Hang for request to the empty.html
+ server.set_route("/empty.html", lambda request: None)
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.PREFIX + "/empty.html", timeout=1)
+ assert "Timeout 1ms exceeded" in exc_info.value.message
+ assert server.PREFIX + "/empty.html" in exc_info.value.message
+ assert isinstance(exc_info.value, TimeoutError)
+
+
+async def test_goto_should_fail_when_exceeding_default_maximum_navigation_timeout(
+ page: Page, server: Server
+) -> None:
+ # Hang for request to the empty.html
+ server.set_route("/empty.html", lambda request: None)
+ page.context.set_default_navigation_timeout(2)
+ page.set_default_navigation_timeout(1)
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.PREFIX + "/empty.html")
+ assert "Timeout 1ms exceeded" in exc_info.value.message
+ assert server.PREFIX + "/empty.html" in exc_info.value.message
+ assert isinstance(exc_info.value, TimeoutError)
+
+
+async def test_goto_should_fail_when_exceeding_browser_context_navigation_timeout(
+ page: Page, server: Server
+) -> None:
+ # Hang for request to the empty.html
+ server.set_route("/empty.html", lambda request: None)
+ page.context.set_default_navigation_timeout(2)
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.PREFIX + "/empty.html")
+ assert "Timeout 2ms exceeded" in exc_info.value.message
+ assert server.PREFIX + "/empty.html" in exc_info.value.message
+ assert isinstance(exc_info.value, TimeoutError)
+
+
+async def test_goto_should_fail_when_exceeding_default_maximum_timeout(
+ page: Page, server: Server
+) -> None:
+ # Hang for request to the empty.html
+ server.set_route("/empty.html", lambda request: None)
+ page.context.set_default_timeout(2)
+ page.set_default_timeout(1)
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.PREFIX + "/empty.html")
+ assert "Timeout 1ms exceeded" in exc_info.value.message
+ assert server.PREFIX + "/empty.html" in exc_info.value.message
+ assert isinstance(exc_info.value, TimeoutError)
+
+
+async def test_goto_should_fail_when_exceeding_browser_context_timeout(
+ page: Page, server: Server
+) -> None:
+ # Hang for request to the empty.html
+ server.set_route("/empty.html", lambda request: None)
+ page.context.set_default_timeout(2)
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.PREFIX + "/empty.html")
+ assert "Timeout 2ms exceeded" in exc_info.value.message
+ assert server.PREFIX + "/empty.html" in exc_info.value.message
+ assert isinstance(exc_info.value, TimeoutError)
+
+
+async def test_goto_should_prioritize_default_navigation_timeout_over_default_timeout(
+ page: Page, server: Server
+) -> None:
+ # Hang for request to the empty.html
+ server.set_route("/empty.html", lambda request: None)
+ page.set_default_timeout(0)
+ page.set_default_navigation_timeout(1)
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.PREFIX + "/empty.html")
+ assert "Timeout 1ms exceeded" in exc_info.value.message
+ assert server.PREFIX + "/empty.html" in exc_info.value.message
+ assert isinstance(exc_info.value, TimeoutError)
+
+
+async def test_goto_should_disable_timeout_when_its_set_to_0(page: Page, server: Server) -> None:
+ loaded: List[bool] = []
+ page.once("load", lambda _: loaded.append(True))
+ await page.goto(server.PREFIX + "/grid.html", timeout=0, wait_until="load")
+ assert loaded == [True]
+
+
+async def test_goto_should_work_when_navigating_to_valid_url(page: Page, server: Server) -> None:
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+
+
+async def test_goto_should_work_when_navigating_to_data_url(page: Page, server: Server) -> None:
+ response = await page.goto("data:text/html,hello")
+ assert response is None
+
+
+async def test_goto_should_work_when_navigating_to_404(page: Page, server: Server) -> None:
+ response = await page.goto(server.PREFIX + "/not-found")
+ assert response
+ assert response.ok is False
+ assert response.status == 404
+
+
+async def test_goto_should_return_last_response_in_redirect_chain(
+ page: Page, server: Server
+) -> None:
+ server.set_redirect("/redirect/1.html", "/redirect/2.html")
+ server.set_redirect("/redirect/2.html", "/redirect/3.html")
+ server.set_redirect("/redirect/3.html", server.EMPTY_PAGE)
+ response = await page.goto(server.PREFIX + "/redirect/1.html")
+ assert response
+ assert response.ok
+ assert response.url == server.EMPTY_PAGE
+
+
+async def test_goto_should_navigate_to_data_url_and_not_fire_dataURL_requests(
+ page: Page, server: Server
+) -> None:
+ requests = []
+ page.on("request", lambda request: requests.append(request))
+ dataURL = "data:text/html,yo
"
+ response = await page.goto(dataURL)
+ assert response is None
+ assert requests == []
+
+
+async def test_goto_should_navigate_to_url_with_hash_and_fire_requests_without_hash(
+ page: Page, server: Server
+) -> None:
+ requests = []
+ page.on("request", lambda request: requests.append(request))
+ response = await page.goto(server.EMPTY_PAGE + "#hash")
+ assert response
+ assert response.status == 200
+ assert response.url == server.EMPTY_PAGE
+ assert len(requests) == 1
+ assert requests[0].url == server.EMPTY_PAGE
+
+
+async def test_goto_should_work_with_self_requesting_page(page: Page, server: Server) -> None:
+ response = await page.goto(server.PREFIX + "/self-request.html")
+ assert response
+ assert response.status == 200
+ assert "self-request.html" in response.url
+
+
+async def test_goto_should_fail_when_navigating_and_show_the_url_at_the_error_message(
+ page: Page, https_server: Server
+) -> None:
+ url = https_server.PREFIX + "/redirect/1.html"
+ with pytest.raises(Error) as exc_info:
+ await page.goto(url)
+ assert url in exc_info.value.message
+
+
+async def test_goto_should_be_able_to_navigate_to_a_page_controlled_by_service_worker(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/serviceworkers/fetch/sw.html")
+ await page.evaluate("window.activationPromise")
+ await page.goto(server.PREFIX + "/serviceworkers/fetch/sw.html")
+
+
+async def test_goto_should_send_referer(page: Page, server: Server) -> None:
+ [request1, request2, _] = await asyncio.gather(
+ server.wait_for_request("/grid.html"),
+ server.wait_for_request("/digits/1.png"),
+ page.goto(server.PREFIX + "/grid.html", referer="http://google.com/"),
+ )
+ assert request1.getHeader("referer") == "http://google.com/"
+ # Make sure subresources do not inherit referer.
+ assert request2.getHeader("referer") == server.PREFIX + "/grid.html"
+ assert page.url == server.PREFIX + "/grid.html"
+
+
+async def test_goto_should_reject_referer_option_when_set_extra_http_headers_provides_referer(
+ page: Page, server: Server
+) -> None:
+ await page.set_extra_http_headers({"referer": "http://microsoft.com/"})
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.PREFIX + "/grid.html", referer="http://google.com/")
+ assert '"referer" is already specified as extra HTTP header' in exc_info.value.message
+ assert server.PREFIX + "/grid.html" in exc_info.value.message
+
+
+async def test_goto_should_work_with_commit(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE, wait_until="commit")
+ assert page.url == server.EMPTY_PAGE
+
+
+async def test_network_idle_should_navigate_to_empty_page_with_networkidle(
+ page: Page, server: Server
+) -> None:
+ response = await page.goto(server.EMPTY_PAGE, wait_until="networkidle")
+ assert response
+ assert response.status == 200
+
+
+async def test_wait_for_nav_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_navigation() as response_info:
+ await page.evaluate("url => window.location.href = url", server.PREFIX + "/grid.html")
+ response = await response_info.value
+ assert response.ok
+ assert "grid.html" in response.url
+
+
+async def test_wait_for_nav_should_respect_timeout(page: Page, server: Server) -> None:
+ with pytest.raises(Error) as exc_info:
+ async with page.expect_navigation(url="**/frame.html", timeout=2500):
+ await page.goto(server.EMPTY_PAGE)
+ assert "Timeout 2500ms exceeded" in exc_info.value.message
+
+
+async def test_wait_for_nav_should_work_with_both_domcontentloaded_and_load(
+ page: Page, server: Server
+) -> None:
+ async with page.expect_navigation(wait_until="domcontentloaded"), page.expect_navigation(
+ wait_until="load"
+ ):
+ await page.goto(server.PREFIX + "/one-style.html")
+
+
+async def test_wait_for_nav_should_work_with_clicking_on_anchor_links(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content('foobar ')
+ async with page.expect_navigation() as response_info:
+ await page.click("a")
+ response = await response_info.value
+ assert response is None
+ assert page.url == server.EMPTY_PAGE + "#foobar"
+
+
+async def test_wait_for_nav_should_work_with_clicking_on_links_which_do_not_commit_navigation(
+ page: Page, server: Server, https_server: Server, browser_name: str
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(f"foobar ")
+ with pytest.raises(Error) as exc_info:
+ async with page.expect_navigation():
+ await page.click("a")
+ expect_ssl_error(exc_info.value.message, browser_name)
+
+
+async def test_wait_for_nav_should_work_with_history_push_state(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(
+ """
+ SPA
+
+ """
+ )
+ async with page.expect_navigation() as response_info:
+ await page.click("a")
+ response = await response_info.value
+ assert response is None
+ assert page.url == server.PREFIX + "/wow.html"
+
+
+async def test_wait_for_nav_should_work_with_history_replace_state(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(
+ """
+ SPA
+
+ """
+ )
+ async with page.expect_navigation() as response_info:
+ await page.click("a")
+ response = await response_info.value
+ assert response is None
+ assert page.url == server.PREFIX + "/replaced.html"
+
+
+async def test_wait_for_nav_should_work_with_dom_history_back_forward(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(
+ """
+ back
+ forward
+
+ """
+ )
+ assert page.url == server.PREFIX + "/second.html"
+ async with page.expect_navigation() as back_response_info:
+ await page.click("a#back")
+ back_response = await back_response_info.value
+ assert back_response is None
+ assert page.url == server.PREFIX + "/first.html"
+ async with page.expect_navigation() as forward_response_info:
+ await page.click("a#forward")
+ forward_response = await forward_response_info.value
+ assert forward_response is None
+ assert page.url == server.PREFIX + "/second.html"
+
+
+@pytest.mark.skip_browser("webkit") # WebKit issues load event in some cases, but not always
+async def test_wait_for_nav_should_work_when_subframe_issues_window_stop(
+ page: Page, server: Server, is_webkit: bool
+) -> None:
+ server.set_route("/frames/style.css", lambda _: None)
+ done = False
+
+ async def nav_and_mark_done() -> None:
+ nonlocal done
+ await page.goto(server.PREFIX + "/frames/one-frame.html")
+ done = True
+
+ task = asyncio.create_task(nav_and_mark_done())
+ await asyncio.sleep(0)
+ async with page.expect_event("frameattached") as frame_info:
+ pass
+ frame = await frame_info.value
+
+ async with page.expect_event("framenavigated", lambda f: f == frame):
+ pass
+ await frame.evaluate("() => window.stop()")
+ await page.wait_for_timeout(2000) # give it some time to erroneously resolve
+ assert done == (not is_webkit) # Chromium and Firefox issue load event in this case.
+ if is_webkit:
+ task.cancel()
+
+
+async def test_wait_for_nav_should_work_with_url_match(page: Page, server: Server) -> None:
+ responses: List[Optional[Response]] = [None, None, None]
+
+ async def wait_for_nav(url: Any, index: int) -> None:
+ async with page.expect_navigation(url=url) as response_info:
+ pass
+ responses[index] = await response_info.value
+
+ response0_promise = asyncio.create_task(wait_for_nav(re.compile(r"one-style\.html"), 0))
+ response1_promise = asyncio.create_task(wait_for_nav(re.compile(r"\/frame.html"), 1))
+ response2_promise = asyncio.create_task(wait_for_nav(lambda url: "foo=bar" in url, 2))
+ assert responses == [None, None, None]
+ await page.goto(server.EMPTY_PAGE)
+ assert responses == [None, None, None]
+ await page.goto(server.PREFIX + "/frame.html")
+ assert responses[0] is None
+ await response1_promise
+ assert responses[1] is not None
+ assert responses[2] is None
+ await page.goto(server.PREFIX + "/one-style.html")
+ await response0_promise
+ assert responses[0] is not None
+ assert responses[1] is not None
+ assert responses[2] is None
+ await page.goto(server.PREFIX + "/frame.html?foo=bar")
+ await response2_promise
+ assert responses[0] is not None
+ assert responses[1] is not None
+ assert responses[2] is not None
+ await page.goto(server.PREFIX + "/empty.html")
+ assert responses[0].url == server.PREFIX + "/one-style.html"
+ assert responses[1].url == server.PREFIX + "/frame.html"
+ assert responses[2].url == server.PREFIX + "/frame.html?foo=bar"
+
+
+async def test_wait_for_nav_should_work_with_url_match_for_same_document_navigations(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_navigation(url=re.compile(r"third\.html")) as response_info:
+ assert not response_info.is_done()
+ await page.evaluate("history.pushState({}, '', '/first.html')")
+ assert not response_info.is_done()
+ await page.evaluate("history.pushState({}, '', '/second.html')")
+ assert not response_info.is_done()
+ await page.evaluate("history.pushState({}, '', '/third.html')")
+ assert response_info.is_done()
+
+
+async def test_wait_for_nav_should_work_for_cross_process_navigations(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ url = server.CROSS_PROCESS_PREFIX + "/empty.html"
+ async with page.expect_navigation(wait_until="domcontentloaded") as response_info:
+ await page.goto(url)
+ response = await response_info.value
+ assert response.url == url
+ assert page.url == url
+ assert await page.evaluate("document.location.href") == url
+
+
+async def test_expect_navigation_should_work_for_cross_process_navigations(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ url = server.CROSS_PROCESS_PREFIX + "/empty.html"
+ async with page.expect_navigation(wait_until="domcontentloaded") as response_info:
+ goto_task = asyncio.create_task(page.goto(url))
+ response = await response_info.value
+ assert response.url == url
+ assert page.url == url
+ assert await page.evaluate("document.location.href") == url
+ await goto_task
+
+
+async def test_wait_for_nav_should_work_with_commit(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_navigation(wait_until="commit") as response_info:
+ await page.evaluate("url => window.location.href = url", server.PREFIX + "/grid.html")
+ response = await response_info.value
+ assert response.ok
+ assert "grid.html" in response.url
+
+
+async def test_wait_for_load_state_should_respect_timeout(page: Page, server: Server) -> None:
+ requests = []
+
+ def handler(request: Any) -> None:
+ requests.append(request)
+
+ server.set_route("/one-style.css", handler)
+
+ await page.goto(server.PREFIX + "/one-style.html", wait_until="domcontentloaded")
+ with pytest.raises(Error) as exc_info:
+ await page.wait_for_load_state("load", timeout=1)
+ assert "Timeout 1ms exceeded." in exc_info.value.message
+
+
+async def test_wait_for_load_state_should_resolve_immediately_if_loaded(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/one-style.html")
+ await page.wait_for_load_state()
+
+
+async def test_wait_for_load_state_should_throw_for_bad_state(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/one-style.html")
+ with pytest.raises(Error) as exc_info:
+ await page.wait_for_load_state("bad") # type: ignore
+ assert (
+ "state: expected one of (load|domcontentloaded|networkidle|commit)"
+ in exc_info.value.message
+ )
+
+
+async def test_wait_for_load_state_should_resolve_immediately_if_load_state_matches(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+
+ requests = []
+
+ def handler(request: Any) -> None:
+ requests.append(request)
+
+ server.set_route("/one-style.css", handler)
+
+ await page.goto(server.PREFIX + "/one-style.html", wait_until="domcontentloaded")
+ await page.wait_for_load_state("domcontentloaded")
+
+
+async def test_wait_for_load_state_networkidle(page: Page, server: Server) -> None:
+ wait_for_network_idle_future = asyncio.create_task(page.wait_for_load_state("networkidle"))
+ await page.goto(server.PREFIX + "/networkidle.html")
+ await wait_for_network_idle_future
+
+
+async def test_wait_for_load_state_should_work_with_pages_that_have_loaded_before_being_connected_to(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("window._popup = window.open(document.location.href)")
+
+ # The url is about:blank in FF.
+ popup = await popup_info.value
+ assert popup.url == server.EMPTY_PAGE
+ await popup.wait_for_load_state()
+ assert popup.url == server.EMPTY_PAGE
+
+
+async def test_wait_for_load_state_should_wait_for_load_state_of_empty_url_popup(
+ page: Page, is_firefox: bool
+) -> None:
+ ready_state = []
+ async with page.expect_popup() as popup_info:
+ ready_state.append(
+ await page.evaluate(
+ """() => {
+ popup = window.open('')
+ return popup.document.readyState
+ }"""
+ )
+ )
+
+ popup = await popup_info.value
+ await popup.wait_for_load_state()
+ assert ready_state == ["uninitialized"] if is_firefox else ["complete"]
+ assert await popup.evaluate("() => document.readyState") == ready_state[0]
+
+
+async def test_wait_for_load_state_should_wait_for_load_state_of_about_blank_popup_(
+ page: Page,
+) -> None:
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("window.open('about:blank') && 1")
+ popup = await popup_info.value
+ await popup.wait_for_load_state()
+ assert await popup.evaluate("document.readyState") == "complete"
+
+
+async def test_wait_for_load_state_should_wait_for_load_state_of_about_blank_popup_with_noopener(
+ page: Page,
+) -> None:
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("window.open('about:blank', null, 'noopener') && 1")
+
+ popup = await popup_info.value
+ await popup.wait_for_load_state()
+ assert await popup.evaluate("document.readyState") == "complete"
+
+
+async def test_wait_for_load_state_should_wait_for_load_state_of_popup_with_network_url_(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("url => window.open(url) && 1", server.EMPTY_PAGE)
+
+ popup = await popup_info.value
+ await popup.wait_for_load_state()
+ assert await popup.evaluate("document.readyState") == "complete"
+
+
+async def test_wait_for_load_state_should_wait_for_load_state_of_popup_with_network_url_and_noopener_(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("url => window.open(url, null, 'noopener') && 1", server.EMPTY_PAGE)
+
+ popup = await popup_info.value
+ await popup.wait_for_load_state()
+ assert await popup.evaluate("document.readyState") == "complete"
+
+
+async def test_wait_for_load_state_should_work_with_clicking_target__blank(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content('yo ')
+ async with page.expect_popup() as popup_info:
+ await page.click("a")
+ popup = await popup_info.value
+ await popup.wait_for_load_state()
+ assert await popup.evaluate("document.readyState") == "complete"
+
+
+async def test_wait_for_load_state_should_wait_for_load_state_of_new_page(
+ context: BrowserContext,
+) -> None:
+ async with context.expect_page() as page_info:
+ await context.new_page()
+ new_page = await page_info.value
+ await new_page.wait_for_load_state()
+ assert await new_page.evaluate("document.readyState") == "complete"
+
+
+async def test_wait_for_load_state_in_popup(context: BrowserContext, server: Server) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ css_requests = []
+
+ def handle_request(request: TestServerRequest) -> None:
+ css_requests.append(request)
+ request.write(b"body {}")
+ request.finish()
+
+ server.set_route("/one-style.css", handle_request)
+
+ async with page.expect_popup() as popup_info:
+ await page.evaluate(
+ "url => window.popup = window.open(url)", server.PREFIX + "/one-style.html"
+ )
+
+ popup = await popup_info.value
+ await popup.wait_for_load_state()
+ assert len(css_requests)
+
+
+async def test_go_back_should_work(page: Page, server: Server) -> None:
+ assert await page.go_back() is None
+
+ await page.goto(server.EMPTY_PAGE)
+ await page.goto(server.PREFIX + "/grid.html")
+
+ response = await page.go_back()
+ assert response
+ assert response.ok
+ assert server.EMPTY_PAGE in response.url
+
+ response = await page.go_forward()
+ assert response
+ assert response.ok
+ assert "/grid.html" in response.url
+
+ response = await page.go_forward()
+ assert response is None
+
+
+async def test_go_back_should_work_with_history_api(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.evaluate(
+ """() => {
+ history.pushState({}, '', '/first.html')
+ history.pushState({}, '', '/second.html')
+ }"""
+ )
+ assert page.url == server.PREFIX + "/second.html"
+
+ await page.go_back()
+ assert page.url == server.PREFIX + "/first.html"
+ await page.go_back()
+ assert page.url == server.EMPTY_PAGE
+ await page.go_forward()
+ assert page.url == server.PREFIX + "/first.html"
+
+
+async def test_frame_goto_should_navigate_subframes(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/frames/one-frame.html")
+ assert "/frames/one-frame.html" in page.frames[0].url
+ assert "/frames/frame.html" in page.frames[1].url
+
+ response = await page.frames[1].goto(server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+ assert response.frame == page.frames[1]
+
+
+async def test_frame_goto_should_reject_when_frame_detaches(
+ page: Page, server: Server, browser_name: str
+) -> None:
+ await page.goto(server.PREFIX + "/frames/one-frame.html")
+
+ server.set_route("/one-style.css", lambda _: None)
+ wait_for_request_task = asyncio.create_task(server.wait_for_request("/one-style.css"))
+ navigation_task = asyncio.create_task(page.frames[1].goto(server.PREFIX + "/one-style.html"))
+ await wait_for_request_task
+
+ await page.eval_on_selector("iframe", "frame => frame.remove()")
+ with pytest.raises(Error) as exc_info:
+ await navigation_task
+ if browser_name == "chromium":
+ assert "net::ERR_FAILED" in exc_info.value.message or (
+ "frame was detached" in exc_info.value.message.lower()
+ )
+ else:
+ assert "frame was detached" in exc_info.value.message.lower()
+
+
+async def test_frame_goto_should_continue_after_client_redirect(page: Page, server: Server) -> None:
+ server.set_route("/frames/script.js", lambda _: None)
+ url = server.PREFIX + "/frames/child-redirect.html"
+
+ with pytest.raises(Error) as exc_info:
+ await page.goto(url, timeout=5000, wait_until="networkidle")
+
+ assert "Timeout 5000ms exceeded." in exc_info.value.message
+ assert f'navigating to "{url}", waiting until "networkidle"' in exc_info.value.message
+
+
+async def test_frame_wait_for_nav_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/frames/one-frame.html")
+ frame = page.frames[1]
+ async with frame.expect_navigation() as response_info:
+ await frame.evaluate("url => window.location.href = url", server.PREFIX + "/grid.html")
+ response = await response_info.value
+ assert response.ok
+ assert "grid.html" in response.url
+ assert response.frame == frame
+ assert "/frames/one-frame.html" in page.url
+
+
+async def test_frame_wait_for_nav_should_fail_when_frame_detaches(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/frames/one-frame.html")
+ frame = page.frames[1]
+ server.set_route("/empty.html", lambda _: None)
+ server.set_route("/one-style.css", lambda _: None)
+ with pytest.raises(Error) as exc_info:
+ async with frame.expect_navigation():
+
+ async def after_it() -> None:
+ await server.wait_for_request("/one-style.html")
+ await page.eval_on_selector(
+ "iframe", "frame => setTimeout(() => frame.remove(), 0)"
+ )
+
+ await asyncio.gather(
+ page.eval_on_selector(
+ "iframe",
+ "frame => frame.contentWindow.location.href = '/one-style.html'",
+ ),
+ after_it(),
+ )
+ assert "frame was detached" in exc_info.value.message
+
+
+async def test_frame_wait_for_load_state_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/frames/one-frame.html")
+ frame = page.frames[1]
+
+ request_future: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route(
+ server.PREFIX + "/one-style.css",
+ lambda route, request: request_future.set_result(route),
+ )
+
+ await frame.goto(server.PREFIX + "/one-style.html", wait_until="domcontentloaded")
+ request = await request_future
+ load_task = asyncio.create_task(frame.wait_for_load_state())
+ # give the promise a chance to resolve, even though it shouldn't
+ await page.evaluate("1")
+ assert not load_task.done()
+ asyncio.create_task(request.continue_())
+ await load_task
+
+
+async def test_reload_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.evaluate("window._foo = 10")
+ await page.reload()
+ assert await page.evaluate("window._foo") is None
+
+
+async def test_reload_should_work_with_data_url(page: Page, server: Server) -> None:
+ await page.goto("data:text/html,hello")
+ assert "hello" in await page.content()
+ assert await page.reload() is None
+ assert "hello" in await page.content()
+
+
+async def test_should_work_with__blank_target(page: Page, server: Server) -> None:
+ def handler(request: TestServerRequest) -> None:
+ request.write(f'Click me '.encode())
+ request.finish()
+
+ server.set_route("/empty.html", handler)
+
+ await page.goto(server.EMPTY_PAGE)
+ await page.click('"Click me"')
+
+
+async def test_should_work_with_cross_process__blank_target(page: Page, server: Server) -> None:
+ def handler(request: TestServerRequest) -> None:
+ request.write(
+ f'Click me '.encode()
+ )
+ request.finish()
+
+ server.set_route("/empty.html", handler)
+
+ await page.goto(server.EMPTY_PAGE)
+ await page.click('"Click me"')
+
+
+def expect_ssl_error(error_message: str, browser_name: str) -> None:
+ if browser_name == "chromium":
+ assert "net::ERR_CERT_AUTHORITY_INVALID" in error_message
+ elif browser_name == "webkit":
+ if sys.platform == "darwin":
+ assert "The certificate for this server is invalid" in error_message
+ elif sys.platform == "win32":
+ assert "SSL peer certificate or SSH remote key was not OK" in error_message
+ else:
+ assert "Unacceptable TLS certificate" in error_message
+ else:
+ assert "SSL_ERROR_UNKNOWN" in error_message
diff --git a/tests/async/test_network.py b/tests/async/test_network.py
new file mode 100644
index 0000000..daf076e
--- /dev/null
+++ b/tests/async/test_network.py
@@ -0,0 +1,879 @@
+# 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 json
+from asyncio import Future
+from pathlib import Path
+from typing import Dict, List, Optional, Union
+
+import pytest
+from flaky import flaky
+from twisted.web import http
+
+from playwright.async_api import Browser, Error, Page, Request, Response, Route
+from tests.server import Server, TestServerRequest
+
+from .utils import Utils
+
+
+def adjust_server_headers(headers: Dict[str, str], browser_name: str) -> Dict[str, str]:
+ if browser_name != "firefox":
+ return headers
+ headers = headers.copy()
+ headers.pop("priority", None)
+ return headers
+
+
+async def test_request_fulfill(page: Page, server: Server) -> None:
+ async def handle_request(route: Route, request: Request) -> None:
+ headers = await route.request.all_headers()
+ assert headers["accept"]
+ assert route.request == request
+ assert repr(route) == f""
+ assert "empty.html" in request.url
+ assert request.headers["user-agent"]
+ assert request.method == "GET"
+ assert request.post_data is None
+ assert request.is_navigation_request()
+ assert request.resource_type == "document"
+ assert request.frame == page.main_frame
+ assert request.frame.url == "about:blank"
+ assert repr(request) == f""
+ await route.fulfill(body="Text")
+
+ await page.route(
+ "**/empty.html",
+ lambda route, request: asyncio.create_task(handle_request(route, request)),
+ )
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+
+ assert response.ok
+ assert repr(response) == f""
+ assert await response.text() == "Text"
+
+
+async def test_request_continue(page: Page, server: Server) -> None:
+ async def handle_request(route: Route, request: Request, intercepted: List[bool]) -> None:
+ intercepted.append(True)
+ await route.continue_()
+
+ intercepted: List[bool] = []
+ await page.route(
+ "**/*",
+ lambda route, request: asyncio.create_task(handle_request(route, request, intercepted)),
+ )
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+ assert intercepted == [True]
+ assert await page.title() == ""
+
+
+async def test_page_events_request_should_fire_for_navigation_requests(
+ page: Page, server: Server
+) -> None:
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await page.goto(server.EMPTY_PAGE)
+ assert len(requests) == 1
+
+
+async def test_page_events_request_should_accept_method(page: Page, server: Server) -> None:
+ class Log:
+ def __init__(self) -> None:
+ self.requests: List[Request] = []
+
+ def handle(self, request: Request) -> None:
+ self.requests.append(request)
+
+ log = Log()
+ page.on("request", log.handle)
+ await page.goto(server.EMPTY_PAGE)
+ assert len(log.requests) == 1
+
+
+async def test_page_events_request_should_fire_for_iframes(
+ page: Page, server: Server, utils: Utils
+) -> None:
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ assert len(requests) == 2
+
+
+async def test_page_events_request_should_fire_for_fetches(page: Page, server: Server) -> None:
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await page.goto(server.EMPTY_PAGE)
+ await page.evaluate('() => fetch("/empty.html")')
+ assert len(requests) == 2
+
+
+async def test_page_events_request_should_report_requests_and_responses_handled_by_service_worker(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html")
+ await page.evaluate("() => window.activationPromise")
+ sw_response = None
+ async with page.expect_request("**/*") as request_info:
+ sw_response = await page.evaluate('() => fetchDummy("foo")')
+ request = await request_info.value
+ assert sw_response == "responseFromServiceWorker:foo"
+ assert request.url == server.PREFIX + "/serviceworkers/fetchdummy/foo"
+ response = await request.response()
+ assert response
+ assert response.url == server.PREFIX + "/serviceworkers/fetchdummy/foo"
+ assert await response.text() == "responseFromServiceWorker:foo"
+
+
+async def test_request_frame_should_work_for_main_frame_navigation_request(
+ page: Page, server: Server
+) -> None:
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await page.goto(server.EMPTY_PAGE)
+ assert len(requests) == 1
+ assert requests[0].frame == page.main_frame
+
+
+async def test_request_frame_should_work_for_subframe_navigation_request(
+ page: Page, server: Server, utils: Utils
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ assert len(requests) == 1
+ assert requests[0].frame == page.frames[1]
+
+
+async def test_request_frame_should_work_for_fetch_requests(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ requests: List[Request] = []
+ page.on("request", lambda r: requests.append(r))
+ await page.evaluate('() => fetch("/digits/1.png")')
+ requests = [r for r in requests if "favicon" not in r.url]
+ assert len(requests) == 1
+ assert requests[0].frame == page.main_frame
+
+
+async def test_request_headers_should_work(
+ page: Page, server: Server, is_chromium: bool, is_firefox: bool, is_webkit: bool
+) -> None:
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ if is_chromium:
+ assert "Chrome" in response.request.headers["user-agent"]
+ elif is_firefox:
+ assert "Firefox" in response.request.headers["user-agent"]
+ elif is_webkit:
+ assert "WebKit" in response.request.headers["user-agent"]
+
+
+async def test_request_headers_should_get_the_same_headers_as_the_server(
+ page: Page,
+ server: Server,
+ is_webkit: bool,
+ is_win: bool,
+ browser_name: str,
+) -> None:
+ if is_webkit and is_win:
+ pytest.xfail("Curl does not show accept-encoding and accept-language")
+ server_request_headers_future: Future[Dict[str, str]] = asyncio.Future()
+
+ def handle(request: http.Request) -> None:
+ normalized_headers = {
+ key.decode().lower(): value[0].decode()
+ for key, value in request.requestHeaders.getAllRawHeaders()
+ }
+ server_request_headers_future.set_result(normalized_headers)
+ request.write(b"done")
+ request.finish()
+
+ server.set_route("/empty.html", handle)
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ server_headers = adjust_server_headers(await server_request_headers_future, browser_name)
+ assert await response.request.all_headers() == server_headers
+
+
+async def test_request_headers_should_get_the_same_headers_as_the_server_cors(
+ page: Page, server: Server, is_webkit: bool, is_win: bool, browser_name: str
+) -> None:
+ if is_webkit and is_win:
+ pytest.xfail("Curl does not show accept-encoding and accept-language")
+ await page.goto(server.PREFIX + "/empty.html")
+ server_request_headers_future: Future[Dict[str, str]] = asyncio.Future()
+
+ def handle_something(request: http.Request) -> None:
+ normalized_headers = {
+ key.decode().lower(): value[0].decode()
+ for key, value in request.requestHeaders.getAllRawHeaders()
+ }
+ server_request_headers_future.set_result(normalized_headers)
+ request.setHeader("Access-Control-Allow-Origin", "*")
+ request.write(b"done")
+ request.finish()
+
+ server.set_route("/something", handle_something)
+
+ text = None
+ async with page.expect_request("**/*") as request_info:
+ text = await page.evaluate(
+ """async url => {
+ const data = await fetch(url);
+ return data.text();
+ }""",
+ server.CROSS_PROCESS_PREFIX + "/something",
+ )
+ request = await request_info.value
+ assert text == "done"
+ server_headers = adjust_server_headers(await server_request_headers_future, browser_name)
+ assert await request.all_headers() == server_headers
+
+
+async def test_should_report_request_headers_array(
+ page: Page, server: Server, is_win: bool, browser_name: str
+) -> None:
+ if is_win and browser_name == "webkit":
+ pytest.skip("libcurl does not support non-set-cookie multivalue headers")
+ expected_headers = []
+
+ def handle(request: http.Request) -> None:
+ for name, values in request.requestHeaders.getAllRawHeaders():
+ for value in values:
+ if browser_name == "firefox" and name.decode().lower() == "priority":
+ continue
+ expected_headers.append({"name": name.decode().lower(), "value": value.decode()})
+ request.finish()
+
+ server.set_route("/headers", handle)
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_request("*/**") as request_info:
+ await page.evaluate(
+ """() => fetch('/headers', {
+ headers: [
+ ['header-a', 'value-a'],
+ ['header-b', 'value-b'],
+ ['header-a', 'value-a-1'],
+ ['header-a', 'value-a-2'],
+ ]
+ })
+ """
+ )
+ request = await request_info.value
+ sorted_pw_request_headers = sorted(
+ list(
+ map(
+ lambda header: {
+ "name": header["name"].lower(),
+ "value": header["value"],
+ },
+ await request.headers_array(),
+ )
+ ),
+ key=lambda header: header["name"],
+ )
+ sorted_expected_headers = sorted(expected_headers, key=lambda header: header["name"])
+ assert sorted_pw_request_headers == sorted_expected_headers
+ assert await request.header_value("Header-A") == "value-a, value-a-1, value-a-2"
+ assert await request.header_value("not-there") is None
+
+
+async def test_should_report_response_headers_array(
+ page: Page, server: Server, is_win: bool, browser_name: str
+) -> None:
+ if is_win and browser_name == "webkit":
+ pytest.skip("libcurl does not support non-set-cookie multivalue headers")
+ expected_headers = {
+ "header-a": ["value-a", "value-a-1", "value-a-2"],
+ "header-b": ["value-b"],
+ "set-cookie": ["a=b", "c=d"],
+ }
+
+ def handle(request: http.Request) -> None:
+ for key in expected_headers:
+ for value in expected_headers[key]:
+ request.responseHeaders.addRawHeader(key, value)
+ request.finish()
+
+ server.set_route("/headers", handle)
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_response("*/**") as response_info:
+ await page.evaluate(
+ """() => fetch('/headers')
+ """
+ )
+ response = await response_info.value
+ actual_headers: Dict[str, List[str]] = {}
+ for header in await response.headers_array():
+ name = header["name"].lower()
+ value = header["value"]
+ if not actual_headers.get(name):
+ actual_headers[name] = []
+ actual_headers[name].append(value)
+
+ for key in ["Keep-Alive", "Connection", "Date", "Transfer-Encoding"]:
+ if key in actual_headers:
+ actual_headers.pop(key)
+ if key.lower() in actual_headers:
+ actual_headers.pop(key.lower())
+ assert actual_headers == expected_headers
+ assert await response.header_value("not-there") is None
+ assert await response.header_value("set-cookie") == "a=b\nc=d"
+ assert await response.header_value("header-a") == "value-a, value-a-1, value-a-2"
+ assert await response.header_values("set-cookie") == ["a=b", "c=d"]
+
+
+async def test_response_headers_should_work(page: Page, server: Server) -> None:
+ server.set_route("/empty.html", lambda r: (r.setHeader("foo", "bar"), r.finish()))
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.headers["foo"] == "bar"
+ assert (await response.all_headers())["foo"] == "bar"
+
+
+async def test_request_post_data_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ server.set_route("/post", lambda r: r.finish())
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await page.evaluate(
+ '() => fetch("./post", { method: "POST", body: JSON.stringify({foo: "bar"})})'
+ )
+ assert len(requests) == 1
+ assert requests[0].post_data == '{"foo":"bar"}'
+
+
+async def test_request_post_data__should_be_undefined_when_there_is_no_post_data(
+ page: Page, server: Server
+) -> None:
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.request.post_data is None
+
+
+async def test_should_parse_the_json_post_data(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ server.set_route("/post", lambda req: req.finish())
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await page.evaluate(
+ """() => fetch('./post', { method: 'POST', body: JSON.stringify({ foo: 'bar' }) })"""
+ )
+ assert len(requests) == 1
+ assert requests[0].post_data_json == {"foo": "bar"}
+
+
+async def test_should_parse_the_data_if_content_type_is_form_urlencoded(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ server.set_route("/post", lambda req: req.finish())
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await page.set_content(
+ """"""
+ )
+ await page.click("input[type=submit]")
+ assert len(requests) == 1
+ assert requests[0].post_data_json == {"foo": "bar", "baz": "123"}
+
+
+async def test_should_be_undefined_when_there_is_no_post_data(page: Page, server: Server) -> None:
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.request.post_data_json is None
+
+
+async def test_should_return_post_data_without_content_type(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_request("**/*") as request_info:
+ await page.evaluate(
+ """({url}) => {
+ const request = new Request(url, {
+ method: 'POST',
+ body: JSON.stringify({ value: 42 }),
+ });
+ request.headers.set('content-type', '');
+ return fetch(request);
+ }""",
+ {"url": server.PREFIX + "/title.html"},
+ )
+ request = await request_info.value
+ assert request.post_data_json == {"value": 42}
+
+
+async def test_should_throw_on_invalid_json_in_post_data(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_request("**/*") as request_info:
+ await page.evaluate(
+ """({url}) => {
+ const request = new Request(url, {
+ method: 'POST',
+ body: '',
+ });
+ request.headers.set('content-type', '');
+ return fetch(request);
+ }""",
+ {"url": server.PREFIX + "/title.html"},
+ )
+ request = await request_info.value
+ with pytest.raises(Error) as exc_info:
+ print(request.post_data_json)
+ assert "POST data is not a valid JSON object: " in str(exc_info.value)
+
+
+async def test_should_work_with_binary_post_data(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ server.set_route("/post", lambda req: req.finish())
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await page.evaluate(
+ """async () => {
+ await fetch('./post', { method: 'POST', body: new Uint8Array(Array.from(Array(256).keys())) })
+ }"""
+ )
+ assert len(requests) == 1
+ buffer = requests[0].post_data_buffer
+ assert len(buffer) == 256
+ for i in range(256):
+ assert buffer[i] == i
+
+
+async def test_should_work_with_binary_post_data_and_interception(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ server.set_route("/post", lambda req: req.finish())
+ requests = []
+ await page.route("/post", lambda route: asyncio.ensure_future(route.continue_()))
+ page.on("request", lambda r: requests.append(r))
+ await page.evaluate(
+ """async () => {
+ await fetch('./post', { method: 'POST', body: new Uint8Array(Array.from(Array(256).keys())) })
+ }"""
+ )
+ assert len(requests) == 1
+ buffer = requests[0].post_data_buffer
+ assert len(buffer) == 256
+ for i in range(256):
+ assert buffer[i] == i
+
+
+async def test_response_text_should_work(page: Page, server: Server) -> None:
+ response = await page.goto(server.PREFIX + "/simple.json")
+ assert response
+ assert await response.text() == '{"foo": "bar"}\n'
+
+
+async def test_response_text_should_return_uncompressed_text(page: Page, server: Server) -> None:
+ server.enable_gzip("/simple.json")
+ response = await page.goto(server.PREFIX + "/simple.json")
+ assert response
+ assert response.headers["content-encoding"] == "gzip"
+ assert await response.text() == '{"foo": "bar"}\n'
+
+
+async def test_response_text_should_throw_when_requesting_body_of_redirected_response(
+ page: Page, server: Server
+) -> None:
+ server.set_redirect("/foo.html", "/empty.html")
+ response = await page.goto(server.PREFIX + "/foo.html")
+ assert response
+ redirected_from = response.request.redirected_from
+ assert redirected_from
+ redirected = await redirected_from.response()
+ assert redirected
+ assert redirected.status == 302
+ error: Optional[Error] = None
+ try:
+ await redirected.text()
+ except Error as exc:
+ error = exc
+ assert error
+ assert "Response body is unavailable for redirect responses" in error.message
+
+
+async def test_response_json_should_work(page: Page, server: Server) -> None:
+ response = await page.goto(server.PREFIX + "/simple.json")
+ assert response
+ assert await response.json() == {"foo": "bar"}
+
+
+async def test_response_body_should_work(page: Page, server: Server, assetdir: Path) -> None:
+ response = await page.goto(server.PREFIX + "/pptr.png")
+ assert response
+ with open(
+ assetdir / "pptr.png",
+ "rb",
+ ) as fd:
+ assert fd.read() == await response.body()
+
+
+async def test_response_body_should_work_with_compression(
+ page: Page, server: Server, assetdir: Path
+) -> None:
+ server.enable_gzip("/pptr.png")
+ response = await page.goto(server.PREFIX + "/pptr.png")
+ assert response
+ with open(
+ assetdir / "pptr.png",
+ "rb",
+ ) as fd:
+ assert fd.read() == await response.body()
+
+
+async def test_response_status_text_should_work(page: Page, server: Server) -> None:
+ server.set_route("/cool", lambda r: (r.setResponseCode(200, b"cool!"), r.finish()))
+
+ response = await page.goto(server.PREFIX + "/cool")
+ assert response
+ assert response.status_text == "cool!"
+
+
+async def test_request_resource_type_should_return_event_source(page: Page, server: Server) -> None:
+ SSE_MESSAGE = {"foo": "bar"}
+ # 1. Setup server-sent events on server that immediately sends a message to the client.
+ server.set_route(
+ "/sse",
+ lambda r: (
+ r.setHeader("Content-Type", "text/event-stream"),
+ r.setHeader("Connection", "keep-alive"),
+ r.setHeader("Cache-Control", "no-cache"),
+ r.setResponseCode(200),
+ r.write(f"data: {json.dumps(SSE_MESSAGE)}\n\n".encode()),
+ r.finish(),
+ ),
+ )
+
+ # 2. Subscribe to page request events.
+ await page.goto(server.EMPTY_PAGE)
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ # 3. Connect to EventSource in browser and return first message.
+ assert (
+ await page.evaluate(
+ """() => {
+ const eventSource = new EventSource('/sse');
+ return new Promise(resolve => {
+ eventSource.onmessage = e => resolve(JSON.parse(e.data));
+ });
+ }"""
+ )
+ == SSE_MESSAGE
+ )
+ assert requests[0].resource_type == "eventsource"
+
+
+async def test_network_events_request(page: Page, server: Server) -> None:
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await page.goto(server.EMPTY_PAGE)
+ assert len(requests) == 1
+ assert requests[0].url == server.EMPTY_PAGE
+ assert requests[0].resource_type == "document"
+ assert requests[0].method == "GET"
+ assert await requests[0].response()
+ assert requests[0].frame == page.main_frame
+ assert requests[0].frame.url == server.EMPTY_PAGE
+
+
+async def test_network_events_response(page: Page, server: Server) -> None:
+ responses = []
+ page.on("response", lambda r: responses.append(r))
+ await page.goto(server.EMPTY_PAGE)
+ assert len(responses) == 1
+ assert responses[0].url == server.EMPTY_PAGE
+ assert responses[0].status == 200
+ assert responses[0].ok
+ assert responses[0].request
+
+
+async def test_network_events_request_failed(
+ page: Page,
+ server: Server,
+ is_chromium: bool,
+ is_webkit: bool,
+ is_mac: bool,
+ is_win: bool,
+) -> None:
+ def handle_request(request: TestServerRequest) -> None:
+ request.setHeader("Content-Type", "text/css")
+ request.loseConnection()
+
+ server.set_route("/one-style.css", handle_request)
+
+ failed_requests = []
+ page.on("requestfailed", lambda request: failed_requests.append(request))
+ await page.goto(server.PREFIX + "/one-style.html")
+ # TODO: https://github.com/microsoft/playwright/issues/12789
+ assert len(failed_requests) >= 1
+ assert "one-style.css" in failed_requests[0].url
+ assert await failed_requests[0].response() is None
+ assert failed_requests[0].resource_type == "stylesheet"
+ if is_chromium:
+ assert failed_requests[0].failure == "net::ERR_EMPTY_RESPONSE"
+ elif is_webkit:
+ if is_mac:
+ assert failed_requests[0].failure == "The network connection was lost."
+ elif is_win:
+ assert failed_requests[0].failure == "Server returned nothing (no headers, no data)"
+ else:
+ assert failed_requests[0].failure in [
+ "Message Corrupt",
+ "Connection terminated unexpectedly",
+ ]
+ else:
+ assert failed_requests[0].failure == "NS_ERROR_NET_RESET"
+ assert failed_requests[0].frame
+
+
+async def test_network_events_request_finished(page: Page, server: Server) -> None:
+ async with page.expect_event("requestfinished") as event_info:
+ await page.goto(server.EMPTY_PAGE)
+ request = await event_info.value
+ assert request.url == server.EMPTY_PAGE
+ assert await request.response()
+ assert request.frame == page.main_frame
+ assert request.frame.url == server.EMPTY_PAGE
+
+
+async def test_network_events_should_fire_events_in_proper_order(
+ page: Page, server: Server
+) -> None:
+ events = []
+ page.on("request", lambda request: events.append("request"))
+ page.on("response", lambda response: events.append("response"))
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ await response.finished()
+ events.append("requestfinished")
+ assert events == ["request", "response", "requestfinished"]
+
+
+async def test_network_events_should_support_redirects(page: Page, server: Server) -> None:
+ FOO_URL = server.PREFIX + "/foo.html"
+ events: Dict[str, List[Union[str, int]]] = {}
+ events[FOO_URL] = []
+ events[server.EMPTY_PAGE] = []
+
+ def _handle_on_request(request: Request) -> None:
+ events[request.url].append(request.method)
+
+ page.on("request", _handle_on_request)
+
+ def _handle_on_response(response: Response) -> None:
+ events[response.url].append(response.status)
+
+ page.on("response", _handle_on_response)
+
+ def _handle_on_requestfinished(request: Request) -> None:
+ events[request.url].append("DONE")
+
+ page.on("requestfinished", _handle_on_requestfinished)
+
+ def _handle_on_requestfailed(request: Request) -> None:
+ events[request.url].append("FAIL")
+
+ page.on("requestfailed", _handle_on_requestfailed)
+ server.set_redirect("/foo.html", "/empty.html")
+ response = await page.goto(FOO_URL)
+ assert response
+ await response.finished()
+ expected = {}
+ expected[FOO_URL] = ["GET", 302, "DONE"]
+ expected[server.EMPTY_PAGE] = ["GET", 200, "DONE"]
+ assert events == expected
+ redirected_from = response.request.redirected_from
+ assert redirected_from
+ assert "/foo.html" in redirected_from.url
+ assert redirected_from.redirected_from is None
+ assert redirected_from.redirected_to == response.request
+
+
+async def test_request_is_navigation_request_should_work(page: Page, server: Server) -> None:
+ requests: Dict[str, Request] = {}
+
+ def handle_request(request: Request) -> None:
+ requests[request.url.split("/").pop()] = request
+
+ page.on("request", handle_request)
+ server.set_redirect("/rrredirect", "/frames/one-frame.html")
+ await page.goto(server.PREFIX + "/rrredirect")
+ assert requests["rrredirect"].is_navigation_request()
+ assert requests["one-frame.html"].is_navigation_request()
+ assert requests["frame.html"].is_navigation_request()
+ assert requests["script.js"].is_navigation_request() is False
+ assert requests["style.css"].is_navigation_request() is False
+
+
+async def test_request_is_navigation_request_should_work_when_navigating_to_image(
+ page: Page, server: Server
+) -> None:
+ requests = []
+ page.on("request", lambda r: requests.append(r))
+ await page.goto(server.PREFIX + "/pptr.png")
+ assert requests[0].is_navigation_request()
+
+
+async def test_set_extra_http_headers_should_work(page: Page, server: Server) -> None:
+ await page.set_extra_http_headers({"foo": "bar"})
+
+ request = (
+ await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ page.goto(server.EMPTY_PAGE),
+ )
+ )[0]
+ assert request.getHeader("foo") == "bar"
+
+
+async def test_set_extra_http_headers_should_work_with_redirects(
+ page: Page, server: Server
+) -> None:
+ server.set_redirect("/foo.html", "/empty.html")
+ await page.set_extra_http_headers({"foo": "bar"})
+
+ request = (
+ await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ page.goto(server.PREFIX + "/foo.html"),
+ )
+ )[0]
+ assert request.getHeader("foo") == "bar"
+
+
+async def test_set_extra_http_headers_should_work_with_extra_headers_from_browser_context(
+ browser: Browser, server: Server
+) -> None:
+ context = await browser.new_context()
+ await context.set_extra_http_headers({"foo": "bar"})
+
+ page = await context.new_page()
+ request = (
+ await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ page.goto(server.EMPTY_PAGE),
+ )
+ )[0]
+ await context.close()
+ assert request.getHeader("foo") == "bar"
+
+
+@flaky # Flaky upstream https://devops.aslushnikov.com/flakiness2.html#filter_spec=should+override+extra+headers+from+browser+context&test_parameter_filters=%5B%5B%22browserName%22%2C%5B%5B%22webkit%22%2C%22include%22%5D%5D%5D%2C%5B%22video%22%2C%5B%5Btrue%2C%22exclude%22%5D%5D%5D%2C%5B%22platform%22%2C%5B%5B%22Windows%22%2C%22include%22%5D%5D%5D%5D
+async def test_set_extra_http_headers_should_override_extra_headers_from_browser_context(
+ browser: Browser, server: Server
+) -> None:
+ context = await browser.new_context(extra_http_headers={"fOo": "bAr", "baR": "foO"})
+
+ page = await context.new_page()
+ await page.set_extra_http_headers({"Foo": "Bar"})
+
+ request = (
+ await asyncio.gather(
+ server.wait_for_request("/empty.html"),
+ page.goto(server.EMPTY_PAGE),
+ )
+ )[0]
+ await context.close()
+ assert request.getHeader("foo") == "Bar"
+ assert request.getHeader("bar") == "foO"
+
+
+async def test_set_extra_http_headers_should_throw_for_non_string_header_values(
+ page: Page,
+) -> None:
+ error: Optional[Error] = None
+ try:
+ await page.set_extra_http_headers({"foo": 1}) # type: ignore
+ except Error as exc:
+ error = exc
+ assert error
+ assert (
+ error.message
+ == "Page.set_extra_http_headers: headers[0].value: expected string, got number"
+ )
+
+
+async def test_response_server_addr(page: Page, server: Server) -> None:
+ response = await page.goto(f"http://127.0.0.1:{server.PORT}")
+ assert response
+ server_addr = await response.server_addr()
+ assert server_addr
+ assert server_addr["port"] == server.PORT
+ assert server_addr["ipAddress"] in ["127.0.0.1", "::1"]
+
+
+async def test_response_security_details(
+ browser: Browser,
+ https_server: Server,
+ browser_name: str,
+ is_win: bool,
+ is_linux: bool,
+) -> None:
+ if (browser_name == "webkit" and is_linux) or (browser_name == "webkit" and is_win):
+ pytest.skip("https://github.com/microsoft/playwright/issues/6759")
+ page = await browser.new_page(ignore_https_errors=True)
+ response = await page.goto(https_server.EMPTY_PAGE)
+ assert response
+ await response.finished()
+ security_details = await response.security_details()
+ assert security_details
+ if browser_name == "webkit" and is_win:
+ assert security_details == {
+ "subjectName": "puppeteer-tests",
+ "validFrom": 1550084863,
+ "validTo": -1,
+ }
+ elif browser_name == "webkit":
+ assert security_details == {
+ "protocol": "TLS 1.3",
+ "subjectName": "puppeteer-tests",
+ "validFrom": 1550084863,
+ "validTo": 33086084863,
+ }
+ else:
+ assert security_details == {
+ "issuer": "puppeteer-tests",
+ "protocol": "TLS 1.3",
+ "subjectName": "puppeteer-tests",
+ "validFrom": 1550084863,
+ "validTo": 33086084863,
+ }
+ await page.close()
+
+
+async def test_response_security_details_none_without_https(page: Page, server: Server) -> None:
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ security_details = await response.security_details()
+ assert security_details is None
+
+
+async def test_should_report_if_request_was_from_service_worker(page: Page, server: Server) -> None:
+ response = await page.goto(server.PREFIX + "/serviceworkers/fetch/sw.html")
+ assert response
+ assert not response.from_service_worker
+ await page.evaluate("() => window.activationPromise")
+ async with page.expect_response("**/example.txt") as response_info:
+ await page.evaluate("() => fetch('/example.txt')")
+ response = await response_info.value
+ assert response.from_service_worker
diff --git a/tests/async/test_page.py b/tests/async/test_page.py
new file mode 100644
index 0000000..3fc2d78
--- /dev/null
+++ b/tests/async/test_page.py
@@ -0,0 +1,1358 @@
+# 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 re
+from pathlib import Path
+from typing import Dict, List, Optional
+
+import pytest
+
+from playwright.async_api import (
+ BrowserContext,
+ Error,
+ JSHandle,
+ Page,
+ Route,
+ TimeoutError,
+)
+from tests.server import Server, TestServerRequest
+from tests.utils import TARGET_CLOSED_ERROR_MESSAGE, must
+
+
+async def test_close_should_reject_all_promises(context: BrowserContext) -> None:
+ new_page = await context.new_page()
+ with pytest.raises(Error) as exc_info:
+ await asyncio.gather(new_page.evaluate("() => new Promise(r => {})"), new_page.close())
+ assert " closed" in exc_info.value.message
+
+
+async def test_closed_should_not_visible_in_context_pages(
+ context: BrowserContext,
+) -> None:
+ page = await context.new_page()
+ assert page in context.pages
+ await page.close()
+ assert page not in context.pages
+
+
+async def test_close_should_run_beforeunload_if_asked_for(
+ context: BrowserContext, server: Server, is_chromium: bool, is_webkit: bool
+) -> None:
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/beforeunload.html")
+ # We have to interact with a page so that 'beforeunload' handlers
+ # fire.
+ await page.click("body")
+
+ async with page.expect_event("dialog") as dialog_info:
+ await page.close(run_before_unload=True)
+ dialog = await dialog_info.value
+
+ assert dialog.type == "beforeunload"
+ assert dialog.default_value == ""
+ if is_chromium:
+ assert dialog.message == ""
+ elif is_webkit:
+ assert dialog.message == "Leave?"
+ else:
+ assert "This page is asking you to confirm that you want to leave" in dialog.message
+ async with page.expect_event("close"):
+ await dialog.accept()
+
+
+async def test_close_should_not_run_beforeunload_by_default(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/beforeunload.html")
+ # We have to interact with a page so that 'beforeunload' handlers
+ # fire.
+ await page.click("body")
+ await page.close()
+
+
+async def test_should_be_able_to_navigate_away_from_page_with_before_unload(
+ server: Server, page: Page
+) -> None:
+ await page.goto(server.PREFIX + "/beforeunload.html")
+ # We have to interact with a page so that 'beforeunload' handlers
+ # fire.
+ await page.click("body")
+ await page.goto(server.EMPTY_PAGE)
+
+
+async def test_close_should_set_the_page_close_state(context: BrowserContext) -> None:
+ page = await context.new_page()
+ assert page.is_closed() is False
+ await page.close()
+ assert page.is_closed()
+
+
+async def test_close_should_terminate_network_waiters(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+
+ async def wait_for_request() -> Error:
+ with pytest.raises(Error) as exc_info:
+ async with page.expect_request(server.EMPTY_PAGE):
+ pass
+ return exc_info.value
+
+ async def wait_for_response() -> Error:
+ with pytest.raises(Error) as exc_info:
+ async with page.expect_response(server.EMPTY_PAGE):
+ pass
+ return exc_info.value
+
+ results = await asyncio.gather(wait_for_request(), wait_for_response(), page.close())
+ for i in range(2):
+ error = results[i]
+ assert error
+ assert TARGET_CLOSED_ERROR_MESSAGE in error.message
+ assert "Timeout" not in error.message
+
+
+async def test_close_should_be_callable_twice(context: BrowserContext) -> None:
+ page = await context.new_page()
+ await asyncio.gather(
+ page.close(),
+ page.close(),
+ )
+ await page.close()
+
+
+async def test_load_should_fire_when_expected(page: Page) -> None:
+ async with page.expect_event("load"):
+ await page.goto("about:blank")
+
+
+@pytest.mark.skip("FIXME")
+async def test_should_work_with_wait_for_loadstate(page: Page, server: Server) -> None:
+ messages = []
+
+ def _handler(request: TestServerRequest) -> None:
+ messages.append("route")
+ request.setHeader("Content-Type", "text/html")
+ request.write(b" ")
+ request.finish()
+
+ server.set_route(
+ "/empty.html",
+ _handler,
+ )
+
+ await page.set_content(f'empty.html ')
+
+ async def wait_for_clickload() -> None:
+ await page.click("a")
+ await page.wait_for_load_state("load")
+ messages.append("clickload")
+
+ async def wait_for_page_load() -> None:
+ await page.wait_for_event("load")
+ messages.append("load")
+
+ await asyncio.gather(
+ wait_for_clickload(),
+ wait_for_page_load(),
+ )
+
+ assert messages == ["route", "load", "clickload"]
+
+
+async def test_async_stacks_should_work(page: Page, server: Server) -> None:
+ await page.route("**/empty.html", lambda route, response: asyncio.create_task(route.abort()))
+ with pytest.raises(Error) as exc_info:
+ await page.goto(server.EMPTY_PAGE)
+ assert exc_info.value.stack
+ assert __file__ in exc_info.value.stack
+
+
+async def test_opener_should_provide_access_to_the_opener_page(page: Page) -> None:
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("window.open('about:blank')")
+ popup = await popup_info.value
+ opener = await popup.opener()
+ assert opener == page
+
+
+async def test_opener_should_return_null_if_parent_page_has_been_closed(
+ page: Page,
+) -> None:
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("window.open('about:blank')")
+ popup = await popup_info.value
+ await page.close()
+ opener = await popup.opener()
+ assert opener is None
+
+
+async def test_domcontentloaded_should_fire_when_expected(page: Page, server: Server) -> None:
+ future = asyncio.create_task(page.goto("about:blank"))
+ async with page.expect_event("domcontentloaded"):
+ pass
+ await future
+
+
+async def test_wait_for_request(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_request(server.PREFIX + "/digits/2.png") as request_info:
+ await page.evaluate(
+ """() => {
+ fetch('/digits/1.png')
+ fetch('/digits/2.png')
+ fetch('/digits/3.png')
+ }"""
+ )
+ request = await request_info.value
+ assert request.url == server.PREFIX + "/digits/2.png"
+
+
+async def test_wait_for_request_should_work_with_predicate(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_request(
+ lambda request: request.url == server.PREFIX + "/digits/2.png"
+ ) as request_info:
+ await page.evaluate(
+ """() => {
+ fetch('/digits/1.png')
+ fetch('/digits/2.png')
+ fetch('/digits/3.png')
+ }"""
+ )
+ request = await request_info.value
+ assert request.url == server.PREFIX + "/digits/2.png"
+
+
+async def test_wait_for_request_should_timeout(page: Page, server: Server) -> None:
+ with pytest.raises(Error) as exc_info:
+ async with page.expect_event("request", timeout=1):
+ pass
+ assert exc_info.type is TimeoutError
+
+
+async def test_wait_for_request_should_respect_default_timeout(page: Page, server: Server) -> None:
+ page.set_default_timeout(1)
+ with pytest.raises(Error) as exc_info:
+ async with page.expect_event("request", lambda _: False):
+ pass
+ assert exc_info.type is TimeoutError
+
+
+async def test_wait_for_request_should_work_with_no_timeout(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_request(server.PREFIX + "/digits/2.png", timeout=0) as request_info:
+ await page.evaluate(
+ """() => setTimeout(() => {
+ fetch('/digits/1.png')
+ fetch('/digits/2.png')
+ fetch('/digits/3.png')
+ }, 50)"""
+ )
+ request = await request_info.value
+ assert request.url == server.PREFIX + "/digits/2.png"
+
+
+async def test_wait_for_request_should_work_with_url_match(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_request(re.compile(r"digits\/\d\.png")) as request_info:
+ await page.evaluate("fetch('/digits/1.png')")
+ request = await request_info.value
+ assert request.url == server.PREFIX + "/digits/1.png"
+
+
+async def test_wait_for_event_should_fail_with_error_upon_disconnect(
+ page: Page,
+) -> None:
+ with pytest.raises(Error) as exc_info:
+ async with page.expect_download():
+ await page.close()
+ assert TARGET_CLOSED_ERROR_MESSAGE in exc_info.value.message
+
+
+async def test_wait_for_response_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_response(server.PREFIX + "/digits/2.png") as response_info:
+ await page.evaluate(
+ """() => {
+ fetch('/digits/1.png')
+ fetch('/digits/2.png')
+ fetch('/digits/3.png')
+ }"""
+ )
+ response = await response_info.value
+ assert response.url == server.PREFIX + "/digits/2.png"
+
+
+async def test_wait_for_response_should_respect_timeout(page: Page) -> None:
+ with pytest.raises(Error) as exc_info:
+ async with page.expect_response("**/*", timeout=1):
+ pass
+ assert exc_info.type is TimeoutError
+
+
+async def test_wait_for_response_should_respect_default_timeout(page: Page) -> None:
+ page.set_default_timeout(1)
+ with pytest.raises(Error) as exc_info:
+ async with page.expect_response(lambda _: False):
+ pass
+ assert exc_info.type is TimeoutError
+
+
+async def test_wait_for_response_should_work_with_predicate(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_response(
+ lambda response: response.url == server.PREFIX + "/digits/2.png"
+ ) as response_info:
+ await page.evaluate(
+ """() => {
+ fetch('/digits/1.png')
+ fetch('/digits/2.png')
+ fetch('/digits/3.png')
+ }"""
+ )
+ response = await response_info.value
+ assert response.url == server.PREFIX + "/digits/2.png"
+
+
+async def test_wait_for_response_should_work_with_no_timeout(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_response(server.PREFIX + "/digits/2.png") as response_info:
+ await page.evaluate(
+ """() => {
+ fetch('/digits/1.png')
+ fetch('/digits/2.png')
+ fetch('/digits/3.png')
+ }"""
+ )
+ response = await response_info.value
+ assert response.url == server.PREFIX + "/digits/2.png"
+
+
+async def test_wait_for_response_should_use_context_timeout(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+
+ context.set_default_timeout(1_000)
+ with pytest.raises(Error) as exc_info:
+ async with page.expect_response("https://playwright.dev"):
+ pass
+ assert exc_info.type is TimeoutError
+ assert "Timeout 1000ms exceeded" in exc_info.value.message
+
+
+async def test_expect_response_should_not_hang_when_predicate_throws(
+ page: Page,
+) -> None:
+ with pytest.raises(Exception, match="Oops!"):
+ async with page.expect_response("**/*"):
+ raise Exception("Oops!")
+
+
+async def test_expose_binding(page: Page) -> None:
+ binding_source = []
+
+ def binding(source: Dict, a: int, b: int) -> int:
+ binding_source.append(source)
+ return a + b
+
+ await page.expose_binding("add", lambda source, a, b: binding(source, a, b))
+
+ result = await page.evaluate("add(5, 6)")
+
+ assert binding_source[0]["context"] == page.context
+ assert binding_source[0]["page"] == page
+ assert binding_source[0]["frame"] == page.main_frame
+ assert result == 11
+
+
+async def test_expose_function(page: Page, server: Server) -> None:
+ await page.expose_function("compute", lambda a, b: a * b)
+ result = await page.evaluate("compute(9, 4)")
+ assert result == 36
+
+
+async def test_expose_function_should_throw_exception_in_page_context(
+ page: Page, server: Server
+) -> None:
+ def throw() -> None:
+ raise Exception("WOOF WOOF")
+
+ await page.expose_function("woof", lambda: throw())
+ result = await page.evaluate(
+ """async() => {
+ try {
+ await woof()
+ } catch (e) {
+ return {message: e.message, stack: e.stack}
+ }
+ }"""
+ )
+ assert result["message"] == "WOOF WOOF"
+ assert __file__ in result["stack"]
+
+
+async def test_expose_function_should_be_callable_from_inside_add_init_script(
+ page: Page,
+) -> None:
+ called = []
+ await page.expose_function("woof", lambda: called.append(True))
+ await page.add_init_script("woof()")
+ await page.reload()
+ assert called == [True]
+
+
+async def test_expose_function_should_survive_navigation(page: Page, server: Server) -> None:
+ await page.expose_function("compute", lambda a, b: a * b)
+ await page.goto(server.EMPTY_PAGE)
+ result = await page.evaluate("compute(9, 4)")
+ assert result == 36
+
+
+async def test_expose_function_should_await_returned_promise(page: Page) -> None:
+ async def mul(a: int, b: int) -> int:
+ return a * b
+
+ await page.expose_function("compute", mul)
+ assert await page.evaluate("compute(3, 5)") == 15
+
+
+async def test_expose_function_should_work_on_frames(page: Page, server: Server) -> None:
+ await page.expose_function("compute", lambda a, b: a * b)
+ await page.goto(server.PREFIX + "/frames/nested-frames.html")
+ frame = page.frames[1]
+ assert await frame.evaluate("compute(3, 5)") == 15
+
+
+async def test_expose_function_should_work_on_frames_before_navigation(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/frames/nested-frames.html")
+ await page.expose_function("compute", lambda a, b: a * b)
+ frame = page.frames[1]
+ assert await frame.evaluate("compute(3, 5)") == 15
+
+
+async def test_expose_function_should_work_after_cross_origin_navigation(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.expose_function("compute", lambda a, b: a * b)
+ await page.goto(server.CROSS_PROCESS_PREFIX + "/empty.html")
+ assert await page.evaluate("compute(9, 4)") == 36
+
+
+async def test_expose_function_should_work_with_complex_objects(page: Page, server: Server) -> None:
+ await page.expose_function("complexObject", lambda a, b: dict(x=a["x"] + b["x"]))
+ result = await page.evaluate("complexObject({x: 5}, {x: 2})")
+ assert result["x"] == 7
+
+
+async def test_expose_bindinghandle_should_work(page: Page, server: Server) -> None:
+ targets: List[JSHandle] = []
+
+ def logme(t: JSHandle) -> int:
+ targets.append(t)
+ return 17
+
+ await page.expose_binding("logme", lambda source, t: logme(t), handle=True)
+ result = await page.evaluate("logme({ foo: 42 })")
+ assert (await targets[0].evaluate("x => x.foo")) == 42
+ assert result == 17
+
+
+async def test_page_error_should_fire(page: Page, server: Server, browser_name: str) -> None:
+ url = server.PREFIX + "/error.html"
+ async with page.expect_event("pageerror") as error_info:
+ await page.goto(url)
+ error = await error_info.value
+ assert error.name == "Error"
+ assert error.message == "Fancy error!"
+ # Note that WebKit reports the stack of the 'throw' statement instead of the Error constructor call.
+ if browser_name == "chromium":
+ assert (
+ error.stack
+ == """Error: Fancy error!
+ at c (myscript.js:14:11)
+ at b (myscript.js:10:5)
+ at a (myscript.js:6:5)
+ at myscript.js:3:1"""
+ )
+ if browser_name == "firefox":
+ assert (
+ error.stack
+ == """Error: Fancy error!
+ at c (myscript.js:14:11)
+ at b (myscript.js:10:5)
+ at a (myscript.js:6:5)
+ at (myscript.js:3:1)"""
+ )
+ if browser_name == "webkit":
+ assert (
+ error.stack
+ == f"""Error: Fancy error!
+ at c ({url}:14:36)
+ at b ({url}:10:6)
+ at a ({url}:6:6)
+ at global code ({url}:3:2)"""
+ )
+
+
+async def test_page_error_should_handle_odd_values(page: Page) -> None:
+ cases = [["null", "null"], ["undefined", "undefined"], ["0", "0"], ['""', ""]]
+ for [value, message] in cases:
+ async with page.expect_event("pageerror") as error_info:
+ await page.evaluate(f"() => setTimeout(() => {{ throw {value}; }}, 0)")
+ error = await error_info.value
+ assert error.message == message
+
+
+async def test_page_error_should_handle_object(page: Page, is_chromium: bool) -> None:
+ async with page.expect_event("pageerror") as error_info:
+ await page.evaluate("() => setTimeout(() => { throw {}; }, 0)")
+ error = await error_info.value
+ assert error.message == "Object" if is_chromium else "[object Object]"
+
+
+async def test_page_error_should_handle_window(page: Page, is_chromium: bool) -> None:
+ async with page.expect_event("pageerror") as error_info:
+ await page.evaluate("() => setTimeout(() => { throw window; }, 0)")
+ error = await error_info.value
+ assert error.message == "Window" if is_chromium else "[object Window]"
+
+
+async def test_page_error_should_pass_error_name_property(page: Page) -> None:
+ async with page.expect_event("pageerror") as error_info:
+ await page.evaluate(
+ """() => setTimeout(() => {
+ const error = new Error("my-message");
+ error.name = "my-name";
+ throw error;
+ }, 0)
+ """
+ )
+ error = await error_info.value
+ assert error.message == "my-message"
+ assert error.name == "my-name"
+
+
+expected_output = "hello
"
+
+
+async def test_set_content_should_work(page: Page, server: Server) -> None:
+ await page.set_content("hello
")
+ result = await page.content()
+ assert result == expected_output
+
+
+async def test_set_content_should_work_with_domcontentloaded(page: Page, server: Server) -> None:
+ await page.set_content("hello
", wait_until="domcontentloaded")
+ result = await page.content()
+ assert result == expected_output
+
+
+async def test_set_content_should_work_with_doctype(page: Page, server: Server) -> None:
+ doctype = ""
+ await page.set_content(f"{doctype}hello
")
+ result = await page.content()
+ assert result == f"{doctype}{expected_output}"
+
+
+async def test_set_content_should_work_with_HTML_4_doctype(page: Page, server: Server) -> None:
+ doctype = (
+ ''
+ )
+ await page.set_content(f"{doctype}hello
")
+ result = await page.content()
+ assert result == f"{doctype}{expected_output}"
+
+
+async def test_set_content_should_respect_timeout(page: Page, server: Server) -> None:
+ img_path = "/img.png"
+ # stall for image
+ server.set_route(img_path, lambda request: None)
+ with pytest.raises(Error) as exc_info:
+ await page.set_content(f' ', timeout=1)
+ assert exc_info.type is TimeoutError
+
+
+async def test_set_content_should_respect_default_navigation_timeout(
+ page: Page, server: Server
+) -> None:
+ page.set_default_navigation_timeout(1)
+ img_path = "/img.png"
+ # stall for image
+ await page.route(img_path, lambda route, request: None)
+
+ with pytest.raises(Error) as exc_info:
+ await page.set_content(f' ')
+ assert "Timeout 1ms exceeded" in exc_info.value.message
+ assert exc_info.type is TimeoutError
+
+
+async def test_set_content_should_await_resources_to_load(page: Page, server: Server) -> None:
+ img_route: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route("**/img.png", lambda route, request: img_route.set_result(route))
+ loaded = []
+
+ async def load() -> None:
+ await page.set_content(f' ')
+ loaded.append(True)
+
+ content_promise = asyncio.create_task(load())
+ await asyncio.sleep(0) # execute scheduled tasks, but don't await them
+ route = await img_route
+ assert loaded == []
+ asyncio.create_task(route.continue_())
+ await content_promise
+
+
+async def test_set_content_should_work_with_tricky_content(page: Page) -> None:
+ await page.set_content("hello world
" + "\x7F")
+ assert await page.eval_on_selector("div", "div => div.textContent") == "hello world"
+
+
+async def test_set_content_should_work_with_accents(page: Page) -> None:
+ await page.set_content("aberración
")
+ assert await page.eval_on_selector("div", "div => div.textContent") == "aberración"
+
+
+async def test_set_content_should_work_with_emojis(page: Page) -> None:
+ await page.set_content("🐥
")
+ assert await page.eval_on_selector("div", "div => div.textContent") == "🐥"
+
+
+async def test_set_content_should_work_with_newline(page: Page) -> None:
+ await page.set_content("\n
")
+ assert await page.eval_on_selector("div", "div => div.textContent") == "\n"
+
+
+async def test_add_script_tag_should_work_with_a_url(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ script_handle = await page.add_script_tag(url="/injectedfile.js")
+ assert script_handle.as_element()
+ assert await page.evaluate("__injected") == 42
+
+
+async def test_add_script_tag_should_work_with_a_url_and_type_module(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.add_script_tag(url="/es6/es6import.js", type="module")
+ assert await page.evaluate("__es6injected") == 42
+
+
+async def test_add_script_tag_should_work_with_a_path_and_type_module(
+ page: Page, server: Server, assetdir: Path
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.add_script_tag(path=assetdir / "es6" / "es6pathimport.js", type="module")
+ await page.wait_for_function("window.__es6injected")
+ assert await page.evaluate("__es6injected") == 42
+
+
+async def test_add_script_tag_should_work_with_a_content_and_type_module(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.add_script_tag(
+ content="import num from '/es6/es6module.js';window.__es6injected = num;",
+ type="module",
+ )
+ await page.wait_for_function("window.__es6injected")
+ assert await page.evaluate("__es6injected") == 42
+
+
+async def test_add_script_tag_should_throw_an_error_if_loading_from_url_fail(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ with pytest.raises(Error) as exc_info:
+ await page.add_script_tag(url="/nonexistfile.js")
+ assert exc_info.value
+
+
+async def test_add_script_tag_should_work_with_a_path(
+ page: Page, server: Server, assetdir: Path
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ script_handle = await page.add_script_tag(path=assetdir / "injectedfile.js")
+ assert script_handle.as_element()
+ assert await page.evaluate("__injected") == 42
+
+
+@pytest.mark.skip_browser("webkit")
+async def test_add_script_tag_should_include_source_url_when_path_is_provided(
+ page: Page, server: Server, assetdir: Path
+) -> None:
+ # Lacking sourceURL support in WebKit
+ await page.goto(server.EMPTY_PAGE)
+ await page.add_script_tag(path=assetdir / "injectedfile.js")
+ result = await page.evaluate("__injectedError.stack")
+ assert os.path.join("assets", "injectedfile.js") in result
+
+
+async def test_add_script_tag_should_work_with_content(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ script_handle = await page.add_script_tag(content="window.__injected = 35;")
+ assert script_handle.as_element()
+ assert await page.evaluate("__injected") == 35
+
+
+@pytest.mark.skip_browser("firefox")
+async def test_add_script_tag_should_throw_when_added_with_content_to_the_csp_page(
+ page: Page, server: Server
+) -> None:
+ # Firefox fires onload for blocked script before it issues the CSP console error.
+ await page.goto(server.PREFIX + "/csp.html")
+ with pytest.raises(Error) as exc_info:
+ await page.add_script_tag(content="window.__injected = 35;")
+ assert exc_info.value
+
+
+async def test_add_script_tag_should_throw_when_added_with_URL_to_the_csp_page(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/csp.html")
+ with pytest.raises(Error) as exc_info:
+ await page.add_script_tag(url=server.CROSS_PROCESS_PREFIX + "/injectedfile.js")
+ assert exc_info.value
+
+
+async def test_add_script_tag_should_throw_a_nice_error_when_the_request_fails(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ url = server.PREFIX + "/this_does_not_exist.js"
+ with pytest.raises(Error) as exc_info:
+ await page.add_script_tag(url=url)
+ assert url in exc_info.value.message
+
+
+async def test_add_style_tag_should_work_with_a_url(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ style_handle = await page.add_style_tag(url="/injectedstyle.css")
+ assert style_handle.as_element()
+ assert (
+ await page.evaluate(
+ "window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')"
+ )
+ == "rgb(255, 0, 0)"
+ )
+
+
+async def test_add_style_tag_should_throw_an_error_if_loading_from_url_fail(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ with pytest.raises(Error) as exc_info:
+ await page.add_style_tag(url="/nonexistfile.js")
+ assert exc_info.value
+
+
+async def test_add_style_tag_should_work_with_a_path(
+ page: Page, server: Server, assetdir: Path
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ style_handle = await page.add_style_tag(path=assetdir / "injectedstyle.css")
+ assert style_handle.as_element()
+ assert (
+ await page.evaluate(
+ "window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')"
+ )
+ == "rgb(255, 0, 0)"
+ )
+
+
+async def test_add_style_tag_should_include_source_url_when_path_is_provided(
+ page: Page, server: Server, assetdir: Path
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.add_style_tag(path=assetdir / "injectedstyle.css")
+ style_handle = await page.query_selector("style")
+ style_content = await page.evaluate("style => style.innerHTML", style_handle)
+ assert os.path.join("assets", "injectedstyle.css") in style_content
+
+
+async def test_add_style_tag_should_work_with_content(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ style_handle = await page.add_style_tag(content="body { background-color: green; }")
+ assert style_handle.as_element()
+ assert (
+ await page.evaluate(
+ "window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')"
+ )
+ == "rgb(0, 128, 0)"
+ )
+
+
+async def test_add_style_tag_should_throw_when_added_with_content_to_the_CSP_page(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/csp.html")
+ with pytest.raises(Error) as exc_info:
+ await page.add_style_tag(content="body { background-color: green; }")
+ assert exc_info.value
+
+
+async def test_add_style_tag_should_throw_when_added_with_URL_to_the_CSP_page(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/csp.html")
+ with pytest.raises(Error) as exc_info:
+ await page.add_style_tag(url=server.CROSS_PROCESS_PREFIX + "/injectedstyle.css")
+ assert exc_info.value
+
+
+async def test_url_should_work(page: Page, server: Server) -> None:
+ assert page.url == "about:blank"
+ await page.goto(server.EMPTY_PAGE)
+ assert page.url == server.EMPTY_PAGE
+
+
+async def test_url_should_include_hashes(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE + "#hash")
+ assert page.url == server.EMPTY_PAGE + "#hash"
+ await page.evaluate("window.location.hash = 'dynamic'")
+ assert page.url == server.EMPTY_PAGE + "#dynamic"
+
+
+async def test_title_should_return_the_page_title(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/title.html")
+ assert await page.title() == "Woof-Woof"
+
+
+async def give_it_a_chance_to_fill(page: Page) -> None:
+ for i in range(5):
+ await page.evaluate(
+ "() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))"
+ )
+
+
+async def test_fill_should_fill_textarea(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.fill("textarea", "some value")
+ assert await page.evaluate("result") == "some value"
+
+
+async def test_fill_should_fill_input(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.fill("input", "some value")
+ assert await page.evaluate("result") == "some value"
+
+
+async def test_fill_should_throw_on_unsupported_inputs(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ for type in [
+ "button",
+ "checkbox",
+ "file",
+ "image",
+ "radio",
+ "reset",
+ "submit",
+ ]:
+ await page.eval_on_selector(
+ "input", "(input, type) => input.setAttribute('type', type)", type
+ )
+ with pytest.raises(Error) as exc_info:
+ await page.fill("input", "")
+ assert f'Input of type "{type}" cannot be filled' in exc_info.value.message
+
+
+async def test_fill_should_fill_different_input_types(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ for type in ["password", "search", "tel", "text", "url"]:
+ await page.eval_on_selector(
+ "input", "(input, type) => input.setAttribute('type', type)", type
+ )
+ await page.fill("input", "text " + type)
+ assert await page.evaluate("result") == "text " + type
+
+
+async def test_fill_should_fill_date_input_after_clicking(page: Page, server: Server) -> None:
+ await page.set_content(" ")
+ await page.click("input")
+ await page.fill("input", "2020-03-02")
+ assert await page.eval_on_selector("input", "input => input.value") == "2020-03-02"
+
+
+@pytest.mark.skip_browser("webkit")
+async def test_fill_should_throw_on_incorrect_date(page: Page, server: Server) -> None:
+ # Disabled as in upstream, we should validate time in the Playwright lib
+ await page.set_content(" ")
+ with pytest.raises(Error) as exc_info:
+ await page.fill("input", "2020-13-05")
+ assert "Malformed value" in exc_info.value.message
+
+
+async def test_fill_should_fill_time_input(page: Page, server: Server) -> None:
+ await page.set_content(" ")
+ await page.fill("input", "13:15")
+ assert await page.eval_on_selector("input", "input => input.value") == "13:15"
+
+
+@pytest.mark.skip_browser("webkit")
+async def test_fill_should_throw_on_incorrect_time(page: Page, server: Server) -> None:
+ # Disabled as in upstream, we should validate time in the Playwright lib
+ await page.set_content(" ")
+ with pytest.raises(Error) as exc_info:
+ await page.fill("input", "25:05")
+ assert "Malformed value" in exc_info.value.message
+
+
+async def test_fill_should_fill_datetime_local_input(page: Page, server: Server) -> None:
+ await page.set_content(" ")
+ await page.fill("input", "2020-03-02T05:15")
+ assert await page.eval_on_selector("input", "input => input.value") == "2020-03-02T05:15"
+
+
+@pytest.mark.only_browser("chromium")
+async def test_fill_should_throw_on_incorrect_datetime_local(page: Page) -> None:
+ await page.set_content(" ")
+ with pytest.raises(Error) as exc_info:
+ await page.fill("input", "abc")
+ assert "Malformed value" in exc_info.value.message
+
+
+async def test_fill_should_fill_contenteditable(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.fill("div[contenteditable]", "some value")
+ assert (
+ await page.eval_on_selector("div[contenteditable]", "div => div.textContent")
+ == "some value"
+ )
+
+
+async def test_fill_should_fill_elements_with_existing_value_and_selection(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+
+ await page.eval_on_selector("input", "input => input.value = 'value one'")
+ await page.fill("input", "another value")
+ assert await page.evaluate("result") == "another value"
+
+ await page.eval_on_selector(
+ "input",
+ """input => {
+ input.selectionStart = 1
+ input.selectionEnd = 2
+ }""",
+ )
+
+ await page.fill("input", "maybe this one")
+ assert await page.evaluate("result") == "maybe this one"
+
+ await page.eval_on_selector(
+ "div[contenteditable]",
+ """div => {
+ div.innerHTML = 'some text some more text and even more text'
+ range = document.createRange()
+ range.selectNodeContents(div.querySelector('span'))
+ selection = window.getSelection()
+ selection.removeAllRanges()
+ selection.addRange(range)
+ }""",
+ )
+
+ await page.fill("div[contenteditable]", "replace with this")
+ assert (
+ await page.eval_on_selector("div[contenteditable]", "div => div.textContent")
+ == "replace with this"
+ )
+
+
+async def test_fill_should_throw_when_element_is_not_an_input_textarea_or_contenteditable(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ with pytest.raises(Error) as exc_info:
+ await page.fill("body", "")
+ assert "Element is not an " in exc_info.value.message
+
+
+async def test_fill_should_throw_if_passed_a_non_string_value(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ with pytest.raises(Error) as exc_info:
+ await page.fill("textarea", 123) # type: ignore
+ assert "expected string, got number" in exc_info.value.message
+
+
+async def test_fill_should_retry_on_disabled_element(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.eval_on_selector("input", "i => i.disabled = true")
+ done = []
+
+ async def fill() -> None:
+ await page.fill("input", "some value")
+ done.append(True)
+
+ promise = asyncio.create_task(fill())
+ await give_it_a_chance_to_fill(page)
+ assert done == []
+ assert await page.evaluate("result") == ""
+
+ await page.eval_on_selector("input", "i => i.disabled = false")
+ await promise
+ assert await page.evaluate("result") == "some value"
+
+
+async def test_fill_should_retry_on_readonly_element(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.eval_on_selector("textarea", "i => i.readOnly = true")
+ done = []
+
+ async def fill() -> None:
+ await page.fill("textarea", "some value")
+ done.append(True)
+
+ promise = asyncio.create_task(fill())
+ await give_it_a_chance_to_fill(page)
+ assert done == []
+ assert await page.evaluate("result") == ""
+
+ await page.eval_on_selector("textarea", "i => i.readOnly = false")
+ await promise
+ assert await page.evaluate("result") == "some value"
+
+
+async def test_fill_should_retry_on_invisible_element(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.eval_on_selector("input", "i => i.style.display = 'none'")
+ done = []
+
+ async def fill() -> None:
+ await page.fill("input", "some value")
+ done.append(True)
+
+ promise = asyncio.create_task(fill())
+ await give_it_a_chance_to_fill(page)
+ assert done == []
+ assert await page.evaluate("result") == ""
+
+ await page.eval_on_selector("input", "i => i.style.display = 'inline'")
+ await promise
+ assert await page.evaluate("result") == "some value"
+
+
+async def test_fill_should_be_able_to_fill_the_body(page: Page) -> None:
+ await page.set_content('')
+ await page.fill("body", "some value")
+ assert await page.evaluate("document.body.textContent") == "some value"
+
+
+async def test_fill_should_fill_fixed_position_input(page: Page) -> None:
+ await page.set_content(' ')
+ await page.fill("input", "some value")
+ assert await page.evaluate("document.querySelector('input').value") == "some value"
+
+
+async def test_fill_should_be_able_to_fill_when_focus_is_in_the_wrong_frame(
+ page: Page,
+) -> None:
+ await page.set_content(
+ """
+
+
+ """
+ )
+ await page.focus("iframe")
+ await page.fill("div", "some value")
+ assert await page.eval_on_selector("div", "d => d.textContent") == "some value"
+
+
+async def test_fill_should_be_able_to_fill_the_input_type_number_(page: Page) -> None:
+ await page.set_content(' ')
+ await page.fill("input", "42")
+ assert await page.evaluate("input.value") == "42"
+
+
+async def test_fill_should_be_able_to_fill_exponent_into_the_input_type_number_(
+ page: Page,
+) -> None:
+ await page.set_content(' ')
+ await page.fill("input", "-10e5")
+ assert await page.evaluate("input.value") == "-10e5"
+
+
+async def test_fill_should_be_able_to_fill_input_type_number__with_empty_string(
+ page: Page,
+) -> None:
+ await page.set_content(' ')
+ await page.fill("input", "")
+ assert await page.evaluate("input.value") == ""
+
+
+async def test_fill_should_not_be_able_to_fill_text_into_the_input_type_number_(
+ page: Page,
+) -> None:
+ await page.set_content(' ')
+ with pytest.raises(Error) as exc_info:
+ await page.fill("input", "abc")
+ assert "Cannot type text into input[type=number]" in exc_info.value.message
+
+
+async def test_fill_should_be_able_to_clear_using_fill(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.fill("input", "some value")
+ assert await page.evaluate("result") == "some value"
+ await page.fill("input", "")
+ assert await page.evaluate("result") == ""
+
+
+async def test_close_event_should_work_with_window_close(page: Page, server: Server) -> None:
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("window['newPage'] = window.open('about:blank')")
+ popup = await popup_info.value
+
+ async with popup.expect_event("close"):
+ await page.evaluate("window['newPage'].close()")
+
+
+async def test_close_event_should_work_with_page_close(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+ async with page.expect_event("close"):
+ await page.close()
+
+
+async def test_page_context_should_return_the_correct_browser_instance(
+ page: Page, context: BrowserContext
+) -> None:
+ assert page.context == context
+
+
+async def test_frame_should_respect_name(page: Page, server: Server) -> None:
+ await page.set_content("")
+ assert page.frame(name="bogus") is None
+ frame = page.frame(name="target")
+ assert frame
+ assert frame == page.main_frame.child_frames[0]
+
+
+async def test_frame_should_respect_url(page: Page, server: Server) -> None:
+ await page.set_content(f'')
+ assert page.frame(url=re.compile(r"bogus")) is None
+ assert must(page.frame(url=re.compile(r"empty"))).url == server.EMPTY_PAGE
+
+
+async def test_press_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+ await page.press("textarea", "a")
+ assert await page.evaluate("document.querySelector('textarea').value") == "a"
+
+
+async def test_frame_press_should_work(page: Page, server: Server) -> None:
+ await page.set_content(
+ f''
+ )
+ frame = page.frame("inner")
+ assert frame
+ await frame.press("textarea", "a")
+ assert await frame.evaluate("document.querySelector('textarea').value") == "a"
+
+
+async def test_should_emulate_reduced_motion(page: Page, server: Server) -> None:
+ assert await page.evaluate("matchMedia('(prefers-reduced-motion: no-preference)').matches")
+ await page.emulate_media(reduced_motion="reduce")
+ assert await page.evaluate("matchMedia('(prefers-reduced-motion: reduce)').matches")
+ assert not await page.evaluate("matchMedia('(prefers-reduced-motion: no-preference)').matches")
+ await page.emulate_media(reduced_motion="no-preference")
+ assert not await page.evaluate("matchMedia('(prefers-reduced-motion: reduce)').matches")
+ assert await page.evaluate("matchMedia('(prefers-reduced-motion: no-preference)').matches")
+
+
+async def test_input_value(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/textarea.html")
+
+ await page.fill("input", "my-text-content")
+ assert await page.input_value("input") == "my-text-content"
+
+ await page.fill("input", "")
+ assert await page.input_value("input") == ""
+
+
+async def test_drag_and_drop_helper_method(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/drag-n-drop.html")
+ await page.drag_and_drop("#source", "#target")
+ assert (
+ await page.eval_on_selector(
+ "#target", "target => target.contains(document.querySelector('#source'))"
+ )
+ is True
+ )
+
+
+async def test_drag_and_drop_with_position(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(
+ """
+
+
+
+
+ """
+ )
+ events_handle = await page.evaluate_handle(
+ """
+ () => {
+ const events = [];
+ document.getElementById('red').addEventListener('mousedown', event => {
+ events.push({
+ type: 'mousedown',
+ x: event.offsetX,
+ y: event.offsetY,
+ });
+ });
+ document.getElementById('blue').addEventListener('mouseup', event => {
+ events.push({
+ type: 'mouseup',
+ x: event.offsetX,
+ y: event.offsetY,
+ });
+ });
+ return events;
+ }
+ """
+ )
+ await page.drag_and_drop(
+ "#red",
+ "#blue",
+ source_position={"x": 34, "y": 7},
+ target_position={"x": 10, "y": 20},
+ )
+ assert await events_handle.json_value() == [
+ {"type": "mousedown", "x": 34, "y": 7},
+ {"type": "mouseup", "x": 10, "y": 20},
+ ]
+
+
+async def test_should_check_box_using_set_checked(page: Page) -> None:
+ await page.set_content("` `")
+ await page.set_checked("input", True)
+ assert await page.evaluate("checkbox.checked") is True
+ await page.set_checked("input", False)
+ assert await page.evaluate("checkbox.checked") is False
+
+
+async def test_should_set_bodysize_and_headersize(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_request("*/**") as request_info:
+ await page.evaluate(
+ "() => fetch('./get', { method: 'POST', body: '12345'}).then(r => r.text())"
+ )
+ request = await request_info.value
+ sizes = await request.sizes()
+ assert sizes["requestBodySize"] == 5
+ assert sizes["requestHeadersSize"] >= 300
+
+
+async def test_should_set_bodysize_to_0(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_request("*/**") as request_info:
+ await page.evaluate("() => fetch('./get').then(r => r.text())")
+ request = await request_info.value
+ sizes = await request.sizes()
+ assert sizes["requestBodySize"] == 0
+ assert sizes["requestHeadersSize"] >= 200
+
+
+@pytest.mark.skip_browser("webkit") # https://bugs.webkit.org/show_bug.cgi?id=225281
+async def test_should_emulate_forced_colors(page: Page) -> None:
+ assert await page.evaluate("matchMedia('(forced-colors: none)').matches")
+ await page.emulate_media(forced_colors="none")
+ assert await page.evaluate("matchMedia('(forced-colors: none)').matches")
+ assert not await page.evaluate("matchMedia('(forced-colors: active)').matches")
+ await page.emulate_media(forced_colors="active")
+ assert await page.evaluate("matchMedia('(forced-colors: active)').matches")
+ assert not await page.evaluate("matchMedia('(forced-colors: none)').matches")
+
+
+async def test_should_not_throw_when_continuing_while_page_is_closing(
+ page: Page, server: Server
+) -> None:
+ done: Optional[asyncio.Future] = None
+
+ def handle_route(route: Route) -> None:
+ nonlocal done
+ done = asyncio.gather(route.continue_(), page.close())
+
+ await page.route("**/*", handle_route)
+ with pytest.raises(Error):
+ await page.goto(server.EMPTY_PAGE)
+ await must(done)
+
+
+async def test_should_not_throw_when_continuing_after_page_is_closed(
+ page: Page, server: Server
+) -> None:
+ done: "asyncio.Future[bool]" = asyncio.Future()
+
+ async def handle_route(route: Route) -> None:
+ await page.close()
+ await route.continue_()
+ nonlocal done
+ done.set_result(True)
+
+ await page.route("**/*", handle_route)
+ with pytest.raises(Error):
+ await page.goto(server.EMPTY_PAGE)
+ await done
+
+
+async def test_expose_binding_should_serialize_cycles(page: Page) -> None:
+ binding_values = []
+
+ def binding(source: Dict, o: Dict) -> None:
+ binding_values.append(o)
+
+ await page.expose_binding("log", lambda source, o: binding(source, o))
+ await page.evaluate("const a = {}; a.b = a; window.log(a)")
+ assert binding_values[0]["b"] == binding_values[0]
+
+
+async def test_page_pause_should_reset_default_timeouts(
+ page: Page, headless: bool, server: Server
+) -> None:
+ if not headless:
+ pytest.skip()
+
+ await page.goto(server.EMPTY_PAGE)
+ await page.pause()
+ with pytest.raises(Error, match="Timeout 30000ms exceeded."):
+ await page.get_by_text("foo").click()
+
+
+async def test_page_pause_should_reset_custom_timeouts(
+ page: Page, headless: bool, server: Server
+) -> None:
+ if not headless:
+ pytest.skip()
+
+ page.set_default_timeout(123)
+ page.set_default_navigation_timeout(456)
+ await page.goto(server.EMPTY_PAGE)
+ await page.pause()
+ with pytest.raises(Error, match="Timeout 123ms exceeded."):
+ await page.get_by_text("foo").click()
+
+ server.set_route("/empty.html", lambda route: None)
+ with pytest.raises(Error, match="Timeout 456ms exceeded."):
+ await page.goto(server.EMPTY_PAGE)
diff --git a/tests/async/test_page_add_locator_handler.py b/tests/async/test_page_add_locator_handler.py
new file mode 100644
index 0000000..412474f
--- /dev/null
+++ b/tests/async/test_page_add_locator_handler.py
@@ -0,0 +1,377 @@
+# 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
+
+from playwright.async_api import Error, Locator, Page, expect
+from tests.server import Server
+from tests.utils import TARGET_CLOSED_ERROR_MESSAGE
+
+
+async def test_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+
+ before_count = 0
+ after_count = 0
+
+ original_locator = page.get_by_text("This interstitial covers the button")
+
+ async def handler(locator: Locator) -> None:
+ nonlocal original_locator
+ assert locator == original_locator
+ nonlocal before_count
+ nonlocal after_count
+ before_count += 1
+ await page.locator("#close").click()
+ after_count += 1
+
+ await page.add_locator_handler(original_locator, handler)
+
+ for args in [
+ ["mouseover", 1],
+ ["mouseover", 1, "capture"],
+ ["mouseover", 2],
+ ["mouseover", 2, "capture"],
+ ["pointerover", 1],
+ ["pointerover", 1, "capture"],
+ ["none", 1],
+ ["remove", 1],
+ ["hide", 1],
+ ]:
+ await page.locator("#aside").hover()
+ before_count = 0
+ after_count = 0
+ await page.evaluate(
+ "(args) => { window.clicked = 0; window.setupAnnoyingInterstitial(...args); }",
+ args,
+ )
+ assert before_count == 0
+ assert after_count == 0
+ await page.locator("#target").click()
+ assert before_count == args[1]
+ assert after_count == args[1]
+ assert await page.evaluate("window.clicked") == 1
+ await expect(page.locator("#interstitial")).not_to_be_visible()
+
+
+async def test_should_work_with_a_custom_check(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+
+ async def handler() -> None:
+ if await page.get_by_text("This interstitial covers the button").is_visible():
+ await page.locator("#close").click()
+
+ await page.add_locator_handler(page.locator("body"), handler, no_wait_after=True)
+
+ for args in [
+ ["mouseover", 2],
+ ["none", 1],
+ ["remove", 1],
+ ["hide", 1],
+ ]:
+ await page.locator("#aside").hover()
+ await page.evaluate(
+ "(args) => { window.clicked = 0; window.setupAnnoyingInterstitial(...args); }",
+ args,
+ )
+ await page.locator("#target").click()
+ assert await page.evaluate("window.clicked") == 1
+ await expect(page.locator("#interstitial")).not_to_be_visible()
+
+
+async def test_should_work_with_locator_hover(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+
+ await page.add_locator_handler(
+ page.get_by_text("This interstitial covers the button"),
+ lambda: page.locator("#close").click(),
+ )
+
+ await page.locator("#aside").hover()
+ await page.evaluate('() => { window.setupAnnoyingInterstitial("pointerover", 1, "capture"); }')
+ await page.locator("#target").hover()
+ await expect(page.locator("#interstitial")).not_to_be_visible()
+ assert (
+ await page.eval_on_selector("#target", "e => window.getComputedStyle(e).backgroundColor")
+ == "rgb(255, 255, 0)"
+ )
+
+
+async def test_should_not_work_with_force_true(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+
+ await page.add_locator_handler(
+ page.get_by_text("This interstitial covers the button"),
+ lambda: page.locator("#close").click(),
+ )
+
+ await page.locator("#aside").hover()
+ await page.evaluate('() => { window.setupAnnoyingInterstitial("none", 1); }')
+ await page.locator("#target").click(force=True, timeout=2000)
+ assert await page.locator("#interstitial").is_visible()
+ assert await page.evaluate("window.clicked") is None
+
+
+async def test_should_throw_when_page_closes(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+
+ await page.add_locator_handler(
+ page.get_by_text("This interstitial covers the button"), lambda: page.close()
+ )
+
+ await page.locator("#aside").hover()
+ await page.evaluate(
+ '() => { window.clicked = 0; window.setupAnnoyingInterstitial("mouseover", 1); }'
+ )
+ with pytest.raises(Error) as exc:
+ await page.locator("#target").click()
+ assert TARGET_CLOSED_ERROR_MESSAGE in exc.value.message
+
+
+async def test_should_throw_when_handler_times_out(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+
+ called = 0
+ stall_future: asyncio.Future[None] = asyncio.Future()
+
+ async def handler() -> None:
+ nonlocal called
+ called += 1
+ # Deliberately timeout.
+ await stall_future
+
+ await page.add_locator_handler(page.get_by_text("This interstitial covers the button"), handler)
+
+ await page.locator("#aside").hover()
+ await page.evaluate(
+ '() => { window.clicked = 0; window.setupAnnoyingInterstitial("mouseover", 1); }'
+ )
+ with pytest.raises(Error) as exc:
+ await page.locator("#target").click(timeout=3000)
+ assert "Timeout 3000ms exceeded" in exc.value.message
+
+ with pytest.raises(Error) as exc:
+ await page.locator("#target").click(timeout=3000)
+ assert "Timeout 3000ms exceeded" in exc.value.message
+
+ # Should not enter the same handler while it is still running.
+ assert called == 1
+ stall_future.cancel()
+
+
+async def test_should_work_with_to_be_visible(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+
+ called = 0
+
+ async def handler() -> None:
+ nonlocal called
+ called += 1
+ await page.locator("#close").click()
+
+ await page.add_locator_handler(page.get_by_text("This interstitial covers the button"), handler)
+
+ await page.evaluate(
+ '() => { window.clicked = 0; window.setupAnnoyingInterstitial("remove", 1); }'
+ )
+ await expect(page.locator("#target")).to_be_visible()
+ await expect(page.locator("#interstitial")).not_to_be_visible()
+ assert called == 1
+
+
+async def test_should_work_when_owner_frame_detaches(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.evaluate(
+ """
+ () => {
+ const iframe = document.createElement('iframe');
+ iframe.src = 'data:text/html,hello from iframe';
+ document.body.append(iframe);
+
+ const target = document.createElement('button');
+ target.textContent = 'Click me';
+ target.id = 'target';
+ target.addEventListener('click', () => window._clicked = true);
+ document.body.appendChild(target);
+
+ const closeButton = document.createElement('button');
+ closeButton.textContent = 'close';
+ closeButton.id = 'close';
+ closeButton.addEventListener('click', () => iframe.remove());
+ document.body.appendChild(closeButton);
+ }
+ """
+ )
+ await page.add_locator_handler(
+ page.frame_locator("iframe").locator("body"),
+ lambda: page.locator("#close").click(),
+ )
+ await page.locator("#target").click()
+ assert await page.query_selector("iframe") is None
+ assert await page.evaluate("window._clicked") is True
+
+
+async def test_should_work_with_times_option(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+ called = 0
+
+ def _handler() -> None:
+ nonlocal called
+ called += 1
+
+ await page.add_locator_handler(page.locator("body"), _handler, no_wait_after=True, times=2)
+ await page.locator("#aside").hover()
+ await page.evaluate(
+ """
+ () => {
+ window.clicked = 0;
+ window.setupAnnoyingInterstitial('mouseover', 4);
+ }
+ """
+ )
+ with pytest.raises(Error) as exc_info:
+ await page.locator("#target").click(timeout=3000)
+ assert called == 2
+ assert await page.evaluate("window.clicked") == 0
+ await expect(page.locator("#interstitial")).to_be_visible()
+ assert "Timeout 3000ms exceeded" in exc_info.value.message
+ assert (
+ 'This interstitial covers the button
from …
subtree intercepts pointer events'
+ in exc_info.value.message
+ )
+
+
+async def test_should_wait_for_hidden_by_default(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+ called = 0
+
+ async def _handler(button: Locator) -> None:
+ nonlocal called
+ called += 1
+ await button.click()
+
+ await page.add_locator_handler(page.get_by_role("button", name="close"), _handler)
+ await page.locator("#aside").hover()
+ await page.evaluate(
+ """
+ () => {
+ window.clicked = 0;
+ window.setupAnnoyingInterstitial('timeout', 1);
+ }
+ """
+ )
+ await page.locator("#target").click()
+ assert await page.evaluate("window.clicked") == 1
+ await expect(page.locator("#interstitial")).not_to_be_visible()
+ assert called == 1
+
+
+async def test_should_wait_for_hidden_by_default_2(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+ called = 0
+
+ def _handler() -> None:
+ nonlocal called
+ called += 1
+
+ await page.add_locator_handler(page.get_by_role("button", name="close"), _handler)
+ await page.locator("#aside").hover()
+ await page.evaluate(
+ """
+ () => {
+ window.clicked = 0;
+ window.setupAnnoyingInterstitial('hide', 1);
+ }
+ """
+ )
+ with pytest.raises(Error) as exc_info:
+ await page.locator("#target").click(timeout=3000)
+ assert await page.evaluate("window.clicked") == 0
+ await expect(page.locator("#interstitial")).to_be_visible()
+ assert called == 1
+ assert (
+ 'locator handler has finished, waiting for get_by_role("button", name="close") to be hidden'
+ in exc_info.value.message
+ )
+
+
+async def test_should_work_with_noWaitAfter(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+ called = 0
+
+ async def _handler(button: Locator) -> None:
+ nonlocal called
+ called += 1
+ if called == 1:
+ await button.click()
+ else:
+ await page.locator("#interstitial").wait_for(state="hidden")
+
+ await page.add_locator_handler(
+ page.get_by_role("button", name="close"), _handler, no_wait_after=True
+ )
+ await page.locator("#aside").hover()
+ await page.evaluate(
+ """
+ () => {
+ window.clicked = 0;
+ window.setupAnnoyingInterstitial('timeout', 1);
+ }
+ """
+ )
+ await page.locator("#target").click()
+ assert await page.evaluate("window.clicked") == 1
+ await expect(page.locator("#interstitial")).not_to_be_visible()
+ assert called == 2
+
+
+async def test_should_removeLocatorHandler(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/handle-locator.html")
+ called = 0
+
+ async def _handler(locator: Locator) -> None:
+ nonlocal called
+ called += 1
+ await locator.click()
+
+ await page.add_locator_handler(page.get_by_role("button", name="close"), _handler)
+ await page.evaluate(
+ """
+ () => {
+ window.clicked = 0;
+ window.setupAnnoyingInterstitial('hide', 1);
+ }
+ """
+ )
+ await page.locator("#target").click()
+ assert called == 1
+ assert await page.evaluate("window.clicked") == 1
+ await expect(page.locator("#interstitial")).not_to_be_visible()
+ await page.evaluate(
+ """
+ () => {
+ window.clicked = 0;
+ window.setupAnnoyingInterstitial('hide', 1);
+ }
+ """
+ )
+ await page.remove_locator_handler(page.get_by_role("button", name="close"))
+ with pytest.raises(Error) as error:
+ await page.locator("#target").click(timeout=3000)
+ assert called == 1
+ assert await page.evaluate("window.clicked") == 0
+ await expect(page.locator("#interstitial")).to_be_visible()
+ assert "Timeout 3000ms exceeded" in error.value.message
diff --git a/tests/async/test_page_base_url.py b/tests/async/test_page_base_url.py
new file mode 100644
index 0000000..bcf1cc1
--- /dev/null
+++ b/tests/async/test_page_base_url.py
@@ -0,0 +1,114 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from pathlib import Path
+from typing import Dict
+
+from playwright.async_api import Browser, BrowserType
+from tests.server import Server
+from tests.utils import must
+
+
+async def test_should_construct_a_new_url_when_a_base_url_in_browser_new_context_is_passed(
+ browser: Browser, server: Server
+) -> None:
+ context = await browser.new_context(base_url=server.PREFIX)
+ page = await context.new_page()
+ assert (must(await page.goto("/empty.html"))).url == server.EMPTY_PAGE
+ await context.close()
+
+
+async def test_should_construct_a_new_url_when_a_base_url_in_browser_new_page_is_passed(
+ browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(base_url=server.PREFIX)
+ assert (must(await page.goto("/empty.html"))).url == server.EMPTY_PAGE
+ await page.close()
+
+
+async def test_should_construct_a_new_url_when_a_base_url_in_browser_new_persistent_context_is_passed(
+ browser_type: BrowserType, tmpdir: Path, server: Server, launch_arguments: Dict
+) -> None:
+ context = await browser_type.launch_persistent_context(
+ tmpdir, **launch_arguments, base_url=server.PREFIX
+ )
+ page = await context.new_page()
+ assert (must(await page.goto("/empty.html"))).url == server.EMPTY_PAGE
+ await context.close()
+
+
+async def test_should_construct_correctly_when_a_baseurl_without_a_trailing_slash_is_passed(
+ browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(base_url=server.PREFIX + "/url-construction")
+ assert (must(await page.goto("mypage.html"))).url == server.PREFIX + "/mypage.html"
+ assert (must(await page.goto("./mypage.html"))).url == server.PREFIX + "/mypage.html"
+ assert (must(await page.goto("/mypage.html"))).url == server.PREFIX + "/mypage.html"
+ await page.close()
+
+
+async def test_should_construct_correctly_when_a_baseurl_with_a_trailing_slash_is_passed(
+ browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(base_url=server.PREFIX + "/url-construction/")
+ assert (
+ must(await page.goto("mypage.html"))
+ ).url == server.PREFIX + "/url-construction/mypage.html"
+ assert (
+ must(await page.goto("./mypage.html"))
+ ).url == server.PREFIX + "/url-construction/mypage.html"
+ assert (must(await page.goto("/mypage.html"))).url == server.PREFIX + "/mypage.html"
+ assert (must(await page.goto("."))).url == server.PREFIX + "/url-construction/"
+ assert (must(await page.goto("/"))).url == server.PREFIX + "/"
+ await page.close()
+
+
+async def test_should_not_construct_a_new_url_when_valid_urls_are_passed(
+ browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(base_url="http://microsoft.com")
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.url == server.EMPTY_PAGE
+
+ await page.goto("data:text/html,Hello world")
+ assert page.url == "data:text/html,Hello world"
+
+ await page.goto("about:blank")
+ assert page.url == "about:blank"
+
+ await page.close()
+
+
+async def test_should_be_able_to_match_a_url_relative_to_its_given_url_with_urlmatcher(
+ browser: Browser, server: Server
+) -> None:
+ page = await browser.new_page(base_url=server.PREFIX + "/foobar/")
+
+ await page.goto("/kek/index.html")
+ await page.wait_for_url("/kek/index.html")
+ assert page.url == server.PREFIX + "/kek/index.html"
+
+ await page.route("./kek/index.html", lambda route: route.fulfill(body="base-url-matched-route"))
+
+ async with page.expect_request("./kek/index.html") as request_info:
+ async with page.expect_response("./kek/index.html") as response_info:
+ await page.goto("./kek/index.html")
+ request = await request_info.value
+ response = await response_info.value
+ assert request.url == server.PREFIX + "/foobar/kek/index.html"
+ assert response.url == server.PREFIX + "/foobar/kek/index.html"
+ assert await response.body() == b"base-url-matched-route"
+
+ await page.close()
diff --git a/tests/async/test_page_clock.py b/tests/async/test_page_clock.py
new file mode 100644
index 0000000..e6bb948
--- /dev/null
+++ b/tests/async/test_page_clock.py
@@ -0,0 +1,462 @@
+# 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 datetime
+from typing import Any, AsyncGenerator, List
+
+import pytest
+
+from playwright.async_api import Error, Page
+from tests.server import Server
+
+
+@pytest.fixture(autouse=True)
+async def calls(page: Page) -> List[Any]:
+ calls: List[Any] = []
+ await page.expose_function("stub", lambda *args: calls.append(list(args)))
+ return calls
+
+
+class TestRunFor:
+ @pytest.fixture(autouse=True)
+ async def before_each(self, page: Page) -> AsyncGenerator[None, None]:
+ await page.clock.install(time=0)
+ await page.clock.pause_at(1000)
+ yield
+
+ async def test_run_for_triggers_immediately_without_specified_delay(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.evaluate("setTimeout(window.stub)")
+ await page.clock.run_for(0)
+ assert len(calls) == 1
+
+ async def test_run_for_does_not_trigger_without_sufficient_delay(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.evaluate("setTimeout(window.stub, 100)")
+ await page.clock.run_for(10)
+ assert len(calls) == 0
+
+ async def test_run_for_triggers_after_sufficient_delay(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.evaluate("setTimeout(window.stub, 100)")
+ await page.clock.run_for(100)
+ assert len(calls) == 1
+
+ async def test_run_for_triggers_simultaneous_timers(self, page: Page, calls: List[Any]) -> None:
+ await page.evaluate("setTimeout(window.stub, 100); setTimeout(window.stub, 100)")
+ await page.clock.run_for(100)
+ assert len(calls) == 2
+
+ async def test_run_for_triggers_multiple_simultaneous_timers(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.evaluate(
+ "setTimeout(window.stub, 100); setTimeout(window.stub, 100); setTimeout(window.stub, 99); setTimeout(window.stub, 100)"
+ )
+ await page.clock.run_for(100)
+ assert len(calls) == 4
+
+ async def test_run_for_waits_after_setTimeout_was_called(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.evaluate("setTimeout(window.stub, 150)")
+ await page.clock.run_for(50)
+ assert len(calls) == 0
+ await page.clock.run_for(100)
+ assert len(calls) == 1
+
+ async def test_run_for_triggers_event_when_some_throw(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.evaluate(
+ "setTimeout(() => { throw new Error(); }, 100); setTimeout(window.stub, 120)"
+ )
+ with pytest.raises(Error):
+ await page.clock.run_for(120)
+ assert len(calls) == 1
+
+ async def test_run_for_creates_updated_Date_while_ticking(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.clock.set_system_time(0)
+ await page.evaluate("setInterval(() => { window.stub(new Date().getTime()); }, 10)")
+ await page.clock.run_for(100)
+ assert calls == [
+ [10],
+ [20],
+ [30],
+ [40],
+ [50],
+ [60],
+ [70],
+ [80],
+ [90],
+ [100],
+ ]
+
+ async def test_run_for_passes_8_seconds(self, page: Page, calls: List[Any]) -> None:
+ await page.evaluate("setInterval(window.stub, 4000)")
+ await page.clock.run_for("08")
+ assert len(calls) == 2
+
+ async def test_run_for_passes_1_minute(self, page: Page, calls: List[Any]) -> None:
+ await page.evaluate("setInterval(window.stub, 6000)")
+ await page.clock.run_for("01:00")
+ assert len(calls) == 10
+
+ async def test_run_for_passes_2_hours_34_minutes_and_10_seconds(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.evaluate("setInterval(window.stub, 10000)")
+ await page.clock.run_for("02:34:10")
+ assert len(calls) == 925
+
+ async def test_run_for_throws_for_invalid_format(self, page: Page, calls: List[Any]) -> None:
+ await page.evaluate("setInterval(window.stub, 10000)")
+ with pytest.raises(Error):
+ await page.clock.run_for("12:02:34:10")
+ assert len(calls) == 0
+
+ async def test_run_for_returns_the_current_now_value(self, page: Page) -> None:
+ await page.clock.set_system_time(0)
+ value = 200
+ await page.clock.run_for(value)
+ assert await page.evaluate("Date.now()") == value
+
+
+class TestFastForward:
+ @pytest.fixture(autouse=True)
+ async def before_each(self, page: Page) -> AsyncGenerator[None, None]:
+ await page.clock.install(time=0)
+ await page.clock.pause_at(1)
+ yield
+
+ async def test_ignores_timers_which_wouldnt_be_run(self, page: Page, calls: List[Any]) -> None:
+ await page.evaluate("setTimeout(() => { window.stub('should not be logged'); }, 1000)")
+ await page.clock.fast_forward(500)
+ assert len(calls) == 0
+
+ async def test_pushes_back_execution_time_for_skipped_timers(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.evaluate("setTimeout(() => { window.stub(Date.now()); }, 1000)")
+ await page.clock.fast_forward(2000)
+ assert calls == [[1000 + 2000]]
+
+ async def test_supports_string_time_arguments(self, page: Page, calls: List[Any]) -> None:
+ await page.evaluate(
+ "setTimeout(() => { window.stub(Date.now()); }, 100000)"
+ ) # 100000 = 1:40
+ await page.clock.fast_forward("01:50")
+ assert calls == [[1000 + 110000]]
+
+
+class TestStubTimers:
+ @pytest.fixture(autouse=True)
+ async def before_each(self, page: Page) -> AsyncGenerator[None, None]:
+ await page.clock.install(time=0)
+ await page.clock.pause_at(1)
+ yield
+
+ async def test_sets_initial_timestamp(self, page: Page) -> None:
+ await page.clock.set_system_time(1.4)
+ assert await page.evaluate("Date.now()") == 1400
+
+ async def test_replaces_global_setTimeout(self, page: Page, calls: List[Any]) -> None:
+ await page.evaluate("setTimeout(window.stub, 1000)")
+ await page.clock.run_for(1000)
+ assert len(calls) == 1
+
+ async def test_global_fake_setTimeout_should_return_id(self, page: Page) -> None:
+ to = await page.evaluate("setTimeout(window.stub, 1000)")
+ assert isinstance(to, int)
+
+ async def test_replaces_global_clearTimeout(self, page: Page, calls: List[Any]) -> None:
+ await page.evaluate(
+ """
+ const to = setTimeout(window.stub, 1000);
+ clearTimeout(to);
+ """
+ )
+ await page.clock.run_for(1000)
+ assert len(calls) == 0
+
+ async def test_replaces_global_setInterval(self, page: Page, calls: List[Any]) -> None:
+ await page.evaluate("setInterval(window.stub, 500)")
+ await page.clock.run_for(1000)
+ assert len(calls) == 2
+
+ async def test_replaces_global_clearInterval(self, page: Page, calls: List[Any]) -> None:
+ await page.evaluate(
+ """
+ const to = setInterval(window.stub, 500);
+ clearInterval(to);
+ """
+ )
+ await page.clock.run_for(1000)
+ assert len(calls) == 0
+
+ async def test_replaces_global_performance_now(self, page: Page) -> None:
+ promise = asyncio.create_task(
+ page.evaluate(
+ """async () => {
+ const prev = performance.now();
+ await new Promise(f => setTimeout(f, 1000));
+ const next = performance.now();
+ return { prev, next };
+ }"""
+ )
+ )
+ await asyncio.sleep(0) # Make sure the promise is scheduled.
+ await page.clock.run_for(1000)
+ assert await promise == {"prev": 1000, "next": 2000}
+
+ async def test_fakes_Date_constructor(self, page: Page) -> None:
+ now = await page.evaluate("new Date().getTime()")
+ assert now == 1000
+
+
+class TestStubTimersPerformance:
+ async def test_replaces_global_performance_time_origin(self, page: Page) -> None:
+ await page.clock.install(time=1)
+ await page.clock.pause_at(2)
+ promise = asyncio.create_task(
+ page.evaluate(
+ """async () => {
+ const prev = performance.now();
+ await new Promise(f => setTimeout(f, 1000));
+ const next = performance.now();
+ return { prev, next };
+ }"""
+ )
+ )
+ await asyncio.sleep(0) # Make sure the promise is scheduled.
+ await page.clock.run_for(1000)
+ assert await page.evaluate("performance.timeOrigin") == 1000
+ assert await promise == {"prev": 1000, "next": 2000}
+
+
+class TestPopup:
+ async def test_should_tick_after_popup(self, page: Page) -> None:
+ await page.clock.install(time=0)
+ now = datetime.datetime.fromisoformat("2015-09-25")
+ await page.clock.pause_at(now)
+ popup, _ = await asyncio.gather(
+ page.wait_for_event("popup"), page.evaluate("window.open('about:blank')")
+ )
+ popup_time = await popup.evaluate("Date.now()")
+ assert popup_time == now.timestamp() * 1000
+ await page.clock.run_for(1000)
+ popup_time_after = await popup.evaluate("Date.now()")
+ assert popup_time_after == now.timestamp() * 1000 + 1000
+
+ async def test_should_tick_before_popup(self, page: Page) -> None:
+ await page.clock.install(time=0)
+ now = datetime.datetime.fromisoformat("2015-09-25")
+ await page.clock.pause_at(now)
+ await page.clock.run_for(1000)
+ popup, _ = await asyncio.gather(
+ page.wait_for_event("popup"), page.evaluate("window.open('about:blank')")
+ )
+ popup_time = await popup.evaluate("Date.now()")
+ assert popup_time == int(now.timestamp() * 1000 + 1000)
+ assert datetime.datetime.fromtimestamp(popup_time / 1_000).year == 2015
+
+ async def test_should_run_time_before_popup(self, page: Page, server: Server) -> None:
+ server.set_route(
+ "/popup.html",
+ lambda res: (
+ res.setHeader("Content-Type", "text/html"),
+ res.write(b""),
+ res.finish(),
+ ),
+ )
+ await page.goto(server.EMPTY_PAGE)
+ # Wait for 2 second in real life to check that it is past in popup.
+ await page.wait_for_timeout(2000)
+ popup, _ = await asyncio.gather(
+ page.wait_for_event("popup"),
+ page.evaluate("window.open('{}')".format(server.PREFIX + "/popup.html")),
+ )
+ popup_time = await popup.evaluate("window.time")
+ assert popup_time >= 2000
+
+ async def test_should_not_run_time_before_popup_on_pause(
+ self, page: Page, server: Server
+ ) -> None:
+ server.set_route(
+ "/popup.html",
+ lambda res: (
+ res.setHeader("Content-Type", "text/html"),
+ res.write(b""),
+ res.finish(),
+ ),
+ )
+ await page.clock.install(time=0)
+ await page.clock.pause_at(1)
+ await page.goto(server.EMPTY_PAGE)
+ # Wait for 2 second in real life to check that it is past in popup.
+ await page.wait_for_timeout(2000)
+ popup, _ = await asyncio.gather(
+ page.wait_for_event("popup"),
+ page.evaluate("window.open('{}')".format(server.PREFIX + "/popup.html")),
+ )
+ popup_time = await popup.evaluate("window.time")
+ assert popup_time == 1000
+
+
+class TestSetFixedTime:
+ async def test_does_not_fake_methods(self, page: Page) -> None:
+ await page.clock.set_fixed_time(0)
+ # Should not stall.
+ await page.evaluate("new Promise(f => setTimeout(f, 1))")
+
+ async def test_allows_setting_time_multiple_times(self, page: Page) -> None:
+ await page.clock.set_fixed_time(0.1)
+ assert await page.evaluate("Date.now()") == 100
+ await page.clock.set_fixed_time(0.2)
+ assert await page.evaluate("Date.now()") == 200
+
+ async def test_fixed_time_is_not_affected_by_clock_manipulation(self, page: Page) -> None:
+ await page.clock.set_fixed_time(0.1)
+ assert await page.evaluate("Date.now()") == 100
+ await page.clock.fast_forward(20)
+ assert await page.evaluate("Date.now()") == 100
+
+ async def test_allows_installing_fake_timers_after_setting_time(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.clock.set_fixed_time(0.1)
+ assert await page.evaluate("Date.now()") == 100
+ await page.clock.set_fixed_time(0.2)
+ await page.evaluate("setTimeout(() => window.stub(Date.now()))")
+ await page.clock.run_for(0)
+ assert calls == [[200]]
+
+
+class TestWhileRunning:
+ async def test_should_progress_time(self, page: Page) -> None:
+ await page.clock.install(time=0)
+ await page.goto("data:text/html,")
+ await page.wait_for_timeout(1000)
+ now = await page.evaluate("Date.now()")
+ assert 1000 <= now <= 2000
+
+ async def test_should_run_for(self, page: Page) -> None:
+ await page.clock.install(time=0)
+ await page.goto("data:text/html,")
+ await page.clock.run_for(10000)
+ now = await page.evaluate("Date.now()")
+ assert 10000 <= now <= 11000
+
+ async def test_should_fast_forward(self, page: Page) -> None:
+ await page.clock.install(time=0)
+ await page.goto("data:text/html,")
+ await page.clock.fast_forward(10000)
+ now = await page.evaluate("Date.now()")
+ assert 10000 <= now <= 11000
+
+ async def test_should_fast_forward_to(self, page: Page) -> None:
+ await page.clock.install(time=0)
+ await page.goto("data:text/html,")
+ await page.clock.fast_forward(10000)
+ now = await page.evaluate("Date.now()")
+ assert 10000 <= now <= 11000
+
+ async def test_should_pause(self, page: Page) -> None:
+ await page.clock.install(time=0)
+ await page.goto("data:text/html,")
+ await page.clock.pause_at(1)
+ await page.wait_for_timeout(1000)
+ await page.clock.resume()
+ now = await page.evaluate("Date.now()")
+ assert 0 <= now <= 1000
+
+ async def test_should_pause_and_fast_forward(self, page: Page) -> None:
+ await page.clock.install(time=0)
+ await page.goto("data:text/html,")
+ await page.clock.pause_at(1)
+ await page.clock.fast_forward(1000)
+ now = await page.evaluate("Date.now()")
+ assert now == 2000
+
+ async def test_should_set_system_time_on_pause(self, page: Page) -> None:
+ await page.clock.install(time=0)
+ await page.goto("data:text/html,")
+ await page.clock.pause_at(1)
+ now = await page.evaluate("Date.now()")
+ assert now == 1000
+
+
+class TestWhileOnPause:
+ async def test_fast_forward_should_not_run_nested_immediate(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.clock.install(time=0)
+ await page.goto("data:text/html,")
+ await page.clock.pause_at(1000)
+ await page.evaluate(
+ """
+ setTimeout(() => {
+ window.stub('outer');
+ setTimeout(() => window.stub('inner'), 0);
+ }, 1000);
+ """
+ )
+ await page.clock.fast_forward(1000)
+ assert calls == [["outer"]]
+ await page.clock.fast_forward(1)
+ assert calls == [["outer"], ["inner"]]
+
+ async def test_run_for_should_not_run_nested_immediate(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.clock.install(time=0)
+ await page.goto("data:text/html,")
+ await page.clock.pause_at(1000)
+ await page.evaluate(
+ """
+ setTimeout(() => {
+ window.stub('outer');
+ setTimeout(() => window.stub('inner'), 0);
+ }, 1000);
+ """
+ )
+ await page.clock.run_for(1000)
+ assert calls == [["outer"]]
+ await page.clock.run_for(1)
+ assert calls == [["outer"], ["inner"]]
+
+ async def test_run_for_should_not_run_nested_immediate_from_microtask(
+ self, page: Page, calls: List[Any]
+ ) -> None:
+ await page.clock.install(time=0)
+ await page.goto("data:text/html,")
+ await page.clock.pause_at(1000)
+ await page.evaluate(
+ """
+ setTimeout(() => {
+ window.stub('outer');
+ void Promise.resolve().then(() => setTimeout(() => window.stub('inner'), 0));
+ }, 1000);
+ """
+ )
+ await page.clock.run_for(1000)
+ assert calls == [["outer"]]
+ await page.clock.run_for(1)
+ assert calls == [["outer"], ["inner"]]
diff --git a/tests/async/test_page_evaluate.py b/tests/async/test_page_evaluate.py
new file mode 100644
index 0000000..9b3e4c1
--- /dev/null
+++ b/tests/async/test_page_evaluate.py
@@ -0,0 +1,320 @@
+# 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 math
+from datetime import datetime, timedelta, timezone
+from typing import Optional
+from urllib.parse import ParseResult, urlparse
+
+from playwright.async_api import Error, Page
+
+
+async def test_evaluate_work(page: Page) -> None:
+ result = await page.evaluate("7 * 3")
+ assert result == 21
+
+
+async def test_evaluate_return_none_for_null(page: Page) -> None:
+ result = await page.evaluate("a => a", None)
+ assert result is None
+
+
+async def test_evaluate_transfer_nan(page: Page) -> None:
+ result = await page.evaluate("a => a", float("nan"))
+ assert math.isnan(result)
+
+
+async def test_evaluate_transfer_neg_zero(page: Page) -> None:
+ result = await page.evaluate("a => a", -0)
+ assert result == float("-0")
+
+
+async def test_evaluate_transfer_infinity(page: Page) -> None:
+ result = await page.evaluate("a => a", float("Infinity"))
+ assert result == float("Infinity")
+
+
+async def test_evaluate_transfer_neg_infinity(page: Page) -> None:
+ result = await page.evaluate("a => a", float("-Infinity"))
+ assert result == float("-Infinity")
+
+
+async def test_evaluate_roundtrip_unserializable_values(page: Page) -> None:
+ value = {
+ "infinity": float("Infinity"),
+ "nInfinity": float("-Infinity"),
+ "nZero": float("-0"),
+ }
+ result = await page.evaluate("a => a", value)
+ assert result == value
+
+
+async def test_evaluate_transfer_arrays(page: Page) -> None:
+ result = await page.evaluate("a => a", [1, 2, 3])
+ assert result == [1, 2, 3]
+
+
+async def test_evaluate_transfer_bigint(page: Page) -> None:
+ assert await page.evaluate("() => 42n") == 42
+ assert await page.evaluate("a => a", 17) == 17
+
+
+async def test_evaluate_return_undefined_for_objects_with_symbols(page: Page) -> None:
+ assert await page.evaluate('[Symbol("foo4")]') == [None]
+ assert (
+ await page.evaluate(
+ """() => {
+ const a = { };
+ a[Symbol('foo4')] = 42;
+ return a;
+ }"""
+ )
+ == {}
+ )
+ assert (
+ await page.evaluate(
+ """() => {
+ return { foo: [{ a: Symbol('foo4') }] };
+ }"""
+ )
+ == {"foo": [{"a": None}]}
+ )
+
+
+async def test_evaluate_work_with_unicode_chars(page: Page) -> None:
+ result = await page.evaluate('a => a["中文字符"]', {"中文字符": 42})
+ assert result == 42
+
+
+async def test_evaluate_throw_when_evaluation_triggers_reload(page: Page) -> None:
+ error: Optional[Error] = None
+ try:
+ await page.evaluate("() => { location.reload(); return new Promise(() => {}); }")
+ except Error as e:
+ error = e
+ assert error
+ assert "navigation" in error.message
+
+
+async def test_evaluate_work_with_exposed_function(page: Page) -> None:
+ await page.expose_function("callController", lambda a, b: a * b)
+ result = await page.evaluate("callController(9, 3)")
+ assert result == 27
+
+
+async def test_evaluate_reject_promise_with_exception(page: Page) -> None:
+ error: Optional[Error] = None
+ try:
+ await page.evaluate("not_existing_object.property")
+ except Error as e:
+ error = e
+ assert error
+ assert "not_existing_object" in error.message
+
+
+async def test_evaluate_support_thrown_strings(page: Page) -> None:
+ error: Optional[Error] = None
+ try:
+ await page.evaluate('throw "qwerty"')
+ except Error as e:
+ error = e
+ assert error
+ assert "qwerty" in error.message
+
+
+async def test_evaluate_support_thrown_numbers(page: Page) -> None:
+ error: Optional[Error] = None
+ try:
+ await page.evaluate("throw 100500")
+ except Error as e:
+ error = e
+ assert error
+ assert "100500" in error.message
+
+
+async def test_evaluate_return_complex_objects(page: Page) -> None:
+ obj = {"foo": "bar!"}
+ result = await page.evaluate("a => a", obj)
+ assert result == obj
+
+
+async def test_evaluate_accept_none_as_one_of_multiple_parameters(page: Page) -> None:
+ result = await page.evaluate(
+ '({ a, b }) => Object.is(a, null) && Object.is(b, "foo")',
+ {"a": None, "b": "foo"},
+ )
+ assert result
+
+
+async def test_evaluate_properly_serialize_none_arguments(page: Page) -> None:
+ assert await page.evaluate("x => ({a: x})", None) == {"a": None}
+
+
+async def test_should_alias_window_document_and_node(page: Page) -> None:
+ object = await page.evaluate("[window, document, document.body]")
+ assert object == ["ref: ", "ref: ", "ref: "]
+
+
+async def test_evaluate_should_work_for_circular_object(page: Page) -> None:
+ a = await page.evaluate(
+ """() => {
+ const a = {x: 47};
+ const b = {a};
+ a.b = b;
+ return a;
+ }"""
+ )
+
+ assert a["b"]["a"]["b"]["a"]["x"] == 47
+ assert a["b"]["a"] == a
+
+
+async def test_evaluate_accept_string(page: Page) -> None:
+ assert await page.evaluate("1 + 2") == 3
+
+
+async def test_evaluate_accept_element_handle_as_an_argument(page: Page) -> None:
+ await page.set_content("")
+ element = await page.query_selector("section")
+ text = await page.evaluate("e => e.textContent", element)
+ assert text == "42"
+
+
+async def test_evaluate_throw_if_underlying_element_was_disposed(page: Page) -> None:
+ await page.set_content("")
+ element = await page.query_selector("section")
+ assert element
+ await element.dispose()
+ error: Optional[Error] = None
+ try:
+ await page.evaluate("e => e.textContent", element)
+ except Error as e:
+ error = e
+ assert error
+ assert "no object with guid" in error.message
+
+
+async def test_evaluate_evaluate_exception(page: Page) -> None:
+ error = await page.evaluate(
+ """() => {
+ function innerFunction() {
+ const e = new Error('error message');
+ e.name = 'foobar';
+ return e;
+ }
+ return innerFunction();
+ }"""
+ )
+ assert isinstance(error, Error)
+ assert error.message == "error message"
+ assert error.name == "foobar"
+ assert error.stack
+ assert "innerFunction" in error.stack
+
+
+async def test_should_pass_exception_argument(page: Page) -> None:
+ def _raise_and_get_exception(exception: Exception) -> Exception:
+ try:
+ raise exception
+ except Exception as e:
+ return e
+
+ error_for_roundtrip = Error("error message")
+ error_for_roundtrip._name = "foobar"
+ error_for_roundtrip._stack = "test stack"
+ error = await page.evaluate(
+ """e => {
+ return { message: e.message, name: e.name, stack: e.stack };
+ }""",
+ error_for_roundtrip,
+ )
+ assert error["message"] == "error message"
+ assert error["name"] == "foobar"
+ assert "test stack" in error["stack"]
+
+ error = await page.evaluate(
+ """e => {
+ return { message: e.message, name: e.name, stack: e.stack };
+ }""",
+ _raise_and_get_exception(Exception("error message")),
+ )
+ assert error["message"] == "error message"
+ assert error["name"] == "Exception"
+ assert "error message" in error["stack"]
+
+
+async def test_evaluate_evaluate_date(page: Page) -> None:
+ result = await page.evaluate('() => ({ date: new Date("2020-05-27T01:31:38.506Z") })')
+ assert result == {
+ "date": datetime.fromisoformat("2020-05-27T01:31:38.506").replace(tzinfo=timezone.utc)
+ }
+
+
+async def test_evaluate_roundtrip_date_without_tzinfo(page: Page) -> None:
+ date = datetime.fromisoformat("2020-05-27T01:31:38.506")
+ result = await page.evaluate("date => date", date)
+ assert result.timestamp() == date.timestamp()
+
+
+async def test_evaluate_roundtrip_date(page: Page) -> None:
+ date = datetime.fromisoformat("2020-05-27T01:31:38.506").replace(tzinfo=timezone.utc)
+ result = await page.evaluate("date => date", date)
+ assert result == date
+
+
+async def test_evaluate_roundtrip_date_with_tzinfo(page: Page) -> None:
+ date = datetime.fromisoformat("2020-05-27T01:31:38.506")
+ date = date.astimezone(timezone(timedelta(hours=4)))
+ result = await page.evaluate("date => date", date)
+ assert result == date
+
+
+async def test_evaluate_jsonvalue_date(page: Page) -> None:
+ date = datetime.fromisoformat("2020-05-27T01:31:38.506").replace(tzinfo=timezone.utc)
+ result = await page.evaluate('() => ({ date: new Date("2020-05-27T01:31:38.506Z") })')
+ assert result == {"date": date}
+
+
+async def test_should_evaluate_url(page: Page) -> None:
+ out = await page.evaluate(
+ "() => ({ someKey: new URL('https://user:pass@example.com/?foo=bar#hi') })"
+ )
+ assert out["someKey"] == ParseResult(
+ scheme="https",
+ netloc="user:pass@example.com",
+ path="/",
+ query="foo=bar",
+ params="",
+ fragment="hi",
+ )
+
+
+async def test_should_roundtrip_url(page: Page) -> None:
+ in_ = urlparse("https://user:pass@example.com/?foo=bar#hi")
+ out = await page.evaluate("url => url", in_)
+ assert in_ == out
+
+
+async def test_should_roundtrip_complex_url(page: Page) -> None:
+ in_ = urlparse(
+ "https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName"
+ )
+ out = await page.evaluate("url => url", in_)
+ assert in_ == out
+
+
+async def test_evaluate_jsonvalue_url(page: Page) -> None:
+ url = urlparse("https://example.com/")
+ result = await page.evaluate('() => ({ someKey: new URL("https://example.com/") })')
+ assert result == {"someKey": url}
diff --git a/tests/async/test_page_network_request.py b/tests/async/test_page_network_request.py
new file mode 100644
index 0000000..61fe65f
--- /dev/null
+++ b/tests/async/test_page_network_request.py
@@ -0,0 +1,61 @@
+# 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
+
+from playwright.async_api import Error, Page, Request
+from tests.server import Server
+
+
+async def test_should_not_allow_to_access_frame_on_popup_main_request(
+ page: Page, server: Server
+) -> None:
+ await page.set_content(f'click me ')
+ request_promise = asyncio.ensure_future(page.context.wait_for_event("request"))
+ popup_promise = asyncio.ensure_future(page.context.wait_for_event("page"))
+ clicked = asyncio.ensure_future(page.get_by_text("click me").click())
+ request: Request = await request_promise
+
+ assert request.is_navigation_request()
+
+ with pytest.raises(Error) as exc_info:
+ request.frame
+ assert "Frame for this navigation request is not available" in exc_info.value.message
+
+ response = await request.response()
+ assert response
+ await response.finished()
+ await popup_promise
+ await clicked
+
+
+async def test_should_parse_the_data_if_content_type_is_application_x_www_form_urlencoded_charset_UTF_8(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_event("request") as request_info:
+ await page.evaluate(
+ """() => fetch('./post', {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ },
+ body: 'foo=bar&baz=123'
+ })"""
+ )
+ request = await request_info.value
+ assert request
+ assert request.post_data_json == {"foo": "bar", "baz": "123"}
diff --git a/tests/async/test_page_network_response.py b/tests/async/test_page_network_response.py
new file mode 100644
index 0000000..76fb7df
--- /dev/null
+++ b/tests/async/test_page_network_response.py
@@ -0,0 +1,70 @@
+# 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
+
+from playwright.async_api import Error, Page
+from tests.server import Server, TestServerRequest
+
+
+async def test_should_reject_response_finished_if_page_closes(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+
+ def handle_get(request: TestServerRequest) -> None:
+ # In Firefox, |fetch| will be hanging until it receives |Content-Type| header
+ # from server.
+ request.setHeader("Content-Type", "text/plain; charset=utf-8")
+ request.write(b"hello ")
+
+ server.set_route("/get", handle_get)
+ # send request and wait for server response
+ [page_response, _] = await asyncio.gather(
+ page.wait_for_event("response"),
+ page.evaluate("() => fetch('./get', { method: 'GET' })"),
+ )
+
+ finish_coroutine = page_response.finished()
+ await page.close()
+ with pytest.raises(Error) as exc_info:
+ await finish_coroutine
+ error = exc_info.value
+ assert "closed" in error.message
+
+
+async def test_should_reject_response_finished_if_context_closes(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+
+ def handle_get(request: TestServerRequest) -> None:
+ # In Firefox, |fetch| will be hanging until it receives |Content-Type| header
+ # from server.
+ request.setHeader("Content-Type", "text/plain; charset=utf-8")
+ request.write(b"hello ")
+
+ server.set_route("/get", handle_get)
+ # send request and wait for server response
+ [page_response, _] = await asyncio.gather(
+ page.wait_for_event("response"),
+ page.evaluate("() => fetch('./get', { method: 'GET' })"),
+ )
+
+ finish_coroutine = page_response.finished()
+ await page.context.close()
+ with pytest.raises(Error) as exc_info:
+ await finish_coroutine
+ error = exc_info.value
+ assert "closed" in error.message
diff --git a/tests/async/test_page_request_fallback.py b/tests/async/test_page_request_fallback.py
new file mode 100644
index 0000000..db83628
--- /dev/null
+++ b/tests/async/test_page_request_fallback.py
@@ -0,0 +1,352 @@
+# 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
+from typing import Any, Callable, Coroutine, cast
+
+import pytest
+
+from playwright.async_api import Error, Page, Request, Route
+from tests.server import Server
+
+
+async def test_should_work(page: Page, server: Server) -> None:
+ await page.route("**/*", lambda route: asyncio.create_task(route.fallback()))
+ await page.goto(server.EMPTY_PAGE)
+
+
+async def test_should_fall_back(page: Page, server: Server) -> None:
+ intercepted = []
+
+ def _handler1(route: Route) -> None:
+ intercepted.append(1)
+ asyncio.create_task(route.fallback())
+
+ await page.route("**/empty.html", _handler1)
+
+ def _handler2(route: Route) -> None:
+ intercepted.append(2)
+ asyncio.create_task(route.fallback())
+
+ await page.route("**/empty.html", _handler2)
+
+ def _handler3(route: Route) -> None:
+ intercepted.append(3)
+ asyncio.create_task(route.fallback())
+
+ await page.route("**/empty.html", _handler3)
+
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [3, 2, 1]
+
+
+async def test_should_fall_back_async_delayed(page: Page, server: Server) -> None:
+ intercepted = []
+
+ def create_handler(i: int) -> Callable[[Route], Coroutine]:
+ async def handler(route: Route) -> None:
+ intercepted.append(i)
+ await asyncio.sleep(0.1)
+ await route.fallback()
+
+ return handler
+
+ await page.route("**/empty.html", create_handler(1))
+ await page.route("**/empty.html", create_handler(2))
+ await page.route("**/empty.html", create_handler(3))
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [3, 2, 1]
+
+
+async def test_should_chain_once(page: Page, server: Server) -> None:
+ await page.route(
+ "**/madeup.txt",
+ lambda route: asyncio.create_task(route.fulfill(status=200, body="fulfilled one")),
+ times=1,
+ )
+ await page.route("**/madeup.txt", lambda route: asyncio.create_task(route.fallback()), times=1)
+
+ resp = await page.goto(server.PREFIX + "/madeup.txt")
+ assert resp
+ body = await resp.body()
+ assert body == b"fulfilled one"
+
+
+async def test_should_not_chain_fulfill(page: Page, server: Server) -> None:
+ failed = [False]
+
+ def handler(route: Route) -> None:
+ failed[0] = True
+
+ await page.route("**/empty.html", handler)
+ await page.route(
+ "**/empty.html",
+ lambda route: asyncio.create_task(route.fulfill(status=200, body="fulfilled")),
+ )
+ await page.route("**/empty.html", lambda route: asyncio.create_task(route.fallback()))
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ body = await response.body()
+ assert body == b"fulfilled"
+ assert not failed[0]
+
+
+async def test_should_not_chain_abort(
+ page: Page, server: Server, is_webkit: bool, is_firefox: bool
+) -> None:
+ failed = [False]
+
+ def handler(route: Route) -> None:
+ failed[0] = True
+
+ await page.route("**/empty.html", handler)
+ await page.route("**/empty.html", lambda route: asyncio.create_task(route.abort()))
+ await page.route("**/empty.html", lambda route: asyncio.create_task(route.fallback()))
+
+ with pytest.raises(Error) as excinfo:
+ await page.goto(server.EMPTY_PAGE)
+ if is_webkit:
+ assert "Blocked by Web Inspector" in excinfo.value.message
+ elif is_firefox:
+ assert "NS_ERROR_FAILURE" in excinfo.value.message
+ else:
+ assert "net::ERR_FAILED" in excinfo.value.message
+ assert not failed[0]
+
+
+async def test_should_fall_back_after_exception(page: Page, server: Server) -> None:
+ await page.route("**/empty.html", lambda route: route.continue_())
+
+ async def handler(route: Route) -> None:
+ try:
+ await route.fulfill(response=cast(Any, {}))
+ except Exception:
+ await route.fallback()
+
+ await page.route("**/empty.html", handler)
+
+ await page.goto(server.EMPTY_PAGE)
+
+
+async def test_should_amend_http_headers(page: Page, server: Server) -> None:
+ values = []
+
+ async def handler(route: Route) -> None:
+ values.append(route.request.headers.get("foo"))
+ values.append(await route.request.header_value("FOO"))
+ await route.continue_()
+
+ await page.route("**/sleep.zzz", handler)
+
+ async def handler_with_header_mods(route: Route) -> None:
+ await route.fallback(headers={**route.request.headers, "FOO": "bar"})
+
+ await page.route("**/*", handler_with_header_mods)
+
+ await page.goto(server.EMPTY_PAGE)
+ with server.expect_request("/sleep.zzz") as server_request_info:
+ await page.evaluate("() => fetch('/sleep.zzz')")
+ values.append(server_request_info.value.getHeader("foo"))
+ assert values == ["bar", "bar", "bar"]
+
+
+async def test_should_delete_header_with_undefined_value(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ server.set_route(
+ "/something",
+ lambda r: (
+ r.setHeader("Acces-Control-Allow-Origin", "*"),
+ r.write(b"done"),
+ r.finish(),
+ ),
+ )
+
+ intercepted_request = []
+
+ async def capture_and_continue(route: Route, request: Request) -> None:
+ intercepted_request.append(request)
+ await route.continue_()
+
+ await page.route("**/*", capture_and_continue)
+
+ async def delete_foo_header(route: Route, request: Request) -> None:
+ headers = await request.all_headers()
+ del headers["foo"]
+ await route.fallback(headers=headers)
+
+ await page.route(server.PREFIX + "/something", delete_foo_header)
+
+ [server_req, text] = await asyncio.gather(
+ server.wait_for_request("/something"),
+ page.evaluate(
+ """
+ async url => {
+ const data = await fetch(url, {
+ headers: {
+ foo: 'a',
+ bar: 'b',
+ }
+ });
+ return data.text();
+ }
+ """,
+ server.PREFIX + "/something",
+ ),
+ )
+
+ assert text == "done"
+ assert not intercepted_request[0].headers.get("foo")
+ assert intercepted_request[0].headers.get("bar") == "b"
+ assert not server_req.getHeader("foo")
+ assert server_req.getHeader("bar") == "b"
+
+
+async def test_should_amend_method(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+
+ method = []
+
+ def _handler(route: Route) -> None:
+ method.append(route.request.method)
+ asyncio.create_task(route.continue_())
+
+ await page.route("**/*", _handler)
+ await page.route("**/*", lambda route: asyncio.create_task(route.fallback(method="POST")))
+
+ [request, _] = await asyncio.gather(
+ server.wait_for_request("/sleep.zzz"),
+ page.evaluate("() => fetch('/sleep.zzz')"),
+ )
+
+ assert method == ["POST"]
+ assert request.method == b"POST"
+
+
+async def test_should_override_request_url(page: Page, server: Server) -> None:
+ url = []
+
+ def _handler1(route: Route) -> None:
+ url.append(route.request.url)
+ asyncio.create_task(route.continue_())
+
+ await page.route("**/global-var.html", _handler1)
+
+ def _handler2(route: Route) -> None:
+ asyncio.create_task(route.fallback(url=server.PREFIX + "/global-var.html"))
+
+ await page.route("**/foo", _handler2)
+
+ [server_request, response, _] = await asyncio.gather(
+ server.wait_for_request("/global-var.html"),
+ page.wait_for_event("response"),
+ page.goto(server.PREFIX + "/foo"),
+ )
+
+ assert url == [server.PREFIX + "/global-var.html"]
+ assert response.url == server.PREFIX + "/global-var.html"
+ assert response.request.url == server.PREFIX + "/global-var.html"
+ assert await page.evaluate("() => window['globalVar']") == 123
+ assert server_request.uri == b"/global-var.html"
+ assert server_request.method == b"GET"
+
+
+async def test_should_amend_post_data(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ post_data = []
+
+ def _handler(route: Route) -> None:
+ post_data.append(route.request.post_data)
+ asyncio.create_task(route.continue_())
+
+ await page.route("**/*", _handler)
+ await page.route("**/*", lambda route: asyncio.create_task(route.fallback(post_data="doggo")))
+ [server_request, _] = await asyncio.gather(
+ server.wait_for_request("/sleep.zzz"),
+ page.evaluate("() => fetch('/sleep.zzz', { method: 'POST', body: 'birdy' })"),
+ )
+ assert post_data == ["doggo"]
+ assert server_request.post_body == b"doggo"
+
+
+async def test_should_amend_binary_post_data(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ post_data_buffer = []
+
+ def _handler1(route: Route) -> None:
+ post_data_buffer.append(route.request.post_data)
+ asyncio.create_task(route.continue_())
+
+ await page.route("**/*", _handler1)
+
+ async def _handler2(route: Route) -> None:
+ await route.fallback(post_data=b"\x00\x01\x02\x03\x04")
+
+ await page.route("**/*", _handler2)
+
+ [server_request, result] = await asyncio.gather(
+ server.wait_for_request("/sleep.zzz"),
+ page.evaluate("fetch('/sleep.zzz', { method: 'POST', body: 'birdy' })"),
+ )
+ # FIXME: should this be bytes?
+ assert post_data_buffer == ["\x00\x01\x02\x03\x04"]
+ assert server_request.method == b"POST"
+ assert server_request.post_body == b"\x00\x01\x02\x03\x04"
+
+
+async def test_should_chain_fallback_with_dynamic_url(server: Server, page: Page) -> None:
+ intercepted = []
+
+ def _handler1(route: Route) -> None:
+ intercepted.append(1)
+ asyncio.create_task(route.fallback(url=server.EMPTY_PAGE))
+
+ await page.route("**/bar", _handler1)
+
+ def _handler2(route: Route, request: Request) -> None:
+ intercepted.append(2)
+ asyncio.create_task(route.fallback(url="http://localhost/bar"))
+
+ await page.route("**/foo", _handler2)
+
+ def _handler3(route: Route, request: Request) -> None:
+ intercepted.append(3)
+ asyncio.create_task(route.fallback(url="http://localhost/foo"))
+
+ await page.route("**/empty.html", _handler3)
+
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [3, 2, 1]
+
+
+async def test_should_amend_json_post_data(server: Server, page: Page) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ post_data = []
+
+ def _handle1(route: Route, request: Request) -> None:
+ post_data.append(route.request.post_data)
+ asyncio.create_task(route.continue_())
+
+ await page.route("**/*", _handle1)
+ await page.route(
+ "**/*",
+ lambda route: asyncio.create_task(route.fallback(post_data={"foo": "bar"})),
+ )
+
+ [server_request, _] = await asyncio.gather(
+ server.wait_for_request("/sleep.zzz"),
+ page.evaluate("() => fetch('/sleep.zzz', { method: 'POST', body: 'birdy' })"),
+ )
+ assert post_data == ['{"foo": "bar"}']
+ assert server_request.post_body == b'{"foo": "bar"}'
diff --git a/tests/async/test_page_request_intercept.py b/tests/async/test_page_request_intercept.py
new file mode 100644
index 0000000..1c7b947
--- /dev/null
+++ b/tests/async/test_page_request_intercept.py
@@ -0,0 +1,95 @@
+# 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
+from typing import cast
+
+import pytest
+
+from playwright.async_api import Error, Page, Route, expect
+from tests.server import Server, TestServerRequest
+
+
+async def test_should_support_timeout_option_in_route_fetch(server: Server, page: Page) -> None:
+ def _handler(request: TestServerRequest) -> None:
+ request.responseHeaders.addRawHeader("Content-Length", "4096")
+ request.responseHeaders.addRawHeader("Content-Type", "text/html")
+ request.write(b"")
+
+ server.set_route("/slow", _handler)
+
+ async def handle(route: Route) -> None:
+ with pytest.raises(Error) as error:
+ await route.fetch(timeout=1000)
+ assert "Request timed out after 1000ms" in error.value.message
+
+ await page.route("**/*", lambda route: handle(route))
+ with pytest.raises(Error) as error:
+ await page.goto(server.PREFIX + "/slow", timeout=2000)
+ assert "Timeout 2000ms exceeded" in error.value.message
+
+
+async def test_should_not_follow_redirects_when_max_redirects_is_set_to_0_in_route_fetch(
+ server: Server, page: Page
+) -> None:
+ server.set_redirect("/foo", "/empty.html")
+
+ async def handle(route: Route) -> None:
+ response = await route.fetch(max_redirects=0)
+ assert response.headers["location"] == "/empty.html"
+ assert response.status == 302
+ await route.fulfill(body="hello")
+
+ await page.route("**/*", lambda route: handle(route))
+ await page.goto(server.PREFIX + "/foo")
+ assert "hello" in await page.content()
+
+
+async def test_should_intercept_with_url_override(server: Server, page: Page) -> None:
+ async def handle(route: Route) -> None:
+ response = await route.fetch(url=server.PREFIX + "/one-style.html")
+ await route.fulfill(response=response)
+
+ await page.route("**/*.html", lambda route: handle(route))
+ response = await page.goto(server.PREFIX + "/empty.html")
+ assert response
+ assert response.status == 200
+ assert "one-style.css" in (await response.body()).decode("utf-8")
+
+
+async def test_should_intercept_with_post_data_override(server: Server, page: Page) -> None:
+ request_promise = asyncio.create_task(server.wait_for_request("/empty.html"))
+
+ async def handle(route: Route) -> None:
+ response = await route.fetch(post_data={"foo": "bar"})
+ await route.fulfill(response=response)
+
+ await page.route("**/*.html", lambda route: handle(route))
+ await page.goto(server.PREFIX + "/empty.html")
+ request = await request_promise
+ assert request.post_body
+ assert request.post_body.decode("utf-8") == '{"foo": "bar"}'
+
+
+async def test_should_fulfill_popup_main_request_using_alias(page: Page, server: Server) -> None:
+ async def route_handler(route: Route) -> None:
+ response = await route.fetch()
+ await route.fulfill(response=response, body="hello")
+
+ await page.context.route("**/*", route_handler)
+ await page.set_content(f'click me ')
+ [popup, _] = await asyncio.gather(
+ page.wait_for_event("popup"), page.get_by_text("click me").click()
+ )
+ await expect(cast(Page, popup).locator("body")).to_have_text("hello")
diff --git a/tests/async/test_page_route.py b/tests/async/test_page_route.py
new file mode 100644
index 0000000..32b1bca
--- /dev/null
+++ b/tests/async/test_page_route.py
@@ -0,0 +1,1059 @@
+# 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 json
+import re
+from pathlib import Path
+from typing import Callable, List, Optional
+
+import pytest
+
+from playwright._impl._glob import glob_to_regex
+from playwright.async_api import (
+ Browser,
+ BrowserContext,
+ Error,
+ Page,
+ Playwright,
+ Request,
+ Route,
+)
+from tests.server import Server, TestServerRequest
+from tests.utils import must
+
+
+async def test_page_route_should_intercept(page: Page, server: Server) -> None:
+ intercepted = []
+
+ async def handle_request(route: Route, request: Request) -> None:
+ assert route.request == request
+ assert "empty.html" in request.url
+ assert request.headers["user-agent"]
+ assert request.method == "GET"
+ assert request.post_data is None
+ assert request.is_navigation_request()
+ assert request.resource_type == "document"
+ assert request.frame == page.main_frame
+ assert request.frame.url == "about:blank"
+ await route.continue_()
+ intercepted.append(True)
+
+ await page.route("**/empty.html", handle_request)
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+ assert len(intercepted) == 1
+
+
+async def test_page_route_should_unroute(page: Page, server: Server) -> None:
+ intercepted = []
+
+ def _handle1(route: Route) -> None:
+ intercepted.append(1)
+ asyncio.create_task(route.continue_())
+
+ await page.route("**/*", _handle1)
+
+ def _handle2(route: Route, request: Request) -> None:
+ intercepted.append(2)
+ asyncio.create_task(route.continue_())
+
+ await page.route("**/empty.html", _handle2)
+
+ def _handle3(route: Route, request: Request) -> None:
+ intercepted.append(3)
+ asyncio.create_task(route.continue_())
+
+ await page.route(
+ "**/empty.html",
+ _handle3,
+ )
+
+ def handler4(route: Route) -> None:
+ intercepted.append(4)
+ asyncio.create_task(route.continue_())
+
+ await page.route(re.compile("empty.html"), handler4)
+
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [4]
+
+ intercepted = []
+ await page.unroute(re.compile("empty.html"), handler4)
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [3]
+
+ intercepted = []
+ await page.unroute("**/empty.html")
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == [1]
+
+
+async def test_page_route_should_work_when_POST_is_redirected_with_302(
+ page: Page, server: Server
+) -> None:
+ server.set_redirect("/rredirect", "/empty.html")
+ await page.goto(server.EMPTY_PAGE)
+ await page.route("**/*", lambda route: route.continue_())
+ await page.set_content(
+ """
+
+ """
+ )
+ async with page.expect_navigation():
+ await page.eval_on_selector("form", "form => form.submit()")
+
+
+# @see https://github.com/GoogleChrome/puppeteer/issues/3973
+async def test_page_route_should_work_when_header_manipulation_headers_with_redirect(
+ page: Page, server: Server
+) -> None:
+ server.set_redirect("/rrredirect", "/empty.html")
+ await page.route(
+ "**/*",
+ lambda route: route.continue_(headers={**route.request.headers, "foo": "bar"}),
+ )
+
+ await page.goto(server.PREFIX + "/rrredirect")
+
+
+# @see https://github.com/GoogleChrome/puppeteer/issues/4743
+async def test_page_route_should_be_able_to_remove_headers(page: Page, server: Server) -> None:
+ async def handle_request(route: Route) -> None:
+ headers = route.request.headers
+ if "origin" in headers:
+ del headers["origin"]
+ await route.continue_(headers=headers)
+
+ await page.route(
+ "**/*", # remove "origin" header
+ handle_request,
+ )
+
+ [serverRequest, _] = await asyncio.gather(
+ server.wait_for_request("/empty.html"), page.goto(server.PREFIX + "/empty.html")
+ )
+ assert serverRequest.getHeader("origin") is None
+
+
+async def test_page_route_should_contain_referer_header(page: Page, server: Server) -> None:
+ requests = []
+
+ def _handle(route: Route, request: Request) -> None:
+ requests.append(route.request)
+ asyncio.create_task(route.continue_())
+
+ await page.route(
+ "**/*",
+ _handle,
+ )
+
+ await page.goto(server.PREFIX + "/one-style.html")
+ assert "/one-style.css" in requests[1].url
+ assert "/one-style.html" in requests[1].headers["referer"]
+
+
+async def test_page_route_should_properly_return_navigation_response_when_URL_has_cookies(
+ context: BrowserContext, page: Page, server: Server
+) -> None:
+ # Setup cookie.
+ await page.goto(server.EMPTY_PAGE)
+ await context.add_cookies([{"url": server.EMPTY_PAGE, "name": "foo", "value": "bar"}])
+
+ # Setup request interception.
+ await page.route("**/*", lambda route: route.continue_())
+ response = await page.reload()
+ assert response
+ assert response.status == 200
+
+
+async def test_page_route_should_show_custom_HTTP_headers(page: Page, server: Server) -> None:
+ await page.set_extra_http_headers({"foo": "bar"})
+
+ def assert_headers(request: Request) -> None:
+ assert request.headers["foo"] == "bar"
+
+ def _handle(route: Route) -> None:
+ assert_headers(route.request)
+ asyncio.create_task(route.continue_())
+
+ await page.route(
+ "**/*",
+ _handle,
+ )
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+
+
+# @see https://github.com/GoogleChrome/puppeteer/issues/4337
+async def test_page_route_should_work_with_redirect_inside_sync_XHR(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ server.set_redirect("/logo.png", "/pptr.png")
+ await page.route("**/*", lambda route: route.continue_())
+ status = await page.evaluate(
+ """async() => {
+ const request = new XMLHttpRequest();
+ request.open('GET', '/logo.png', false); // `false` makes the request synchronous
+ request.send(null);
+ return request.status;
+ }"""
+ )
+
+ assert status == 200
+
+
+async def test_page_route_should_work_with_custom_referer_headers(
+ page: Page, server: Server
+) -> None:
+ await page.set_extra_http_headers({"referer": server.EMPTY_PAGE})
+
+ def assert_headers(route: Route) -> None:
+ assert route.request.headers["referer"] == server.EMPTY_PAGE
+
+ def _handle(route: Route, request: Request) -> None:
+ assert_headers(route)
+ asyncio.create_task(route.continue_())
+
+ await page.route(
+ "**/*",
+ _handle,
+ )
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.ok
+
+
+async def test_page_route_should_be_abortable(page: Page, server: Server) -> None:
+ await page.route(r"/\.css$/", lambda route: asyncio.create_task(route.abort()))
+ failed = []
+
+ def handle_request(request: Request) -> None:
+ if ".css" in request.url:
+ failed.append(True)
+
+ page.on("requestfailed", handle_request)
+
+ response = await page.goto(server.PREFIX + "/one-style.html")
+ assert response
+ assert response.ok
+ assert response.request.failure is None
+ assert len(failed) == 0
+
+
+async def test_page_route_should_be_abortable_with_custom_error_codes(
+ page: Page, server: Server, is_webkit: bool, is_firefox: bool
+) -> None:
+ await page.route(
+ "**/*",
+ lambda route: route.abort("internetdisconnected"),
+ )
+ failed_requests = []
+ page.on("requestfailed", lambda request: failed_requests.append(request))
+ with pytest.raises(Error):
+ await page.goto(server.EMPTY_PAGE)
+ assert len(failed_requests) == 1
+ failed_request = failed_requests[0]
+ if is_webkit:
+ assert failed_request.failure == "Blocked by Web Inspector"
+ elif is_firefox:
+ assert failed_request.failure == "NS_ERROR_OFFLINE"
+ else:
+ assert failed_request.failure == "net::ERR_INTERNET_DISCONNECTED"
+
+
+async def test_page_route_should_send_referer(page: Page, server: Server) -> None:
+ await page.set_extra_http_headers({"referer": "http://google.com/"})
+
+ await page.route("**/*", lambda route: route.continue_())
+ [request, _] = await asyncio.gather(
+ server.wait_for_request("/grid.html"),
+ page.goto(server.PREFIX + "/grid.html"),
+ )
+ assert request.getHeader("referer") == "http://google.com/"
+
+
+async def test_page_route_should_fail_navigation_when_aborting_main_resource(
+ page: Page, server: Server, is_webkit: bool, is_firefox: bool
+) -> None:
+ await page.route("**/*", lambda route: route.abort())
+ with pytest.raises(Error) as exc:
+ await page.goto(server.EMPTY_PAGE)
+ assert exc
+ if is_webkit:
+ assert "Blocked by Web Inspector" in exc.value.message
+ elif is_firefox:
+ assert "NS_ERROR_FAILURE" in exc.value.message
+ else:
+ assert "net::ERR_FAILED" in exc.value.message
+
+
+async def test_page_route_should_not_work_with_redirects(page: Page, server: Server) -> None:
+ intercepted = []
+
+ def _handle(route: Route, request: Request) -> None:
+ asyncio.create_task(route.continue_())
+ intercepted.append(route.request)
+
+ await page.route(
+ "**/*",
+ _handle,
+ )
+
+ server.set_redirect("/non-existing-page.html", "/non-existing-page-2.html")
+ server.set_redirect("/non-existing-page-2.html", "/non-existing-page-3.html")
+ server.set_redirect("/non-existing-page-3.html", "/non-existing-page-4.html")
+ server.set_redirect("/non-existing-page-4.html", "/empty.html")
+
+ response = await page.goto(server.PREFIX + "/non-existing-page.html")
+ assert response
+ assert response.status == 200
+ assert "empty.html" in response.url
+
+ assert len(intercepted) == 1
+ assert intercepted[0].resource_type == "document"
+ assert intercepted[0].is_navigation_request()
+ assert "/non-existing-page.html" in intercepted[0].url
+
+ chain = []
+ r: Optional[Request] = response.request
+ while r:
+ chain.append(r)
+ assert r.is_navigation_request()
+ r = r.redirected_from
+
+ assert len(chain) == 5
+ assert "/empty.html" in chain[0].url
+ assert "/non-existing-page-4.html" in chain[1].url
+ assert "/non-existing-page-3.html" in chain[2].url
+ assert "/non-existing-page-2.html" in chain[3].url
+ assert "/non-existing-page.html" in chain[4].url
+ for idx, _ in enumerate(chain):
+ assert chain[idx].redirected_to == (chain[idx - 1] if idx > 0 else None)
+
+
+async def test_page_route_should_work_with_redirects_for_subresources(
+ page: Page, server: Server
+) -> None:
+ intercepted: List[Request] = []
+
+ def _handle(route: Route) -> None:
+ asyncio.create_task(route.continue_())
+ intercepted.append(route.request)
+
+ await page.route(
+ "**/*",
+ _handle,
+ )
+
+ server.set_redirect("/one-style.css", "/two-style.css")
+ server.set_redirect("/two-style.css", "/three-style.css")
+ server.set_redirect("/three-style.css", "/four-style.css")
+ server.set_route(
+ "/four-style.css",
+ lambda req: (req.write(b"body {box-sizing: border-box; }"), req.finish()),
+ )
+
+ response = await page.goto(server.PREFIX + "/one-style.html")
+ assert response
+ assert response.status == 200
+ assert "one-style.html" in response.url
+
+ # TODO: https://github.com/microsoft/playwright/issues/12789
+ assert len(intercepted) >= 2
+ assert intercepted[0].resource_type == "document"
+ assert "one-style.html" in intercepted[0].url
+
+ r: Optional[Request] = intercepted[1]
+ for url in [
+ "/one-style.css",
+ "/two-style.css",
+ "/three-style.css",
+ "/four-style.css",
+ ]:
+ assert r
+ assert r.resource_type == "stylesheet"
+ assert url in r.url
+ r = r.redirected_to
+ assert r is None
+
+
+async def test_page_route_should_work_with_equal_requests(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ hits = [True]
+
+ def handle_request(request: TestServerRequest, hits: List[bool]) -> None:
+ request.write(str(len(hits) * 11).encode())
+ request.finish()
+ hits.append(True)
+
+ server.set_route("/zzz", lambda r: handle_request(r, hits))
+
+ spinner: List[bool] = []
+
+ async def handle_route(route: Route) -> None:
+ if len(spinner) == 1:
+ await route.abort()
+ spinner.pop(0)
+ else:
+ await route.continue_()
+ spinner.append(True)
+
+ # Cancel 2nd request.
+ await page.route("**/*", handle_route)
+
+ results = []
+ for idx in range(3):
+ results.append(
+ await page.evaluate(
+ """() => fetch('/zzz').then(response => response.text()).catch(e => 'FAILED')"""
+ )
+ )
+ assert results == ["11", "FAILED", "22"]
+
+
+async def test_page_route_should_navigate_to_dataURL_and_not_fire_dataURL_requests(
+ page: Page, server: Server
+) -> None:
+ requests = []
+
+ def _handle(route: Route) -> None:
+ requests.append(route.request)
+ asyncio.create_task(route.continue_())
+
+ await page.route(
+ "**/*",
+ _handle,
+ )
+
+ data_URL = "data:text/html,yo
"
+ response = await page.goto(data_URL)
+ assert response is None
+ assert len(requests) == 0
+
+
+async def test_page_route_should_be_able_to_fetch_dataURL_and_not_fire_dataURL_requests(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ requests = []
+
+ def _handle(route: Route) -> None:
+ requests.append(route.request)
+ asyncio.create_task(route.continue_())
+
+ await page.route("**/*", _handle)
+
+ data_URL = "data:text/html,yo
"
+ text = await page.evaluate("url => fetch(url).then(r => r.text())", data_URL)
+ assert text == "yo
"
+ assert len(requests) == 0
+
+
+async def test_page_route_should_navigate_to_URL_with_hash_and_and_fire_requests_without_hash(
+ page: Page, server: Server
+) -> None:
+ requests = []
+
+ def _handle(route: Route) -> None:
+ requests.append(route.request)
+ asyncio.create_task(route.continue_())
+
+ await page.route(
+ "**/*",
+ _handle,
+ )
+
+ response = await page.goto(server.EMPTY_PAGE + "#hash")
+ assert response
+ assert response.status == 200
+ assert response.url == server.EMPTY_PAGE
+ assert len(requests) == 1
+ assert requests[0].url == server.EMPTY_PAGE
+
+
+async def test_page_route_should_work_with_encoded_server(page: Page, server: Server) -> None:
+ # The requestWillBeSent will report encoded URL, whereas interception will
+ # report URL as-is. @see crbug.com/759388
+ await page.route("**/*", lambda route: route.continue_())
+ response = await page.goto(server.PREFIX + "/some nonexisting page")
+ assert response
+ assert response.status == 404
+
+
+async def test_page_route_should_work_with_encoded_server___2(page: Page, server: Server) -> None:
+ # The requestWillBeSent will report URL as-is, whereas interception will
+ # report encoded URL for stylesheet. @see crbug.com/759388
+ requests: List[Request] = []
+
+ def _handle(route: Route) -> None:
+ asyncio.create_task(route.continue_())
+ requests.append(route.request)
+
+ await page.route("**/*", _handle)
+
+ response = await page.goto(
+ f"""data:text/html, """
+ )
+ assert response is None
+ # TODO: https://github.com/microsoft/playwright/issues/12789
+ assert len(requests) >= 1
+ assert (must(await requests[0].response())).status == 404
+
+
+async def test_page_route_should_not_throw_Invalid_Interception_Id_if_the_request_was_cancelled(
+ page: Page, server: Server
+) -> None:
+ await page.set_content("")
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route("**/*", lambda r, _: route_future.set_result(r))
+
+ async with page.expect_request("**/*"):
+ await page.eval_on_selector(
+ "iframe", """(frame, url) => frame.src = url""", server.EMPTY_PAGE
+ )
+ # Delete frame to cause request to be canceled.
+ await page.eval_on_selector("iframe", "frame => frame.remove()")
+ route = await route_future
+ await route.continue_()
+
+
+async def test_page_route_should_intercept_main_resource_during_cross_process_navigation(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ intercepted = []
+
+ def _handle(route: Route) -> None:
+ intercepted.append(True)
+ asyncio.create_task(route.continue_())
+
+ await page.route(
+ server.CROSS_PROCESS_PREFIX + "/empty.html",
+ _handle,
+ )
+
+ response = await page.goto(server.CROSS_PROCESS_PREFIX + "/empty.html")
+ assert response
+ assert response.ok
+ assert len(intercepted) == 1
+
+
+@pytest.mark.skip_browser("webkit")
+async def test_page_route_should_create_a_redirect(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/empty.html")
+
+ async def handle_route(route: Route, request: Request) -> None:
+ if request.url != (server.PREFIX + "/redirect_this"):
+ return await route.continue_()
+ await route.fulfill(status=301, headers={"location": "/empty.html"})
+
+ await page.route(
+ "**/*",
+ handle_route,
+ )
+
+ text = await page.evaluate(
+ """async url => {
+ const data = await fetch(url);
+ return data.text();
+ }""",
+ server.PREFIX + "/redirect_this",
+ )
+ assert text == ""
+
+
+async def test_page_route_should_support_cors_with_GET(
+ page: Page, server: Server, browser_name: str
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+
+ async def handle_route(route: Route, request: Request) -> None:
+ headers = {"access-control-allow-origin": "*" if request.url.endswith("allow") else "none"}
+ await route.fulfill(
+ content_type="application/json",
+ headers=headers,
+ status=200,
+ body=json.dumps(["electric", "gas"]),
+ )
+
+ await page.route(
+ "**/cars*",
+ handle_route,
+ )
+ # Should succeed
+ resp = await page.evaluate(
+ """async () => {
+ const response = await fetch('https://example.com/cars?allow', { mode: 'cors' });
+ return response.json();
+ }"""
+ )
+
+ assert resp == ["electric", "gas"]
+
+ # Should be rejected
+ with pytest.raises(Error) as exc:
+ await page.evaluate(
+ """async () => {
+ const response = await fetch('https://example.com/cars?reject', { mode: 'cors' });
+ return response.json();
+ }"""
+ )
+ if browser_name == "chromium":
+ assert "Failed" in exc.value.message
+ elif browser_name == "webkit":
+ assert "TypeError" in exc.value.message
+ elif browser_name == "firefox":
+ assert "NetworkError" in exc.value.message
+
+
+async def test_page_route_should_support_cors_with_POST(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.route(
+ "**/cars",
+ lambda route: route.fulfill(
+ content_type="application/json",
+ headers={"Access-Control-Allow-Origin": "*"},
+ status=200,
+ body=json.dumps(["electric", "gas"]),
+ ),
+ )
+
+ resp = await page.evaluate(
+ """async () => {
+ const response = await fetch('https://example.com/cars', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ mode: 'cors',
+ body: JSON.stringify({ 'number': 1 })
+ });
+ return response.json();
+ }"""
+ )
+
+ assert resp == ["electric", "gas"]
+
+
+async def test_page_route_should_support_cors_for_different_methods(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.route(
+ "**/cars",
+ lambda route, request: route.fulfill(
+ content_type="application/json",
+ headers={"Access-Control-Allow-Origin": "*"},
+ status=200,
+ body=json.dumps([request.method, "electric", "gas"]),
+ ),
+ )
+
+ # First POST
+ resp = await page.evaluate(
+ """async () => {
+ const response = await fetch('https://example.com/cars', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ mode: 'cors',
+ body: JSON.stringify({ 'number': 1 })
+ });
+ return response.json();
+ }"""
+ )
+
+ assert resp == ["POST", "electric", "gas"]
+ # Then DELETE
+ resp = await page.evaluate(
+ """async () => {
+ const response = await fetch('https://example.com/cars', {
+ method: 'DELETE',
+ headers: {},
+ mode: 'cors',
+ body: ''
+ });
+ return response.json();
+ }"""
+ )
+
+ assert resp == ["DELETE", "electric", "gas"]
+
+
+async def test_request_fulfill_should_work_a(page: Page, server: Server) -> None:
+ await page.route(
+ "**/*",
+ lambda route: route.fulfill(
+ status=201,
+ headers={"foo": "bar"},
+ content_type="text/html",
+ body="Yo, page!",
+ ),
+ )
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 201
+ assert response.headers["foo"] == "bar"
+ assert await page.evaluate("() => document.body.textContent") == "Yo, page!"
+
+
+async def test_request_fulfill_should_work_with_status_code_422(page: Page, server: Server) -> None:
+ await page.route(
+ "**/*",
+ lambda route: route.fulfill(status=422, body="Yo, page!"),
+ )
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 422
+ assert response.status_text == "Unprocessable Entity"
+ assert await page.evaluate("() => document.body.textContent") == "Yo, page!"
+
+
+async def test_request_fulfill_should_allow_mocking_binary_responses(
+ page: Page,
+ server: Server,
+ assert_to_be_golden: Callable[[bytes, str], None],
+ assetdir: Path,
+) -> None:
+ await page.route(
+ "**/*",
+ lambda route: route.fulfill(
+ content_type="image/png",
+ body=(assetdir / "pptr.png").read_bytes(),
+ ),
+ )
+
+ await page.evaluate(
+ """PREFIX => {
+ const img = document.createElement('img');
+ img.src = PREFIX + '/does-not-exist.png';
+ document.body.appendChild(img);
+ return new Promise(fulfill => img.onload = fulfill);
+ }""",
+ server.PREFIX,
+ )
+ img = await page.query_selector("img")
+ assert img
+ assert_to_be_golden(await img.screenshot(), "mock-binary-response.png")
+
+
+async def test_request_fulfill_should_allow_mocking_svg_with_charset(
+ page: Page, server: Server, assert_to_be_golden: Callable[[bytes, str], None]
+) -> None:
+ await page.route(
+ "**/*",
+ lambda route: route.fulfill(
+ content_type="image/svg+xml ; charset=utf-8",
+ body=' ',
+ ),
+ )
+
+ await page.evaluate(
+ """PREFIX => {
+ const img = document.createElement('img');
+ img.src = PREFIX + '/does-not-exist.svg';
+ document.body.appendChild(img);
+ return new Promise((f, r) => { img.onload = f; img.onerror = r; });
+ }""",
+ server.PREFIX,
+ )
+ img = await page.query_selector("img")
+ assert img
+ assert_to_be_golden(await img.screenshot(), "mock-svg.png")
+
+
+async def test_request_fulfill_should_work_with_file_path(
+ page: Page,
+ server: Server,
+ assert_to_be_golden: Callable[[bytes, str], None],
+ assetdir: Path,
+) -> None:
+ await page.route(
+ "**/*",
+ lambda route: route.fulfill(content_type="shouldBeIgnored", path=assetdir / "pptr.png"),
+ )
+ await page.evaluate(
+ """PREFIX => {
+ const img = document.createElement('img');
+ img.src = PREFIX + '/does-not-exist.png';
+ document.body.appendChild(img);
+ return new Promise(fulfill => img.onload = fulfill);
+ }""",
+ server.PREFIX,
+ )
+ img = await page.query_selector("img")
+ assert img
+ assert_to_be_golden(await img.screenshot(), "mock-binary-response.png")
+
+
+async def test_request_fulfill_should_stringify_intercepted_request_response_headers(
+ page: Page, server: Server
+) -> None:
+ await page.route(
+ "**/*",
+ lambda route: route.fulfill(
+ status=200, headers={"foo": True}, body="Yo, page!" # type: ignore
+ ),
+ )
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 200
+ headers = response.headers
+ assert headers["foo"] == "True"
+ assert await page.evaluate("() => document.body.textContent") == "Yo, page!"
+
+
+async def test_request_fulfill_should_not_modify_the_headers_sent_to_the_server(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/empty.html")
+ interceptedRequests = []
+
+ # this is just to enable request interception, which disables caching in chromium
+ await page.route(server.PREFIX + "/unused", lambda route, req: None)
+
+ def _handler1(response: TestServerRequest) -> None:
+ interceptedRequests.append(response)
+ response.setHeader("Access-Control-Allow-Origin", "*")
+ response.write(b"done")
+ response.finish()
+
+ server.set_route("/something", _handler1)
+
+ text = await page.evaluate(
+ """async url => {
+ const data = await fetch(url);
+ return data.text();
+ }""",
+ server.CROSS_PROCESS_PREFIX + "/something",
+ )
+ assert text == "done"
+
+ playwrightRequest: "asyncio.Future[Request]" = asyncio.Future()
+
+ def _handler2(route: Route, request: Request) -> None:
+ playwrightRequest.set_result(request)
+ asyncio.create_task(route.continue_(headers={**request.headers}))
+
+ await page.route(
+ server.CROSS_PROCESS_PREFIX + "/something",
+ _handler2,
+ )
+
+ textAfterRoute = await page.evaluate(
+ """async url => {
+ const data = await fetch(url);
+ return data.text();
+ }""",
+ server.CROSS_PROCESS_PREFIX + "/something",
+ )
+ assert textAfterRoute == "done"
+
+ assert len(interceptedRequests) == 2
+ assert interceptedRequests[0].requestHeaders == interceptedRequests[1].requestHeaders
+
+
+async def test_request_fulfill_should_include_the_origin_header(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/empty.html")
+ interceptedRequest = []
+
+ def _handle(route: Route, request: Request) -> None:
+ interceptedRequest.append(request)
+ asyncio.create_task(
+ route.fulfill(
+ headers={"Access-Control-Allow-Origin": "*"},
+ content_type="text/plain",
+ body="done",
+ )
+ )
+
+ await page.route(server.CROSS_PROCESS_PREFIX + "/something", _handle)
+
+ text = await page.evaluate(
+ """async url => {
+ const data = await fetch(url);
+ return data.text();
+ }""",
+ server.CROSS_PROCESS_PREFIX + "/something",
+ )
+ assert text == "done"
+ assert len(interceptedRequest) == 1
+ assert interceptedRequest[0].headers["origin"] == server.PREFIX
+
+
+async def test_request_fulfill_should_work_with_request_interception(
+ page: Page, server: Server
+) -> None:
+ requests = {}
+
+ async def _handle_route(route: Route) -> None:
+ requests[route.request.url.split("/").pop()] = route.request
+ await route.continue_()
+
+ await page.route("**/*", _handle_route)
+
+ server.set_redirect("/rrredirect", "/frames/one-frame.html")
+ await page.goto(server.PREFIX + "/rrredirect")
+ assert requests["rrredirect"].is_navigation_request()
+ assert requests["frame.html"].is_navigation_request()
+ assert requests["script.js"].is_navigation_request() is False
+ assert requests["style.css"].is_navigation_request() is False
+
+
+async def test_Interception_should_work_with_request_interception(
+ browser: Browser, https_server: Server
+) -> None:
+ context = await browser.new_context(ignore_https_errors=True)
+ page = await context.new_page()
+
+ await page.route("**/*", lambda route: asyncio.ensure_future(route.continue_()))
+ response = await page.goto(https_server.EMPTY_PAGE)
+ assert response
+ assert response.status == 200
+ await context.close()
+
+
+async def test_ignore_http_errors_service_worker_should_intercept_after_a_service_worker(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html")
+ await page.evaluate("() => window.activationPromise")
+
+ # Sanity check.
+ sw_response = await page.evaluate('() => fetchDummy("foo")')
+ assert sw_response == "responseFromServiceWorker:foo"
+
+ def _handle_route(route: Route) -> None:
+ asyncio.ensure_future(
+ route.fulfill(
+ status=200,
+ content_type="text/css",
+ body="responseFromInterception:" + route.request.url.split("/")[-1],
+ )
+ )
+
+ await page.route("**/foo", _handle_route)
+
+ # Page route is applied after service worker fetch event.
+ sw_response2 = await page.evaluate('() => fetchDummy("foo")')
+ assert sw_response2 == "responseFromServiceWorker:foo"
+
+ # Page route is not applied to service worker initiated fetch.
+ non_intercepted_response = await page.evaluate('() => fetchDummy("passthrough")')
+ assert non_intercepted_response == "FAILURE: Not Found"
+
+
+async def test_page_route_should_support_times_parameter(page: Page, server: Server) -> None:
+ intercepted = []
+
+ async def handle_request(route: Route) -> None:
+ await route.continue_()
+ intercepted.append(True)
+
+ await page.route("**/empty.html", handle_request, times=1)
+
+ await page.goto(server.EMPTY_PAGE)
+ await page.goto(server.EMPTY_PAGE)
+ await page.goto(server.EMPTY_PAGE)
+ assert len(intercepted) == 1
+
+
+async def test_should_work_if_handler_with_times_parameter_was_removed_from_another_handler(
+ context: BrowserContext, page: Page, server: Server
+) -> None:
+ intercepted = []
+
+ async def handler(route: Route) -> None:
+ intercepted.append("first")
+ await route.continue_()
+
+ await page.route("**/*", handler, times=1)
+
+ async def handler2(route: Route) -> None:
+ intercepted.append("second")
+ await page.unroute("**/*", handler)
+ await route.fallback()
+
+ await page.route("**/*", handler2)
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == ["second"]
+ intercepted.clear()
+ await page.goto(server.EMPTY_PAGE)
+ assert intercepted == ["second"]
+
+
+async def test_should_fulfill_with_global_fetch_result(
+ page: Page, playwright: Playwright, server: Server
+) -> None:
+ async def handle_request(route: Route) -> None:
+ request = await playwright.request.new_context()
+ response = await request.get(server.PREFIX + "/simple.json")
+ await route.fulfill(response=response)
+ await request.dispose()
+
+ await page.route("**/*", handle_request)
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 200
+ assert await response.json() == {"foo": "bar"}
+
+
+async def test_glob_to_regex() -> None:
+ assert glob_to_regex("**/*.js").match("https://localhost:8080/foo.js")
+ assert not glob_to_regex("**/*.css").match("https://localhost:8080/foo.js")
+ assert not glob_to_regex("*.js").match("https://localhost:8080/foo.js")
+ assert glob_to_regex("https://**/*.js").match("https://localhost:8080/foo.js")
+ assert glob_to_regex("http://localhost:8080/simple/path.js").match(
+ "http://localhost:8080/simple/path.js"
+ )
+ assert glob_to_regex("http://localhost:8080/?imple/path.js").match(
+ "http://localhost:8080/Simple/path.js"
+ )
+ assert glob_to_regex("**/{a,b}.js").match("https://localhost:8080/a.js")
+ assert glob_to_regex("**/{a,b}.js").match("https://localhost:8080/b.js")
+ assert not glob_to_regex("**/{a,b}.js").match("https://localhost:8080/c.js")
+
+ assert glob_to_regex("**/*.{png,jpg,jpeg}").match("https://localhost:8080/c.jpg")
+ assert glob_to_regex("**/*.{png,jpg,jpeg}").match("https://localhost:8080/c.jpeg")
+ assert glob_to_regex("**/*.{png,jpg,jpeg}").match("https://localhost:8080/c.png")
+ assert not glob_to_regex("**/*.{png,jpg,jpeg}").match("https://localhost:8080/c.css")
+ assert glob_to_regex("foo*").match("foo.js")
+ assert not glob_to_regex("foo*").match("foo/bar.js")
+ assert not glob_to_regex("http://localhost:3000/signin-oidc*").match(
+ "http://localhost:3000/signin-oidc/foo"
+ )
+ assert glob_to_regex("http://localhost:3000/signin-oidc*").match(
+ "http://localhost:3000/signin-oidcnice"
+ )
+
+ assert glob_to_regex("**/three-columns/settings.html?**id=[a-z]**").match(
+ "http://mydomain:8080/blah/blah/three-columns/settings.html?id=settings-e3c58efe-02e9-44b0-97ac-dd138100cf7c&blah"
+ )
+
+ assert glob_to_regex("\\?") == re.compile(r"^\?$")
+ assert glob_to_regex("\\") == re.compile(r"^\\$")
+ assert glob_to_regex("\\\\") == re.compile(r"^\\$")
+ assert glob_to_regex("\\[") == re.compile(r"^\[$")
+ assert glob_to_regex("[a-z]") == re.compile(r"^[a-z]$")
+ assert glob_to_regex("$^+.\\*()|\\?\\{\\}\\[\\]") == re.compile(r"^\$\^\+\.\*\(\)\|\?\{\}\[\]$")
diff --git a/tests/async/test_page_select_option.py b/tests/async/test_page_select_option.py
new file mode 100644
index 0000000..370a9db
--- /dev/null
+++ b/tests/async/test_page_select_option.py
@@ -0,0 +1,214 @@
+# 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 pytest
+
+from playwright.async_api import Error, Page, TimeoutError
+from tests.server import Server
+
+
+async def test_select_option_should_select_single_option(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.select_option("select", "blue")
+ assert await page.evaluate("result.onInput") == ["blue"]
+ assert await page.evaluate("result.onChange") == ["blue"]
+
+
+async def test_select_option_should_select_single_option_by_value(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.select_option("select", "blue")
+ assert await page.evaluate("result.onInput") == ["blue"]
+ assert await page.evaluate("result.onChange") == ["blue"]
+
+
+async def test_select_option_should_select_single_option_by_label(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.select_option("select", label="Indigo")
+ assert await page.evaluate("result.onInput") == ["indigo"]
+ assert await page.evaluate("result.onChange") == ["indigo"]
+
+
+async def test_select_option_should_select_single_option_by_handle(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.select_option("select", element=await page.query_selector("[id=whiteOption]"))
+ assert await page.evaluate("result.onInput") == ["white"]
+ assert await page.evaluate("result.onChange") == ["white"]
+
+
+async def test_select_option_should_select_single_option_by_index(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.select_option("select", index=2)
+ assert await page.evaluate("result.onInput") == ["brown"]
+ assert await page.evaluate("result.onChange") == ["brown"]
+
+
+async def test_select_option_should_select_only_first_option(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.select_option("select", ["blue", "green", "red"])
+ assert await page.evaluate("result.onInput") == ["blue"]
+ assert await page.evaluate("result.onChange") == ["blue"]
+
+
+async def test_select_option_should_not_throw_when_select_causes_navigation(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.eval_on_selector(
+ "select",
+ "select => select.addEventListener('input', () => window.location = '/empty.html')",
+ )
+ async with page.expect_navigation():
+ await page.select_option("select", "blue")
+ assert "empty.html" in page.url
+
+
+async def test_select_option_should_select_multiple_options(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.evaluate("makeMultiple()")
+ await page.select_option("select", ["blue", "green", "red"])
+ assert await page.evaluate("result.onInput") == ["blue", "green", "red"]
+ assert await page.evaluate("result.onChange") == ["blue", "green", "red"]
+
+
+async def test_select_option_should_select_multiple_options_with_attributes(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.evaluate("makeMultiple()")
+ await page.select_option(
+ "select",
+ value="blue",
+ label="Green",
+ index=4,
+ )
+ assert await page.evaluate("result.onInput") == ["blue", "gray", "green"]
+ assert await page.evaluate("result.onChange") == ["blue", "gray", "green"]
+
+
+async def test_select_option_should_respect_event_bubbling(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.select_option("select", "blue")
+ assert await page.evaluate("result.onBubblingInput") == ["blue"]
+ assert await page.evaluate("result.onBubblingChange") == ["blue"]
+
+
+async def test_select_option_should_throw_when_element_is_not_a__select_(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ with pytest.raises(Error) as exc_info:
+ await page.select_option("body", "")
+ assert "Element is not a element" in exc_info.value.message
+
+
+async def test_select_option_should_return_on_no_matched_values(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ with pytest.raises(TimeoutError) as exc_info:
+ await page.select_option("select", ["42", "abc"], timeout=1000)
+ assert "Timeout 1000" in exc_info.value.message
+
+
+async def test_select_option_should_return_an_array_of_matched_values(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.evaluate("makeMultiple()")
+ result = await page.select_option("select", ["blue", "black", "magenta"])
+ assert result == ["black", "blue", "magenta"]
+
+
+async def test_select_option_should_return_an_array_of_one_element_when_multiple_is_not_set(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ result = await page.select_option("select", ["42", "blue", "black", "magenta"])
+ assert len(result) == 1
+
+
+async def test_select_option_should_return_on_no_values(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ result = await page.select_option("select", [])
+ assert result == []
+
+
+async def test_select_option_should_not_allow_null_items(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.evaluate("makeMultiple()")
+ with pytest.raises(Error) as exc_info:
+ await page.select_option("select", ["blue", None, "black", "magenta"]) # type: ignore
+ assert "expected string, got object" in exc_info.value.message
+
+
+async def test_select_option_should_unselect_with_null(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.evaluate("makeMultiple()")
+ result = await page.select_option("select", ["blue", "black", "magenta"])
+ assert result == ["black", "blue", "magenta"]
+ await page.select_option("select", None)
+ assert await page.eval_on_selector(
+ "select",
+ "select => Array.from(select.options).every(option => !option.selected)",
+ )
+
+
+async def test_select_option_should_deselect_all_options_when_passed_no_values_for_a_multiple_select(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.evaluate("makeMultiple()")
+ await page.select_option("select", ["blue", "black", "magenta"])
+ await page.select_option("select", [])
+ assert await page.eval_on_selector(
+ "select",
+ "select => Array.from(select.options).every(option => !option.selected)",
+ )
+
+
+async def test_select_option_should_deselect_all_options_when_passed_no_values_for_a_select_without_multiple(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.select_option("select", ["blue", "black", "magenta"])
+ await page.select_option("select", [])
+ assert await page.eval_on_selector(
+ "select",
+ "select => Array.from(select.options).every(option => !option.selected)",
+ )
+
+
+async def test_select_option_should_work_when_re_defining_top_level_event_class(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.evaluate("window.Event = null")
+ await page.select_option("select", "blue")
+ assert await page.evaluate("result.onInput") == ["blue"]
+ assert await page.evaluate("result.onChange") == ["blue"]
+
+
+async def test_select_options_should_fall_back_to_selecting_by_label(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.PREFIX + "/input/select.html")
+ await page.select_option("select", "Blue")
+ assert await page.evaluate("result.onInput") == ["blue"]
+ assert await page.evaluate("result.onChange") == ["blue"]
diff --git a/tests/async/test_pdf.py b/tests/async/test_pdf.py
new file mode 100644
index 0000000..876cd34
--- /dev/null
+++ b/tests/async/test_pdf.py
@@ -0,0 +1,43 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+from pathlib import Path
+
+import pytest
+
+from playwright.async_api import Page
+from tests.server import Server
+
+pytestmark = pytest.mark.only_browser("chromium")
+
+
+async def test_should_be_able_to_save_pdf_file(page: Page, tmpdir: Path) -> None:
+ output_file = tmpdir / "foo.png"
+ await page.pdf(path=str(output_file))
+ assert os.path.getsize(output_file) > 0
+
+
+async def test_should_be_able_capture_pdf_without_path(page: Page) -> None:
+ buffer = await page.pdf()
+ assert buffer
+
+
+async def test_should_be_able_to_generate_outline(page: Page, server: Server, tmpdir: Path) -> None:
+ await page.goto(server.PREFIX + "/headings.html")
+ output_file_no_outline = tmpdir / "outputNoOutline.pdf"
+ output_file_outline = tmpdir / "outputOutline.pdf"
+ await page.pdf(path=output_file_no_outline)
+ await page.pdf(path=output_file_outline, tagged=True, outline=True)
+ assert os.path.getsize(output_file_outline) > os.path.getsize(output_file_no_outline)
diff --git a/tests/async/test_popup.py.disabled b/tests/async/test_popup.py.disabled
new file mode 100644
index 0000000..c153f16
--- /dev/null
+++ b/tests/async/test_popup.py.disabled
@@ -0,0 +1,448 @@
+# 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
+from typing import List, Optional
+
+from playwright.async_api import Browser, BrowserContext, Request, Route
+
+from tests.server import Server
+from tests.utils import must
+
+
+async def test_link_navigation_inherit_user_agent_from_browser_context(
+ browser: Browser, server: Server
+) -> None:
+ context = await browser.new_context(user_agent="hey")
+
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content('link ')
+ request_waitable = asyncio.create_task(server.wait_for_request("/popup/popup.html"))
+ await asyncio.sleep(0) # execute scheduled tasks, but don't await them
+ async with context.expect_page() as page_info:
+ await page.click("a")
+ popup = await page_info.value
+ await popup.wait_for_load_state("domcontentloaded")
+ user_agent = await popup.evaluate("window.initialUserAgent")
+ request = await request_waitable
+ assert user_agent == "hey"
+ assert request.requestHeaders.getRawHeaders("user-agent") == ["hey"]
+ await context.close()
+
+
+async def test_link_navigation_respect_routes_from_browser_context(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content('link ')
+
+ intercepted: List[bool] = []
+
+ async def handle_request(route: Route) -> None:
+ intercepted.append(True)
+ await route.continue_()
+
+ await context.route("**/empty.html", handle_request)
+ async with context.expect_page():
+ await page.click("a")
+ assert intercepted == [True]
+
+
+async def test_window_open_inherit_user_agent_from_browser_context(
+ browser: Browser, server: Server
+) -> None:
+ context = await browser.new_context(user_agent="hey")
+
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ request_promise = asyncio.create_task(server.wait_for_request("/dummy.html"))
+ await asyncio.sleep(0) # execute scheduled tasks, but don't await them
+ user_agent = await page.evaluate(
+ """url => {
+ win = window.open(url)
+ return win.navigator.userAgent
+ }""",
+ server.PREFIX + "/dummy.html",
+ )
+ request = await request_promise
+ assert user_agent == "hey"
+ assert request.requestHeaders.getRawHeaders("user-agent") == ["hey"]
+ await context.close()
+
+
+async def test_should_inherit_extra_headers_from_browser_context(
+ browser: Browser, server: Server
+) -> None:
+ context = await browser.new_context(extra_http_headers={"foo": "bar"})
+
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ request_promise = asyncio.create_task(server.wait_for_request("/dummy.html"))
+ await asyncio.sleep(0) # execute scheduled tasks, but don't await them
+ await page.evaluate("url => window._popup = window.open(url)", server.PREFIX + "/dummy.html")
+ request = await request_promise
+ assert request.requestHeaders.getRawHeaders("foo") == ["bar"]
+ await context.close()
+
+
+async def test_should_inherit_offline_from_browser_context(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ await context.set_offline(True)
+ online = await page.evaluate(
+ """url => {
+ win = window.open(url)
+ return win.navigator.onLine
+ }""",
+ server.PREFIX + "/dummy.html",
+ )
+ assert online is False
+
+
+async def test_should_inherit_http_credentials_from_browser_context(
+ browser: Browser, server: Server
+) -> None:
+ server.set_auth("/title.html", "user", "pass")
+ context = await browser.new_context(http_credentials={"username": "user", "password": "pass"})
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await page.evaluate(
+ "url => window._popup = window.open(url)", server.PREFIX + "/title.html"
+ )
+ popup = await popup_info.value
+ await popup.wait_for_load_state("domcontentloaded")
+ assert await popup.title() == "Woof-Woof"
+ await context.close()
+
+
+async def test_should_inherit_touch_support_from_browser_context(
+ browser: Browser, server: Server
+) -> None:
+ context = await browser.new_context(viewport={"width": 400, "height": 500}, has_touch=True)
+
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ has_touch = await page.evaluate(
+ """() => {
+ win = window.open('')
+ return 'ontouchstart' in win
+ }"""
+ )
+
+ assert has_touch
+ await context.close()
+
+
+async def test_should_inherit_viewport_size_from_browser_context(
+ browser: Browser, server: Server
+) -> None:
+ context = await browser.new_context(viewport={"width": 400, "height": 500})
+
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ size = await page.evaluate(
+ """() => {
+ win = window.open('about:blank')
+ return { width: win.innerWidth, height: win.innerHeight }
+ }"""
+ )
+
+ assert size == {"width": 400, "height": 500}
+ await context.close()
+
+
+@pytest.mark.skip(reason="Not supported by Camoufox")
+async def test_should_use_viewport_size_from_window_features(
+ browser: Browser, server: Server
+) -> None:
+ context = await browser.new_context(viewport={"width": 700, "height": 700})
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ size = None
+ async with page.expect_popup() as popup_info:
+ size = await page.evaluate(
+ """async () => {
+ 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');
+ await new Promise(resolve => {
+ const interval = setInterval(() => {
+ if (win.innerWidth === 600 && win.innerHeight === 300) {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 10);
+ });
+ return { width: win.innerWidth, height: win.innerHeight }
+ }"""
+ )
+ popup = await popup_info.value
+ await popup.set_viewport_size({"width": 500, "height": 400})
+ await popup.wait_for_load_state()
+ resized = await popup.evaluate(
+ "() => ({ width: window.innerWidth, height: window.innerHeight })"
+ )
+ await context.close()
+ assert size == {"width": 600, "height": 300}
+ assert resized == {"width": 500, "height": 400}
+
+
+async def test_should_respect_routes_from_browser_context(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+
+ def handle_request(route: Route, request: Request, intercepted: List[bool]) -> None:
+ asyncio.create_task(route.continue_())
+ intercepted.append(True)
+
+ intercepted: List[bool] = []
+ await context.route(
+ "**/empty.html",
+ lambda route, request: handle_request(route, request, intercepted),
+ )
+
+ async with page.expect_popup():
+ await page.evaluate("url => window.__popup = window.open(url)", server.EMPTY_PAGE)
+ assert len(intercepted) == 1
+
+
+async def test_browser_context_add_init_script_should_apply_to_an_in_process_popup(
+ context: BrowserContext, server: Server
+) -> None:
+ await context.add_init_script("window.injected = 123")
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ injected = await page.evaluate(
+ """() => {
+ const win = window.open('about:blank');
+ return win.injected;
+ }"""
+ )
+
+ assert injected == 123
+
+
+async def test_browser_context_add_init_script_should_apply_to_a_cross_process_popup(
+ context: BrowserContext, server: Server
+) -> None:
+ await context.add_init_script("window.injected = 123")
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("url => window.open(url)", server.CROSS_PROCESS_PREFIX + "/title.html")
+ popup = await popup_info.value
+ assert await popup.evaluate("injected") == 123
+ await popup.reload()
+ assert await popup.evaluate("injected") == 123
+
+
+async def test_should_expose_function_from_browser_context(
+ context: BrowserContext, server: Server
+) -> None:
+ await context.expose_function("add", lambda a, b: a + b)
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ added = await page.evaluate(
+ """async () => {
+ win = window.open('about:blank')
+ return win.add(9, 4)
+ }"""
+ )
+
+ assert added == 13
+
+
+async def test_should_work(context: BrowserContext) -> None:
+ page = await context.new_page()
+ async with page.expect_popup() as popup_info:
+ await page.evaluate('window.__popup = window.open("about:blank")')
+ popup = await popup_info.value
+ assert await page.evaluate("!!window.opener") is False
+ assert await popup.evaluate("!!window.opener")
+
+
+async def test_should_work_with_window_features(context: BrowserContext, server: Server) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await page.evaluate(
+ 'window.__popup = window.open(window.location.href, "Title", "toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=200,top=0,left=0")'
+ )
+ popup = await popup_info.value
+ assert await page.evaluate("!!window.opener") is False
+ assert await popup.evaluate("!!window.opener")
+
+
+async def test_window_open_emit_for_immediately_closed_popups(
+ context: BrowserContext,
+) -> None:
+ page = await context.new_page()
+ async with page.expect_popup() as popup_info:
+ await page.evaluate(
+ """() => {
+ win = window.open('about:blank')
+ win.close()
+ }"""
+ )
+ popup = await popup_info.value
+ assert popup
+
+
+async def test_should_emit_for_immediately_closed_popups(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await page.evaluate(
+ """() => {
+ win = window.open(window.location.href)
+ win.close()
+ }"""
+ )
+ popup = await popup_info.value
+ assert popup
+
+
+async def test_should_be_able_to_capture_alert(context: BrowserContext) -> None:
+ page = await context.new_page()
+ evaluate_task: Optional[asyncio.Future] = None
+
+ async def evaluate() -> None:
+ nonlocal evaluate_task
+ evaluate_task = asyncio.create_task(
+ page.evaluate(
+ """() => {
+ const win = window.open('')
+ win.alert('hello')
+ }"""
+ )
+ )
+
+ [popup, dialog, _] = await asyncio.gather(
+ page.wait_for_event("popup"), context.wait_for_event("dialog"), evaluate()
+ )
+
+ assert dialog.message == "hello"
+ assert dialog.page == popup
+ await dialog.dismiss()
+ await must(evaluate_task)
+
+
+async def test_should_work_with_empty_url(context: BrowserContext) -> None:
+ page = await context.new_page()
+ async with page.expect_popup() as popup_info:
+ await page.evaluate("() => window.__popup = window.open('')")
+ popup = await popup_info.value
+ assert await page.evaluate("!!window.opener") is False
+ assert await popup.evaluate("!!window.opener")
+
+
+async def test_should_work_with_noopener_and_no_url(context: BrowserContext) -> None:
+ page = await context.new_page()
+ async with page.expect_popup() as popup_info:
+ await page.evaluate('() => window.__popup = window.open(undefined, null, "noopener")')
+ popup = await popup_info.value
+ # Chromium reports 'about:blank#blocked' here.
+ assert popup.url.split("#")[0] == "about:blank"
+ assert await page.evaluate("!!window.opener") is False
+ assert await popup.evaluate("!!window.opener") is False
+
+
+async def test_should_work_with_noopener_and_about_blank(
+ context: BrowserContext,
+) -> None:
+ page = await context.new_page()
+ async with page.expect_popup() as popup_info:
+ await page.evaluate('() => window.__popup = window.open("about:blank", null, "noopener")')
+ popup = await popup_info.value
+ assert await page.evaluate("!!window.opener") is False
+ assert await popup.evaluate("!!window.opener") is False
+
+
+async def test_should_work_with_noopener_and_url(context: BrowserContext, server: Server) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_popup() as popup_info:
+ await page.evaluate(
+ 'url => window.__popup = window.open(url, null, "noopener")',
+ server.EMPTY_PAGE,
+ )
+ popup = await popup_info.value
+ assert await page.evaluate("!!window.opener") is False
+ assert await popup.evaluate("!!window.opener") is False
+
+
+async def test_should_work_with_clicking_target__blank(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content('yo ')
+ async with page.expect_popup() as popup_info:
+ await page.click("a")
+ popup = await popup_info.value
+ assert await page.evaluate("!!window.opener") is False
+ assert await popup.evaluate("!!window.opener")
+ assert popup.main_frame.page == popup
+
+
+async def test_should_work_with_fake_clicking_target__blank_and_rel_noopener(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content('yo ')
+ async with page.expect_popup() as popup_info:
+ await page.eval_on_selector("a", "a => a.click()")
+ popup = await popup_info.value
+ assert await page.evaluate("!!window.opener") is False
+ assert await popup.evaluate("!!window.opener") is False
+
+
+async def test_should_work_with_clicking_target__blank_and_rel_noopener(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content('yo ')
+ async with page.expect_popup() as popup_info:
+ await page.click("a")
+ popup = await popup_info.value
+ assert await page.evaluate("!!window.opener") is False
+ assert await popup.evaluate("!!window.opener") is False
+
+
+async def test_should_not_treat_navigations_as_new_popups(
+ context: BrowserContext, server: Server
+) -> None:
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content('yo ')
+ async with page.expect_popup() as popup_info:
+ await page.click("a")
+ popup = await popup_info.value
+ handled_popups = []
+ page.on(
+ "popup",
+ lambda popup: handled_popups.append(True),
+ )
+
+ await popup.goto(server.CROSS_PROCESS_PREFIX + "/empty.html")
+ assert len(handled_popups) == 0
diff --git a/tests/async/test_proxy.py b/tests/async/test_proxy.py
new file mode 100644
index 0000000..59b99ff
--- /dev/null
+++ b/tests/async/test_proxy.py
@@ -0,0 +1,131 @@
+# 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 base64
+from typing import Callable
+
+import pytest
+
+from playwright.async_api import Browser, Error
+from tests.server import Server, TestServerRequest
+
+
+async def test_should_throw_for_bad_server_value(
+ browser_factory: "Callable[..., asyncio.Future[Browser]]",
+) -> None:
+ with pytest.raises(Error) as exc_info:
+ await browser_factory(proxy={"server": 123})
+ assert "proxy.server: expected string, got number" in exc_info.value.message
+
+
+async def test_should_use_proxy(
+ browser_factory: "Callable[..., asyncio.Future[Browser]]", server: Server
+) -> None:
+ server.set_route(
+ "/target.html",
+ lambda r: (
+ r.write(b"Served by the proxy "),
+ r.finish(),
+ ),
+ )
+ browser = await browser_factory(proxy={"server": f"localhost:{server.PORT}"})
+ page = await browser.new_page()
+ await page.goto("http://non-existent.com/target.html")
+ assert await page.title() == "Served by the proxy"
+
+
+async def test_should_use_proxy_for_second_page(
+ browser_factory: "Callable[..., asyncio.Future[Browser]]", server: Server
+) -> None:
+ server.set_route(
+ "/target.html",
+ lambda r: (
+ r.write(b"Served by the proxy "),
+ r.finish(),
+ ),
+ )
+ browser = await browser_factory(proxy={"server": f"localhost:{server.PORT}"})
+
+ page1 = await browser.new_page()
+ await page1.goto("http://non-existent.com/target.html")
+ assert await page1.title() == "Served by the proxy"
+
+ page2 = await browser.new_page()
+ await page2.goto("http://non-existent.com/target.html")
+ assert await page2.title() == "Served by the proxy"
+
+
+async def test_should_work_with_ip_port_notion(
+ browser_factory: "Callable[..., asyncio.Future[Browser]]", server: Server
+) -> None:
+ server.set_route(
+ "/target.html",
+ lambda r: (
+ r.write(b"Served by the proxy "),
+ r.finish(),
+ ),
+ )
+ browser = await browser_factory(proxy={"server": f"127.0.0.1:{server.PORT}"})
+ page = await browser.new_page()
+ await page.goto("http://non-existent.com/target.html")
+ assert await page.title() == "Served by the proxy"
+
+
+async def test_should_authenticate(
+ browser_factory: "Callable[..., asyncio.Future[Browser]]", server: Server
+) -> None:
+ def handler(req: TestServerRequest) -> None:
+ auth = req.getHeader("proxy-authorization")
+ if not auth:
+ req.setHeader(b"Proxy-Authenticate", b'Basic realm="Access to internal site"')
+ req.setResponseCode(407)
+ else:
+ req.write(f"{auth} ".encode("utf-8"))
+ req.finish()
+
+ server.set_route("/target.html", handler)
+
+ browser = await browser_factory(
+ proxy={
+ "server": f"localhost:{server.PORT}",
+ "username": "user",
+ "password": "secret",
+ }
+ )
+ page = await browser.new_page()
+ await page.goto("http://non-existent.com/target.html")
+ assert await page.title() == "Basic " + base64.b64encode(b"user:secret").decode("utf-8")
+
+
+async def test_should_authenticate_with_empty_password(
+ browser_factory: "Callable[..., asyncio.Future[Browser]]", server: Server
+) -> None:
+ def handler(req: TestServerRequest) -> None:
+ auth = req.getHeader("proxy-authorization")
+ if not auth:
+ req.setHeader(b"Proxy-Authenticate", b'Basic realm="Access to internal site"')
+ req.setResponseCode(407)
+ else:
+ req.write(f"{auth} ".encode("utf-8"))
+ req.finish()
+
+ server.set_route("/target.html", handler)
+
+ browser = await browser_factory(
+ proxy={"server": f"localhost:{server.PORT}", "username": "user", "password": ""}
+ )
+ page = await browser.new_page()
+ await page.goto("http://non-existent.com/target.html")
+ assert await page.title() == "Basic " + base64.b64encode(b"user:").decode("utf-8")
diff --git a/tests/async/test_queryselector.py b/tests/async/test_queryselector.py
new file mode 100644
index 0000000..69fccf4
--- /dev/null
+++ b/tests/async/test_queryselector.py
@@ -0,0 +1,352 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from pathlib import Path
+
+import pytest
+
+from playwright.async_api import Browser, Error, Page, Selectors
+
+from .utils import Utils
+
+
+async def test_selectors_register_should_work(
+ selectors: Selectors, browser: Browser, browser_name: str
+) -> None:
+ tag_selector = """
+ {
+ create(root, target) {
+ return target.nodeName;
+ },
+ query(root, selector) {
+ return root.querySelector(selector);
+ },
+ queryAll(root, selector) {
+ return Array.from(root.querySelectorAll(selector));
+ }
+ }"""
+
+ selector_name = f"tag_{browser_name}"
+ selector2_name = f"tag2_{browser_name}"
+
+ # Register one engine before creating context.
+ await selectors.register(selector_name, tag_selector)
+
+ context = await browser.new_context()
+ # Register another engine after creating context.
+ await selectors.register(selector2_name, tag_selector)
+
+ page = await context.new_page()
+ await page.set_content("
")
+
+ assert await page.eval_on_selector(f"{selector_name}=DIV", "e => e.nodeName") == "DIV"
+ assert await page.eval_on_selector(f"{selector_name}=SPAN", "e => e.nodeName") == "SPAN"
+ assert await page.eval_on_selector_all(f"{selector_name}=DIV", "es => es.length") == 2
+
+ assert await page.eval_on_selector(f"{selector2_name}=DIV", "e => e.nodeName") == "DIV"
+ assert await page.eval_on_selector(f"{selector2_name}=SPAN", "e => e.nodeName") == "SPAN"
+ assert await page.eval_on_selector_all(f"{selector2_name}=DIV", "es => es.length") == 2
+
+ # Selector names are case-sensitive.
+ with pytest.raises(Error) as exc:
+ await page.query_selector("tAG=DIV")
+ assert 'Unknown engine "tAG" while parsing selector tAG=DIV' in exc.value.message
+
+ await context.close()
+
+
+async def test_selectors_register_should_work_with_path(
+ selectors: Selectors, page: Page, utils: Utils, assetdir: Path
+) -> None:
+ await utils.register_selector_engine(
+ selectors, "foo", path=assetdir / "sectionselectorengine.js"
+ )
+ await page.set_content("")
+ assert await page.eval_on_selector("foo=whatever", "e => e.nodeName") == "SECTION"
+
+
+async def test_selectors_register_should_work_in_main_and_isolated_world(
+ selectors: Selectors, page: Page, utils: Utils
+) -> None:
+ dummy_selector_script = """{
+ create(root, target) { },
+ query(root, selector) {
+ return window.__answer;
+ },
+ queryAll(root, selector) {
+ return window['__answer'] ? [window['__answer'], document.body, document.documentElement] : [];
+ }
+ }"""
+
+ await utils.register_selector_engine(selectors, "main", dummy_selector_script)
+ await utils.register_selector_engine(
+ selectors, "isolated", dummy_selector_script, content_script=True
+ )
+ await page.set_content("
")
+ await page.evaluate('() => window.__answer = document.querySelector("span")')
+ # Works in main if asked.
+ assert await page.eval_on_selector("main=ignored", "e => e.nodeName") == "SPAN"
+ assert await page.eval_on_selector("css=div >> main=ignored", "e => e.nodeName") == "SPAN"
+ assert await page.eval_on_selector_all("main=ignored", "es => window.__answer !== undefined")
+ assert await page.eval_on_selector_all("main=ignored", "es => es.filter(e => e).length") == 3
+ # Works in isolated by default.
+ assert await page.query_selector("isolated=ignored") is None
+ assert await page.query_selector("css=div >> isolated=ignored") is None
+ # $$eval always works in main, to avoid adopting nodes one by one.
+ assert await page.eval_on_selector_all(
+ "isolated=ignored", "es => window.__answer !== undefined"
+ )
+ assert (
+ await page.eval_on_selector_all("isolated=ignored", "es => es.filter(e => e).length") == 3
+ )
+ # At least one engine in main forces all to be in main.
+ assert (
+ await page.eval_on_selector("main=ignored >> isolated=ignored", "e => e.nodeName") == "SPAN"
+ )
+ assert (
+ await page.eval_on_selector("isolated=ignored >> main=ignored", "e => e.nodeName") == "SPAN"
+ )
+ # Can be chained to css.
+ assert (
+ await page.eval_on_selector("main=ignored >> css=section", "e => e.nodeName") == "SECTION"
+ )
+
+
+async def test_selectors_register_should_handle_errors(
+ selectors: Selectors, page: Page, utils: Utils
+) -> None:
+ with pytest.raises(Error) as exc:
+ await page.query_selector("neverregister=ignored")
+ assert (
+ 'Unknown engine "neverregister" while parsing selector neverregister=ignored'
+ in exc.value.message
+ )
+
+ dummy_selector_engine_script = """{
+ create(root, target) {
+ return target.nodeName;
+ },
+ query(root, selector) {
+ return root.querySelector('dummy');
+ },
+ queryAll(root, selector) {
+ return Array.from(root.query_selector_all('dummy'));
+ }
+ }"""
+
+ with pytest.raises(Error) as exc:
+ await selectors.register("$", dummy_selector_engine_script)
+ assert (
+ exc.value.message
+ == "Selectors.register: Selector engine name may only contain [a-zA-Z0-9_] characters"
+ )
+
+ # Selector names are case-sensitive.
+ await utils.register_selector_engine(selectors, "dummy", dummy_selector_engine_script)
+ await utils.register_selector_engine(selectors, "duMMy", dummy_selector_engine_script)
+
+ with pytest.raises(Error) as exc:
+ await selectors.register("dummy", dummy_selector_engine_script)
+ assert (
+ exc.value.message
+ == 'Selectors.register: "dummy" selector engine has been already registered'
+ )
+
+ with pytest.raises(Error) as exc:
+ await selectors.register("css", dummy_selector_engine_script)
+ assert exc.value.message == 'Selectors.register: "css" is a predefined selector engine'
+
+
+async def test_should_work_with_layout_selectors(page: Page) -> None:
+ # +--+ +--+
+ # | 1| | 2|
+ # +--+ ++-++
+ # | 3| | 4|
+ # +-------+ ++-++
+ # | 0 | | 5|
+ # | +--+ +--+--+
+ # | | 6| | 7|
+ # | +--+ +--+
+ # | |
+ # O-------+
+ # +--+
+ # | 8|
+ # +--++--+
+ # | 9|
+ # +--+
+
+ boxes = [
+ # x, y, width, height
+ [0, 0, 150, 150],
+ [100, 200, 50, 50],
+ [200, 200, 50, 50],
+ [100, 150, 50, 50],
+ [201, 150, 50, 50],
+ [200, 100, 50, 50],
+ [50, 50, 50, 50],
+ [150, 50, 50, 50],
+ [150, -51, 50, 50],
+ [201, -101, 50, 50],
+ ]
+ await page.set_content(
+ ' '
+ )
+ await page.eval_on_selector(
+ "container",
+ """(container, boxes) => {
+ for (let i = 0; i < boxes.length; i++) {
+ const div = document.createElement('div');
+ div.style.position = 'absolute';
+ div.style.overflow = 'hidden';
+ div.style.boxSizing = 'border-box';
+ div.style.border = '1px solid black';
+ div.id = 'id' + i;
+ div.textContent = 'id' + i;
+ const box = boxes[i];
+ div.style.left = box[0] + 'px';
+ // Note that top is a flipped y coordinate.
+ div.style.top = (250 - box[1] - box[3]) + 'px';
+ div.style.width = box[2] + 'px';
+ div.style.height = box[3] + 'px';
+ container.appendChild(div);
+ const span = document.createElement('span');
+ span.textContent = '' + i;
+ div.appendChild(span);
+ }
+ }""",
+ boxes,
+ )
+
+ assert await page.eval_on_selector("div:right-of(#id6)", "e => e.id") == "id7"
+ assert await page.eval_on_selector("div:right-of(#id1)", "e => e.id") == "id2"
+ assert await page.eval_on_selector("div:right-of(#id3)", "e => e.id") == "id4"
+ assert await page.query_selector("div:right-of(#id4)") is None
+ assert await page.eval_on_selector("div:right-of(#id0)", "e => e.id") == "id7"
+ assert await page.eval_on_selector("div:right-of(#id8)", "e => e.id") == "id9"
+ assert (
+ await page.eval_on_selector_all("div:right-of(#id3)", "els => els.map(e => e.id).join(',')")
+ == "id4,id2,id5,id7,id8,id9"
+ )
+ assert (
+ await page.eval_on_selector_all(
+ "div:right-of(#id3, 50)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id2,id5,id7,id8"
+ )
+ assert (
+ await page.eval_on_selector_all(
+ "div:right-of(#id3, 49)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id7,id8"
+ )
+
+ assert await page.eval_on_selector("div:left-of(#id2)", "e => e.id") == "id1"
+ assert await page.query_selector("div:left-of(#id0)") is None
+ assert await page.eval_on_selector("div:left-of(#id5)", "e => e.id") == "id0"
+ assert await page.eval_on_selector("div:left-of(#id9)", "e => e.id") == "id8"
+ assert await page.eval_on_selector("div:left-of(#id4)", "e => e.id") == "id3"
+ assert (
+ await page.eval_on_selector_all("div:left-of(#id5)", "els => els.map(e => e.id).join(',')")
+ == "id0,id7,id3,id1,id6,id8"
+ )
+ assert (
+ await page.eval_on_selector_all(
+ "div:left-of(#id5, 3)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id7,id8"
+ )
+
+ assert await page.eval_on_selector("div:above(#id0)", "e => e.id") == "id3"
+ assert await page.eval_on_selector("div:above(#id5)", "e => e.id") == "id4"
+ assert await page.eval_on_selector("div:above(#id7)", "e => e.id") == "id5"
+ assert await page.eval_on_selector("div:above(#id8)", "e => e.id") == "id0"
+ assert await page.eval_on_selector("div:above(#id9)", "e => e.id") == "id8"
+ assert await page.query_selector("div:above(#id2)") is None
+ assert (
+ await page.eval_on_selector_all("div:above(#id5)", "els => els.map(e => e.id).join(',')")
+ == "id4,id2,id3,id1"
+ )
+ assert (
+ await page.eval_on_selector_all(
+ "div:above(#id5, 20)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id4,id3"
+ )
+
+ assert await page.eval_on_selector("div:below(#id4)", "e => e.id") == "id5"
+ assert await page.eval_on_selector("div:below(#id3)", "e => e.id") == "id0"
+ assert await page.eval_on_selector("div:below(#id2)", "e => e.id") == "id4"
+ assert await page.eval_on_selector("div:below(#id6)", "e => e.id") == "id8"
+ assert await page.eval_on_selector("div:below(#id7)", "e => e.id") == "id8"
+ assert await page.eval_on_selector("div:below(#id8)", "e => e.id") == "id9"
+ assert await page.query_selector("div:below(#id9)") is None
+ assert (
+ await page.eval_on_selector_all("div:below(#id3)", "els => els.map(e => e.id).join(',')")
+ == "id0,id5,id6,id7,id8,id9"
+ )
+ assert (
+ await page.eval_on_selector_all(
+ "div:below(#id3, 105)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id0,id5,id6,id7"
+ )
+
+ assert await page.eval_on_selector("div:near(#id0)", "e => e.id") == "id3"
+ assert (
+ await page.eval_on_selector_all("div:near(#id7)", "els => els.map(e => e.id).join(',')")
+ == "id0,id5,id3,id6"
+ )
+ assert (
+ await page.eval_on_selector_all("div:near(#id0)", "els => els.map(e => e.id).join(',')")
+ == "id3,id6,id7,id8,id1,id5"
+ )
+ assert (
+ await page.eval_on_selector_all("div:near(#id6)", "els => els.map(e => e.id).join(',')")
+ == "id0,id3,id7"
+ )
+ assert (
+ await page.eval_on_selector_all("div:near(#id6, 10)", "els => els.map(e => e.id).join(',')")
+ == "id0"
+ )
+ assert (
+ await page.eval_on_selector_all(
+ "div:near(#id0, 100)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id3,id6,id7,id8,id1,id5,id4,id2"
+ )
+
+ assert (
+ await page.eval_on_selector_all(
+ "div:below(#id5):above(#id8)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id7,id6"
+ )
+ assert await page.eval_on_selector("div:below(#id5):above(#id8)", "e => e.id") == "id7"
+
+ assert (
+ await page.eval_on_selector_all(
+ "div:right-of(#id0) + div:above(#id8)",
+ "els => els.map(e => e.id).join(',')",
+ )
+ == "id5,id6,id3"
+ )
+
+ with pytest.raises(Error) as exc_info:
+ await page.query_selector(":near(50)")
+ assert (
+ '"near" engine expects a selector list and optional maximum distance in pixels'
+ in exc_info.value.message
+ )
+ with pytest.raises(Error) as exc_info:
+ await page.query_selector('left-of="div"')
+ assert '"left-of" selector cannot be first' in exc_info.value.message
diff --git a/tests/async/test_request_continue.py b/tests/async/test_request_continue.py
new file mode 100644
index 0000000..17d21ae
--- /dev/null
+++ b/tests/async/test_request_continue.py
@@ -0,0 +1,186 @@
+# 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
+from typing import Optional
+
+from playwright.async_api import Page, Route
+from tests.server import Server, TestServerRequest
+
+
+async def test_request_continue_should_work(page: Page, server: Server) -> None:
+ await page.route("**/*", lambda route: asyncio.create_task(route.continue_()))
+ await page.goto(server.EMPTY_PAGE)
+
+
+async def test_request_continue_should_amend_http_headers(page: Page, server: Server) -> None:
+ await page.route(
+ "**/*",
+ lambda route: asyncio.create_task(
+ route.continue_(headers={**route.request.headers, "FOO": "bar"})
+ ),
+ )
+
+ await page.goto(server.EMPTY_PAGE)
+ [request, _] = await asyncio.gather(
+ server.wait_for_request("/sleep.zzz"),
+ page.evaluate('() => fetch("/sleep.zzz")'),
+ )
+ assert request.getHeader("foo") == "bar"
+
+
+async def test_request_continue_should_amend_method(page: Page, server: Server) -> None:
+ server_request = asyncio.create_task(server.wait_for_request("/sleep.zzz"))
+ await page.goto(server.EMPTY_PAGE)
+ await page.route("**/*", lambda route: asyncio.create_task(route.continue_(method="POST")))
+ [request, _] = await asyncio.gather(
+ server.wait_for_request("/sleep.zzz"),
+ page.evaluate('() => fetch("/sleep.zzz")'),
+ )
+ assert request.method.decode() == "POST"
+ assert (await server_request).method.decode() == "POST"
+
+
+async def test_request_continue_should_amend_method_on_main_request(
+ page: Page, server: Server
+) -> None:
+ request = asyncio.create_task(server.wait_for_request("/empty.html"))
+ await page.route("**/*", lambda route: asyncio.create_task(route.continue_(method="POST")))
+ await page.goto(server.EMPTY_PAGE)
+ assert (await request).method.decode() == "POST"
+
+
+async def test_request_continue_should_amend_post_data(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.route(
+ "**/*",
+ lambda route: asyncio.create_task(route.continue_(post_data=b"doggo")),
+ )
+
+ [server_request, _] = await asyncio.gather(
+ server.wait_for_request("/sleep.zzz"),
+ page.evaluate(
+ """
+ () => fetch('/sleep.zzz', { method: 'POST', body: 'birdy' })
+ """
+ ),
+ )
+ assert server_request.post_body
+ assert server_request.post_body.decode() == "doggo"
+
+
+async def test_should_override_request_url(page: Page, server: Server) -> None:
+ request = asyncio.create_task(server.wait_for_request("/empty.html"))
+ await page.route(
+ "**/foo",
+ lambda route: asyncio.create_task(route.continue_(url=server.EMPTY_PAGE)),
+ )
+
+ await page.goto(server.PREFIX + "/foo")
+ assert (await request).method == b"GET"
+
+
+async def test_should_raise_except(page: Page, server: Server) -> None:
+ exc_fut: "asyncio.Future[Optional[Exception]]" = asyncio.Future()
+
+ async def capture_exception(route: Route) -> None:
+ try:
+ await route.continue_(url="file:///tmp/does-not-exist")
+ exc_fut.set_result(None)
+ except Exception as e:
+ exc_fut.set_result(e)
+
+ await page.route("**/*", capture_exception)
+ asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ assert "New URL must have same protocol as overridden URL" in str(await exc_fut)
+
+
+async def test_should_amend_utf8_post_data(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.route(
+ "**/*",
+ lambda route: asyncio.create_task(route.continue_(post_data="пушкин")),
+ )
+
+ [server_request, result] = await asyncio.gather(
+ server.wait_for_request("/sleep.zzz"),
+ page.evaluate("fetch('/sleep.zzz', { method: 'POST', body: 'birdy' })"),
+ )
+ assert server_request.method == b"POST"
+ assert server_request.post_body
+ assert server_request.post_body.decode("utf8") == "пушкин"
+
+
+async def test_should_amend_binary_post_data(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.route(
+ "**/*",
+ lambda route: asyncio.create_task(route.continue_(post_data=b"\x00\x01\x02\x03\x04")),
+ )
+
+ [server_request, result] = await asyncio.gather(
+ server.wait_for_request("/sleep.zzz"),
+ page.evaluate("fetch('/sleep.zzz', { method: 'POST', body: 'birdy' })"),
+ )
+ assert server_request.method == b"POST"
+ assert server_request.post_body == b"\x00\x01\x02\x03\x04"
+
+
+async def test_continue_should_not_change_multipart_form_data_body(
+ page: Page, server: Server, browser_name: str
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ server.set_route(
+ "/upload",
+ lambda context: (
+ context.write(b"done"),
+ context.setHeader("Content-Type", "text/plain"),
+ context.finish(),
+ ),
+ )
+
+ async def send_form_data() -> TestServerRequest:
+ req_task = asyncio.create_task(server.wait_for_request("/upload"))
+ status = await page.evaluate(
+ """async () => {
+ const newFile = new File(['file content'], 'file.txt');
+ const formData = new FormData();
+ formData.append('file', newFile);
+ const response = await fetch('/upload', {
+ method: 'POST',
+ credentials: 'include',
+ body: formData,
+ });
+ return response.status;
+ }"""
+ )
+ req = await req_task
+ assert status == 200
+ return req
+
+ req_before = await send_form_data()
+ await page.route("**/*", lambda route: route.continue_())
+ req_after = await send_form_data()
+
+ file_content = (
+ 'Content-Disposition: form-data; name="file"; filename="file.txt"\r\n'
+ "Content-Type: application/octet-stream\r\n"
+ "\r\n"
+ "file content\r\n"
+ "------"
+ )
+ assert req_before.post_body
+ assert req_after.post_body
+ assert file_content in req_before.post_body.decode()
+ assert file_content in req_after.post_body.decode()
diff --git a/tests/async/test_request_fulfill.py b/tests/async/test_request_fulfill.py
new file mode 100644
index 0000000..4f6739b
--- /dev/null
+++ b/tests/async/test_request_fulfill.py
@@ -0,0 +1,71 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from playwright.async_api import Page, Route
+from tests.server import Server
+
+
+async def test_should_fetch_original_request_and_fulfill(page: Page, server: Server) -> None:
+ async def handle(route: Route) -> None:
+ response = await page.request.fetch(route.request)
+ await route.fulfill(response=response)
+
+ await page.route("**/*", handle)
+ response = await page.goto(server.PREFIX + "/title.html")
+ assert response
+ assert response.status == 200
+ assert await page.title() == "Woof-Woof"
+
+
+async def test_should_fulfill_json(page: Page, server: Server) -> None:
+ async def handle(route: Route) -> None:
+ await route.fulfill(status=201, headers={"foo": "bar"}, json={"bar": "baz"})
+
+ await page.route("**/*", handle)
+
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 201
+ assert response.headers["content-type"] == "application/json"
+ assert await response.json() == {"bar": "baz"}
+
+
+async def test_should_fulfill_json_overriding_existing_response(page: Page, server: Server) -> None:
+ server.set_route(
+ "/tags",
+ lambda request: (
+ request.setHeader("foo", "bar"),
+ request.write('{"tags": ["a", "b"]}'.encode()),
+ request.finish(),
+ ),
+ )
+
+ original = {}
+
+ async def handle(route: Route) -> None:
+ response = await route.fetch()
+ json = await response.json()
+ original["tags"] = json["tags"]
+ json["tags"] = ["c"]
+ await route.fulfill(response=response, json=json)
+
+ await page.route("**/*", handle)
+
+ response = await page.goto(server.PREFIX + "/tags")
+ assert response
+ assert response.status == 200
+ assert response.headers["content-type"] == "application/json"
+ assert response.headers["foo"] == "bar"
+ assert original["tags"] == ["a", "b"]
+ assert await response.json() == {"tags": ["c"]}
diff --git a/tests/async/test_request_intercept.py b/tests/async/test_request_intercept.py
new file mode 100644
index 0000000..910ded7
--- /dev/null
+++ b/tests/async/test_request_intercept.py
@@ -0,0 +1,171 @@
+# 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
+from pathlib import Path
+
+from twisted.web import http
+
+from playwright.async_api import Page, Route
+from tests.server import Server
+
+
+async def test_should_fulfill_intercepted_response(page: Page, server: Server) -> None:
+ async def handle(route: Route) -> None:
+ response = await page.request.fetch(route.request)
+ await route.fulfill(
+ response=response,
+ status=201,
+ headers={"foo": "bar"},
+ content_type="text/plain",
+ body="Yo, page!",
+ )
+
+ await page.route("**/*", handle)
+ response = await page.goto(server.PREFIX + "/empty.html")
+ assert response
+ assert response.status == 201
+ assert response.headers["foo"] == "bar"
+ assert response.headers["content-type"] == "text/plain"
+ assert await page.evaluate("() => document.body.textContent") == "Yo, page!"
+
+
+async def test_should_fulfill_response_with_empty_body(page: Page, server: Server) -> None:
+ async def handle(route: Route) -> None:
+ response = await page.request.fetch(route.request)
+ await route.fulfill(response=response, status=201, body="", headers={"content-length": "0"})
+
+ await page.route("**/*", handle)
+ response = await page.goto(server.PREFIX + "/title.html")
+ assert response
+ assert response.status == 201
+ assert await response.text() == ""
+
+
+async def test_should_override_with_defaults_when_intercepted_response_not_provided(
+ page: Page, server: Server, browser_name: str
+) -> None:
+ def server_handler(request: http.Request) -> None:
+ request.setHeader("foo", "bar")
+ request.write("my content".encode())
+ request.finish()
+
+ server.set_route("/empty.html", server_handler)
+
+ async def handle(route: Route) -> None:
+ await page.request.fetch(route.request)
+ await route.fulfill(status=201)
+
+ await page.route("**/*", handle)
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 201
+ assert await response.text() == ""
+ if browser_name == "webkit":
+ assert response.headers == {"content-type": "text/plain"}
+ else:
+ assert response.headers == {}
+
+
+async def test_should_fulfill_with_any_response(page: Page, server: Server) -> None:
+ def server_handler(request: http.Request) -> None:
+ request.setHeader("foo", "bar")
+ request.write("Woo-hoo".encode())
+ request.finish()
+
+ server.set_route("/sample", server_handler)
+ sample_response = await page.request.get(server.PREFIX + "/sample")
+ await page.route(
+ "**/*",
+ lambda route: route.fulfill(
+ response=sample_response, status=201, content_type="text/plain"
+ ),
+ )
+ response = await page.goto(server.EMPTY_PAGE)
+ assert response
+ assert response.status == 201
+ assert await response.text() == "Woo-hoo"
+ assert response.headers["foo"] == "bar"
+
+
+async def test_should_support_fulfill_after_intercept(
+ page: Page, server: Server, assetdir: Path
+) -> None:
+ request_future = asyncio.create_task(server.wait_for_request("/title.html"))
+
+ async def handle_route(route: Route) -> None:
+ response = await page.request.fetch(route.request)
+ await route.fulfill(response=response)
+
+ await page.route("**", handle_route)
+ response = await page.goto(server.PREFIX + "/title.html")
+ assert response
+ request = await request_future
+ assert request.uri.decode() == "/title.html"
+ original = (assetdir / "title.html").read_text()
+ assert await response.text() == original
+
+
+async def test_should_give_access_to_the_intercepted_response(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+
+ route_task: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route("**/title.html", lambda route: route_task.set_result(route))
+
+ eval_task = asyncio.create_task(
+ page.evaluate("url => fetch(url)", server.PREFIX + "/title.html")
+ )
+
+ route = await route_task
+ response = await page.request.fetch(route.request)
+
+ assert response.status == 200
+ assert response.status_text == "OK"
+ assert response.ok is True
+ assert response.url.endswith("/title.html") is True
+ assert response.headers["content-type"] == "text/html; charset=utf-8"
+ assert list(
+ filter(
+ lambda header: header["name"].lower() == "content-type",
+ response.headers_array,
+ )
+ ) == [{"name": "Content-Type", "value": "text/html; charset=utf-8"}]
+
+ await asyncio.gather(
+ route.fulfill(response=response),
+ eval_task,
+ )
+
+
+async def test_should_give_access_to_the_intercepted_response_body(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+
+ route_task: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route("**/simple.json", lambda route: route_task.set_result(route))
+
+ eval_task = asyncio.create_task(
+ page.evaluate("url => fetch(url)", server.PREFIX + "/simple.json")
+ )
+
+ route = await route_task
+ response = await page.request.fetch(route.request)
+
+ assert await response.text() == '{"foo": "bar"}\n'
+
+ await asyncio.gather(
+ route.fulfill(response=response),
+ eval_task,
+ )
diff --git a/tests/async/test_resource_timing.py b/tests/async/test_resource_timing.py
new file mode 100644
index 0000000..2a14414
--- /dev/null
+++ b/tests/async/test_resource_timing.py
@@ -0,0 +1,106 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Dict
+
+import pytest
+from flaky import flaky
+
+from playwright.async_api import Browser, Page
+from tests.server import Server
+
+
+async def test_should_work(page: Page, server: Server) -> None:
+ async with page.expect_event("requestfinished") as request_info:
+ await page.goto(server.EMPTY_PAGE)
+ request = await request_info.value
+ timing = request.timing
+ verify_connections_timing_consistency(timing)
+ assert timing["requestStart"] >= timing["connectEnd"]
+ assert timing["responseStart"] >= timing["requestStart"]
+ assert timing["responseEnd"] >= timing["responseStart"]
+ assert timing["responseEnd"] < 10000
+
+
+@flaky
+async def test_should_work_for_subresource(
+ page: Page, server: Server, is_win: bool, is_mac: bool, is_webkit: bool
+) -> None:
+ if is_webkit and (is_mac or is_win):
+ pytest.skip()
+ requests = []
+ page.on("requestfinished", lambda request: requests.append(request))
+ await page.goto(server.PREFIX + "/one-style.html")
+ assert len(requests) >= 2
+ timing = requests[1].timing
+ verify_connections_timing_consistency(timing)
+ assert timing["requestStart"] >= 0
+ assert timing["responseStart"] > timing["requestStart"]
+ assert timing["responseEnd"] >= timing["responseStart"]
+ assert timing["responseEnd"] < 10000
+
+
+@flaky # Upstream flaky
+async def test_should_work_for_ssl(browser: Browser, https_server: Server) -> None:
+ page = await browser.new_page(ignore_https_errors=True)
+ async with page.expect_event("requestfinished") as request_info:
+ await page.goto(https_server.EMPTY_PAGE)
+ request = await request_info.value
+ timing = request.timing
+ verify_connections_timing_consistency(timing)
+ assert timing["requestStart"] >= timing["connectEnd"]
+ assert timing["responseStart"] >= timing["requestStart"]
+ assert timing["responseEnd"] >= timing["responseStart"]
+ assert timing["responseEnd"] < 10000
+ await page.close()
+
+
+@pytest.mark.skip_browser("webkit") # In WebKit, redirects don"t carry the timing info
+async def test_should_work_for_redirect(page: Page, server: Server) -> None:
+ server.set_redirect("/foo.html", "/empty.html")
+ responses = []
+ page.on("response", lambda response: responses.append(response))
+ await page.goto(server.PREFIX + "/foo.html")
+ for r in responses:
+ await r.finished()
+
+ assert len(responses) == 2
+ assert responses[0].url == server.PREFIX + "/foo.html"
+ assert responses[1].url == server.PREFIX + "/empty.html"
+
+ timing1 = responses[0].request.timing
+ verify_connections_timing_consistency(timing1)
+ assert timing1["requestStart"] >= timing1["connectEnd"]
+ assert timing1["responseStart"] > timing1["requestStart"]
+ assert timing1["responseEnd"] >= timing1["responseStart"]
+ assert timing1["responseEnd"] < 10000
+
+ timing2 = responses[1].request.timing
+ verify_connections_timing_consistency(timing2)
+ assert timing2["requestStart"] >= 0
+ assert timing2["responseStart"] > timing2["requestStart"]
+ assert timing2["responseEnd"] >= timing2["responseStart"]
+ assert timing2["responseEnd"] < 10000
+
+
+def verify_timing_value(value: float, previous: float) -> None:
+ assert value == -1 or value > 0 and value >= previous
+
+
+def verify_connections_timing_consistency(timing: Dict) -> None:
+ verify_timing_value(timing["domainLookupStart"], -1)
+ verify_timing_value(timing["domainLookupEnd"], timing["domainLookupStart"])
+ verify_timing_value(timing["connectStart"], timing["domainLookupEnd"])
+ verify_timing_value(timing["secureConnectionStart"], timing["connectStart"])
+ verify_timing_value(timing["connectEnd"], timing["secureConnectionStart"])
diff --git a/tests/async/test_screenshot.py b/tests/async/test_screenshot.py
new file mode 100644
index 0000000..b7c679a
--- /dev/null
+++ b/tests/async/test_screenshot.py
@@ -0,0 +1,44 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Callable
+
+from playwright.async_api import Page
+
+from tests.server import Server
+from tests.utils import must
+
+
+async def test_should_screenshot_with_mask(
+ page: Page, server: Server, assert_to_be_golden: Callable[[bytes, str], None]
+) -> None:
+ await page.set_viewport_size(
+ {
+ "width": 500,
+ "height": 500,
+ }
+ )
+ await page.goto(server.PREFIX + "/grid.html")
+ assert_to_be_golden(
+ await page.screenshot(mask=[page.locator("div").nth(5)]),
+ "mask-should-work-with-page.png",
+ )
+ assert_to_be_golden(
+ await page.locator("body").screenshot(mask=[page.locator("div").nth(5)]),
+ "mask-should-work-with-locator.png",
+ )
+ assert_to_be_golden(
+ await must(await page.query_selector("body")).screenshot(mask=[page.locator("div").nth(5)]),
+ "mask-should-work-with-element-handle.png",
+ )
diff --git a/tests/async/test_selector_generator.py b/tests/async/test_selector_generator.py
new file mode 100644
index 0000000..10b0729
--- /dev/null
+++ b/tests/async/test_selector_generator.py
@@ -0,0 +1,48 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from playwright.async_api import Error, Page, Playwright
+
+
+async def test_should_use_data_test_id_in_strict_errors(page: Page, playwright: Playwright) -> None:
+ playwright.selectors.set_test_id_attribute("data-custom-id")
+ try:
+ await page.set_content(
+ """
+
+
+ """
+ )
+ with pytest.raises(Error) as exc_info:
+ await page.locator(".foo").hover(timeout=200)
+ assert "strict mode violation" in exc_info.value.message
+ assert ' None:
+ await page.set_content(
+ """
+
Hello my
+wo"rld """
+ )
+ await page.eval_on_selector(
+ "input",
+ """input => {
+ input.setAttribute('placeholder', 'hello my\\nwo"rld');
+ input.setAttribute('title', 'hello my\\nwo"rld');
+ input.setAttribute('alt', 'hello my\\nwo"rld');
+ }""",
+ )
+ await expect(page.get_by_text('hello my\nwo"rld')).to_have_attribute("id", "label")
+ await expect(page.get_by_text('hello my wo"rld')).to_have_attribute("id", "label")
+ await expect(page.get_by_label('hello my\nwo"rld')).to_have_attribute("id", "control")
+ await expect(page.get_by_placeholder('hello my\nwo"rld')).to_have_attribute("id", "control")
+ await expect(page.get_by_alt_text('hello my\nwo"rld')).to_have_attribute("id", "control")
+ await expect(page.get_by_title('hello my\nwo"rld')).to_have_attribute("id", "control")
+
+ await page.set_content(
+ """
+
Hello my
+world """
+ )
+ await page.eval_on_selector(
+ "input",
+ """input => {
+ input.setAttribute('placeholder', 'hello my\\nworld');
+ input.setAttribute('title', 'hello my\\nworld');
+ input.setAttribute('alt', 'hello my\\nworld');
+ }""",
+ )
+ await expect(page.get_by_text("hello my\nworld")).to_have_attribute("id", "label")
+ await expect(page.get_by_text("hello my world")).to_have_attribute("id", "label")
+ await expect(page.get_by_label("hello my\nworld")).to_have_attribute("id", "control")
+ await expect(page.get_by_placeholder("hello my\nworld")).to_have_attribute("id", "control")
+ await expect(page.get_by_alt_text("hello my\nworld")).to_have_attribute("id", "control")
+ await expect(page.get_by_title("hello my\nworld")).to_have_attribute("id", "control")
+
+ await page.set_content("""
Text here
""")
+ await expect(page.get_by_title("my title", exact=True)).to_have_count(1, timeout=500)
+ await expect(page.get_by_title("my t\\itle", exact=True)).to_have_count(0, timeout=500)
+ await expect(page.get_by_title("my t\\\\itle", exact=True)).to_have_count(0, timeout=500)
+
+ await page.set_content("""
foo >> bar """)
+ await page.eval_on_selector(
+ "input",
+ """input => {
+ input.setAttribute('placeholder', 'foo >> bar');
+ input.setAttribute('title', 'foo >> bar');
+ input.setAttribute('alt', 'foo >> bar');
+ }""",
+ )
+ assert await page.get_by_text("foo >> bar").text_content() == "foo >> bar"
+ await expect(page.locator("label")).to_have_text("foo >> bar")
+ await expect(page.get_by_text("foo >> bar")).to_have_text("foo >> bar")
+ assert await page.get_by_text(re.compile("foo >> bar")).text_content() == "foo >> bar"
+ await expect(page.get_by_label("foo >> bar")).to_have_attribute("id", "target")
+ await expect(page.get_by_label(re.compile("foo >> bar"))).to_have_attribute("id", "target")
+ await expect(page.get_by_placeholder("foo >> bar")).to_have_attribute("id", "target")
+ await expect(page.get_by_alt_text("foo >> bar")).to_have_attribute("id", "target")
+ await expect(page.get_by_title("foo >> bar")).to_have_attribute("id", "target")
+ await expect(page.get_by_placeholder(re.compile("foo >> bar"))).to_have_attribute(
+ "id", "target"
+ )
+ await expect(page.get_by_alt_text(re.compile("foo >> bar"))).to_have_attribute("id", "target")
+ await expect(page.get_by_title(re.compile("foo >> bar"))).to_have_attribute("id", "target")
+
+
+async def test_get_by_role_escaping(
+ page: Page,
+) -> None:
+ await page.set_content(
+ """
+
issues 123
+
he llo 56
+
Click me
+ """
+ )
+ assert await page.get_by_role("button").evaluate_all("els => els.map(e => e.outerHTML)") == [
+ "
Click me ",
+ ]
+ assert await page.get_by_role("link").evaluate_all("els => els.map(e => e.outerHTML)") == [
+ """
issues 123 """,
+ """
he llo 56 """,
+ ]
+
+ assert await page.get_by_role("link", name="issues 123").evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ ) == [
+ """
issues 123 """,
+ ]
+ assert await page.get_by_role("link", name="sues").evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ ) == [
+ """
issues 123 """,
+ ]
+ assert await page.get_by_role("link", name=" he \n llo ").evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ ) == [
+ """
he llo 56 """,
+ ]
+ assert (
+ await page.get_by_role("button", name="issues").evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ )
+ == []
+ )
+
+ assert (
+ await page.get_by_role("link", name="sues", exact=True).evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ )
+ == []
+ )
+ assert await page.get_by_role("link", name=" he \n llo 56 ", exact=True).evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ ) == [
+ """
he llo 56 """,
+ ]
+
+ assert await page.get_by_role("button", name="Click me", exact=True).evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ ) == [
+ "
Click me ",
+ ]
+ assert (
+ await page.get_by_role("button", name="Click \\me", exact=True).evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ )
+ == []
+ )
+ assert (
+ await page.get_by_role("button", name="Click \\\\me", exact=True).evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ )
+ == []
+ )
+
+
+async def test_include_hidden_should_work(
+ page: Page,
+) -> None:
+ await page.set_content("""
Hidden """)
+ assert (
+ await page.get_by_role("button", name="Hidden").evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ )
+ == []
+ )
+ assert await page.get_by_role("button", name="Hidden", include_hidden=True).evaluate_all(
+ "els => els.map(e => e.outerHTML)"
+ ) == [
+ """
Hidden """,
+ ]
diff --git a/tests/async/test_selectors_misc.py b/tests/async/test_selectors_misc.py
new file mode 100644
index 0000000..5527d6e
--- /dev/null
+++ b/tests/async/test_selectors_misc.py
@@ -0,0 +1,54 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from playwright.async_api import Page
+
+
+async def test_should_work_with_internal_and(page: Page) -> None:
+ await page.set_content(
+ """
+
hello
world
+
hello2 world2
+ """
+ )
+ assert (
+ await page.eval_on_selector_all(
+ 'div >> internal:and="span"', "els => els.map(e => e.textContent)"
+ )
+ ) == []
+ assert (
+ await page.eval_on_selector_all(
+ 'div >> internal:and=".foo"', "els => els.map(e => e.textContent)"
+ )
+ ) == ["hello"]
+ assert (
+ await page.eval_on_selector_all(
+ 'div >> internal:and=".bar"', "els => els.map(e => e.textContent)"
+ )
+ ) == ["world"]
+ assert (
+ await page.eval_on_selector_all(
+ 'span >> internal:and="span"', "els => els.map(e => e.textContent)"
+ )
+ ) == ["hello2", "world2"]
+ assert (
+ await page.eval_on_selector_all(
+ '.foo >> internal:and="div"', "els => els.map(e => e.textContent)"
+ )
+ ) == ["hello"]
+ assert (
+ await page.eval_on_selector_all(
+ '.bar >> internal:and="span"', "els => els.map(e => e.textContent)"
+ )
+ ) == ["world2"]
diff --git a/tests/async/test_selectors_text.py b/tests/async/test_selectors_text.py
new file mode 100644
index 0000000..5602928
--- /dev/null
+++ b/tests/async/test_selectors_text.py
@@ -0,0 +1,210 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import re
+
+import pytest
+
+from playwright.async_api import Error, Page, expect
+
+
+async def test_has_text_and_internal_text_should_match_full_node_text_in_strict_mode(
+ page: Page,
+) -> None:
+ await page.set_content(
+ """
+
helloworld
+
hello
+ """
+ )
+ await expect(page.get_by_text("helloworld", exact=True)).to_have_id("div1")
+ await expect(page.get_by_text("hello", exact=True)).to_have_id("div2")
+ await expect(page.locator("div", has_text=re.compile("^helloworld$"))).to_have_id("div1")
+ await expect(page.locator("div", has_text=re.compile("^hello$"))).to_have_id("div2")
+
+ await page.set_content(
+ """
+
hello world
+
hello
+ """
+ )
+ await expect(page.get_by_text("helloworld", exact=True)).to_have_id("div1")
+ assert await page.get_by_text("hello", exact=True).evaluate_all(
+ "els => els.map(e => e.id)"
+ ) == ["span1", "span2"]
+ await expect(page.locator("div", has_text=re.compile("^helloworld$"))).to_have_id("div1")
+ await expect(page.locator("div", has_text=re.compile("^hello$"))).to_have_id("div2")
+
+
+async def test_should_work(page: Page) -> None:
+ await page.set_content(
+ """
+
yo
ya
\nye
+ """
+ )
+ assert await page.eval_on_selector("text=ya", "e => e.outerHTML") == "
ya
"
+ assert await page.eval_on_selector('text="ya"', "e => e.outerHTML") == "
ya
"
+ assert await page.eval_on_selector("text=/^[ay]+$/", "e => e.outerHTML") == "
ya
"
+ assert await page.eval_on_selector("text=/Ya/i", "e => e.outerHTML") == "
ya
"
+ assert await page.eval_on_selector("text=ye", "e => e.outerHTML") == "
\nye
"
+ assert ">\nye
" in await page.get_by_text("ye").evaluate("e => e.outerHTML")
+
+ await page.set_content(
+ """
+ ye
ye
+ """
+ )
+ assert await page.eval_on_selector('text="ye"', "e => e.outerHTML") == " ye
"
+ assert "> ye " in await page.get_by_text("ye", exact=True).first.evaluate(
+ "e => e.outerHTML"
+ )
+
+ await page.set_content(
+ """
+ yo
"ya
hello world!
+ """
+ )
+ assert await page.eval_on_selector('text="\\"ya"', "e => e.outerHTML") == '"ya
'
+ assert (
+ await page.eval_on_selector("text=/hello/", "e => e.outerHTML")
+ == " hello world!
"
+ )
+ assert (
+ await page.eval_on_selector("text=/^\\s*heLLo/i", "e => e.outerHTML")
+ == " hello world!
"
+ )
+
+ await page.set_content(
+ """
+
+ """
+ )
+ assert await page.eval_on_selector("text=hey", "e => e.outerHTML") == "hey
"
+ assert await page.eval_on_selector('text=yo>>text="ya"', "e => e.outerHTML") == "ya
"
+ assert await page.eval_on_selector('text=yo>> text="ya"', "e => e.outerHTML") == "ya
"
+ assert await page.eval_on_selector("text=yo >>text='ya'", "e => e.outerHTML") == "ya
"
+ assert (
+ await page.eval_on_selector("text=yo >> text='ya'", "e => e.outerHTML") == "ya
"
+ )
+ assert await page.eval_on_selector("'yo'>>\"ya\"", "e => e.outerHTML") == "ya
"
+ assert await page.eval_on_selector("\"yo\" >> 'ya'", "e => e.outerHTML") == "ya
"
+
+ await page.set_content(
+ """
+ yo
yo
+ """
+ )
+ assert (
+ await page.eval_on_selector_all("text=yo", "es => es.map(e => e.outerHTML).join('\\n')")
+ == 'yo
\nyo
'
+ )
+
+ await page.set_content("'
\"
\\
x
")
+ assert await page.eval_on_selector("text='\\''", "e => e.outerHTML") == "'
"
+ assert await page.eval_on_selector("text='\"'", "e => e.outerHTML") == '"
'
+ assert await page.eval_on_selector('text="\\""', "e => e.outerHTML") == '"
'
+ assert await page.eval_on_selector('text="\'"', "e => e.outerHTML") == "'
"
+ assert await page.eval_on_selector('text="\\x"', "e => e.outerHTML") == "x
"
+ assert await page.eval_on_selector("text='\\x'", "e => e.outerHTML") == "x
"
+ assert await page.eval_on_selector("text='\\\\'", "e => e.outerHTML") == "\\
"
+ assert await page.eval_on_selector('text="\\\\"', "e => e.outerHTML") == "\\
"
+ assert await page.eval_on_selector('text="', "e => e.outerHTML") == '"
'
+ assert await page.eval_on_selector("text='", "e => e.outerHTML") == "'
"
+ assert await page.eval_on_selector('"x"', "e => e.outerHTML") == "x
"
+ assert await page.eval_on_selector("'x'", "e => e.outerHTML") == "x
"
+ with pytest.raises(Error):
+ await page.query_selector_all('"')
+ with pytest.raises(Error):
+ await page.query_selector_all("'")
+
+ await page.set_content(" '
\"
")
+ assert await page.eval_on_selector('text="', "e => e.outerHTML") == ' "
'
+ assert await page.eval_on_selector("text='", "e => e.outerHTML") == " '
"
+
+ await page.set_content("Hi''>>foo=bar
")
+ assert (
+ await page.eval_on_selector("text=\"Hi''>>foo=bar\"", "e => e.outerHTML")
+ == "Hi''>>foo=bar
"
+ )
+ await page.set_content("Hi'\">>foo=bar
")
+ assert (
+ await page.eval_on_selector('text="Hi\'\\">>foo=bar"', "e => e.outerHTML")
+ == "Hi'\">>foo=bar
"
+ )
+
+ await page.set_content("Hi>>
")
+ assert await page.eval_on_selector('text="Hi>>">>span', "e => e.outerHTML") == " "
+ assert (
+ await page.eval_on_selector("text=/Hi\\>\\>/ >> span", "e => e.outerHTML")
+ == " "
+ )
+
+ await page.set_content("a b
a
")
+ assert await page.eval_on_selector("text=a", "e => e.outerHTML") == "a b
"
+ assert await page.eval_on_selector("text=b", "e => e.outerHTML") == "a b
"
+ assert await page.eval_on_selector("text=ab", "e => e.outerHTML") == "a b
"
+ assert await page.query_selector("text=abc") is None
+ assert await page.eval_on_selector_all("text=a", "els => els.length") == 2
+ assert await page.eval_on_selector_all("text=b", "els => els.length") == 1
+ assert await page.eval_on_selector_all("text=ab", "els => els.length") == 1
+ assert await page.eval_on_selector_all("text=abc", "els => els.length") == 0
+
+ await page.set_content("
")
+ await page.eval_on_selector(
+ "div",
+ """div => {
+ div.appendChild(document.createTextNode('hello'))
+ div.appendChild(document.createTextNode('world'))
+ }""",
+ )
+ await page.eval_on_selector(
+ "span",
+ """span => {
+ span.appendChild(document.createTextNode('hello'))
+ span.appendChild(document.createTextNode('world'))
+ }""",
+ )
+ assert await page.eval_on_selector("text=lowo", "e => e.outerHTML") == "helloworld
"
+ assert (
+ await page.eval_on_selector_all("text=lowo", "els => els.map(e => e.outerHTML).join('')")
+ == "helloworld
helloworld "
+ )
+
+ await page.set_content("Sign in Hello\n \nworld ")
+ assert (
+ await page.eval_on_selector("text=Sign in", "e => e.outerHTML")
+ == "Sign in "
+ )
+ assert len((await page.query_selector_all("text=Sign \tin"))) == 1
+ assert len((await page.query_selector_all('text="Sign in"'))) == 1
+ assert (
+ await page.eval_on_selector("text=lo wo", "e => e.outerHTML")
+ == "Hello\n \nworld "
+ )
+ assert (
+ await page.eval_on_selector('text="Hello world"', "e => e.outerHTML")
+ == "Hello\n \nworld "
+ )
+ assert await page.query_selector('text="lo wo"') is None
+ assert len((await page.query_selector_all("text=lo \nwo"))) == 1
+ assert len(await page.query_selector_all('text="lo \nwo"')) == 0
+
+ await page.set_content("let'shello
")
+ assert (
+ await page.eval_on_selector("text=/let's/i >> span", "e => e.outerHTML")
+ == "hello "
+ )
+ assert (
+ await page.eval_on_selector("text=/let\\'s/i >> span", "e => e.outerHTML")
+ == "hello "
+ )
diff --git a/tests/async/test_tap.py b/tests/async/test_tap.py
new file mode 100644
index 0000000..abb3c61
--- /dev/null
+++ b/tests/async/test_tap.py
@@ -0,0 +1,237 @@
+# 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
+from typing import AsyncGenerator, Optional, cast
+
+import pytest
+
+from playwright.async_api import Browser, BrowserContext, ElementHandle, JSHandle, Page
+
+
+@pytest.fixture
+async def context(browser: Browser) -> AsyncGenerator[BrowserContext, None]:
+ context = await browser.new_context(has_touch=True)
+ yield context
+ await context.close()
+
+
+async def test_should_send_all_of_the_correct_events(page: Page) -> None:
+ await page.set_content(
+ """
+ a
+ b
+ """
+ )
+ await page.tap("#a")
+ element_handle = await track_events(await page.query_selector("#b"))
+ await page.tap("#b")
+ assert await element_handle.json_value() == [
+ "pointerover",
+ "pointerenter",
+ "pointerdown",
+ "touchstart",
+ "pointerup",
+ "pointerout",
+ "pointerleave",
+ "touchend",
+ "mouseover",
+ "mouseenter",
+ "mousemove",
+ "mousedown",
+ "mouseup",
+ "click",
+ ]
+
+
+async def test_should_not_send_mouse_events_touchstart_is_canceled(page: Page) -> None:
+ await page.set_content("hello world")
+ await page.evaluate(
+ """() => {
+ // touchstart is not cancelable unless passive is false
+ document.addEventListener('touchstart', t => t.preventDefault(), {passive: false});
+ }"""
+ )
+ events_handle = await track_events(await page.query_selector("body"))
+ await page.tap("body")
+ assert await events_handle.json_value() == [
+ "pointerover",
+ "pointerenter",
+ "pointerdown",
+ "touchstart",
+ "pointerup",
+ "pointerout",
+ "pointerleave",
+ "touchend",
+ ]
+
+
+async def test_should_not_send_mouse_events_touchend_is_canceled(page: Page) -> None:
+ await page.set_content("hello world")
+ await page.evaluate(
+ """() => {
+ // touchstart is not cancelable unless passive is false
+ document.addEventListener('touchend', t => t.preventDefault());
+ }"""
+ )
+ events_handle = await track_events(await page.query_selector("body"))
+ await page.tap("body")
+ assert await events_handle.json_value() == [
+ "pointerover",
+ "pointerenter",
+ "pointerdown",
+ "touchstart",
+ "pointerup",
+ "pointerout",
+ "pointerleave",
+ "touchend",
+ ]
+
+
+async def test_should_work_with_modifiers(page: Page) -> None:
+ await page.set_content("hello world")
+ alt_key_promise = asyncio.create_task(
+ page.evaluate(
+ """() => new Promise(resolve => {
+ document.addEventListener('touchstart', event => {
+ resolve(event.altKey);
+ }, {passive: false});
+ })"""
+ )
+ )
+ await asyncio.sleep(0) # make sure the evals hit the page
+ await page.evaluate("""() => void 0""")
+ await page.tap("body", modifiers=["Alt"])
+ assert await alt_key_promise is True
+
+
+async def test_should_send_well_formed_touch_points(page: Page) -> None:
+ promises = asyncio.gather(
+ page.evaluate(
+ """() => new Promise(resolve => {
+ document.addEventListener('touchstart', event => {
+ resolve([...event.touches].map(t => ({
+ identifier: t.identifier,
+ clientX: t.clientX,
+ clientY: t.clientY,
+ pageX: t.pageX,
+ pageY: t.pageY,
+ radiusX: 'radiusX' in t ? t.radiusX : t['webkitRadiusX'],
+ radiusY: 'radiusY' in t ? t.radiusY : t['webkitRadiusY'],
+ rotationAngle: 'rotationAngle' in t ? t.rotationAngle : t['webkitRotationAngle'],
+ force: 'force' in t ? t.force : t['webkitForce'],
+ })));
+ }, false);
+ })"""
+ ),
+ page.evaluate(
+ """() => new Promise(resolve => {
+ document.addEventListener('touchend', event => {
+ resolve([...event.touches].map(t => ({
+ identifier: t.identifier,
+ clientX: t.clientX,
+ clientY: t.clientY,
+ pageX: t.pageX,
+ pageY: t.pageY,
+ radiusX: 'radiusX' in t ? t.radiusX : t['webkitRadiusX'],
+ radiusY: 'radiusY' in t ? t.radiusY : t['webkitRadiusY'],
+ rotationAngle: 'rotationAngle' in t ? t.rotationAngle : t['webkitRotationAngle'],
+ force: 'force' in t ? t.force : t['webkitForce'],
+ })));
+ }, false);
+ })"""
+ ),
+ )
+ # make sure the evals hit the page
+ await page.evaluate("""() => void 0""")
+ await page.touchscreen.tap(40, 60)
+ [touchstart, touchend] = await promises
+ assert touchstart == [
+ {
+ "clientX": 40,
+ "clientY": 60,
+ "force": 1,
+ "identifier": 0,
+ "pageX": 40,
+ "pageY": 60,
+ "radiusX": 1,
+ "radiusY": 1,
+ "rotationAngle": 0,
+ }
+ ]
+ assert touchend == []
+
+
+async def test_should_wait_until_an_element_is_visible_to_tap_it(page: Page) -> None:
+ div = cast(
+ ElementHandle,
+ await page.evaluate_handle(
+ """() => {
+ const button = document.createElement('button');
+ button.textContent = 'not clicked';
+ document.body.appendChild(button);
+ button.style.display = 'none';
+ return button;
+ }"""
+ ),
+ )
+ tap_promise = asyncio.create_task(div.tap())
+ await asyncio.sleep(0) # issue tap
+ await div.evaluate("""div => div.onclick = () => div.textContent = 'clicked'""")
+ await div.evaluate("""div => div.style.display = 'block'""")
+ await tap_promise
+ assert await div.text_content() == "clicked"
+
+
+async def test_locators_tap(page: Page) -> None:
+ await page.set_content(
+ """
+ a
+ b
+ """
+ )
+ await page.locator("#a").tap()
+ element_handle = await track_events(await page.query_selector("#b"))
+ await page.locator("#b").tap()
+ assert await element_handle.json_value() == [
+ "pointerover",
+ "pointerenter",
+ "pointerdown",
+ "touchstart",
+ "pointerup",
+ "pointerout",
+ "pointerleave",
+ "touchend",
+ "mouseover",
+ "mouseenter",
+ "mousemove",
+ "mousedown",
+ "mouseup",
+ "click",
+ ]
+
+
+async def track_events(target: Optional[ElementHandle]) -> JSHandle:
+ assert target
+ return await target.evaluate_handle(
+ """target => {
+ const events = [];
+ for (const event of [
+ 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'click',
+ 'pointercancel', 'pointerdown', 'pointerenter', 'pointerleave', 'pointermove', 'pointerout', 'pointerover', 'pointerup',
+ 'touchstart', 'touchend', 'touchmove', 'touchcancel',])
+ target.addEventListener(event, () => events.push(event), false);
+ return events;
+ }"""
+ )
diff --git a/tests/async/test_tracing.py b/tests/async/test_tracing.py
new file mode 100644
index 0000000..a9cfdfb
--- /dev/null
+++ b/tests/async/test_tracing.py
@@ -0,0 +1,285 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+from pathlib import Path
+from typing import Dict, List
+
+from playwright.async_api import Browser, BrowserContext, BrowserType, Page
+from tests.server import Server
+from tests.utils import get_trace_actions, parse_trace
+
+
+async def test_browser_context_output_trace(
+ browser: Browser, server: Server, tmp_path: Path
+) -> None:
+ context = await browser.new_context()
+ await context.tracing.start(screenshots=True, snapshots=True)
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/grid.html")
+ await context.tracing.stop(path=tmp_path / "trace.zip")
+ assert Path(tmp_path / "trace.zip").exists()
+
+
+async def test_start_stop(browser: Browser) -> None:
+ context = await browser.new_context()
+ await context.tracing.start()
+ await context.tracing.stop()
+ await context.close()
+
+
+async def test_browser_context_should_not_throw_when_stopping_without_start_but_not_exporting(
+ context: BrowserContext, server: Server, tmp_path: Path
+) -> None:
+ await context.tracing.stop()
+
+
+async def test_browser_context_output_trace_chunk(
+ browser: Browser, server: Server, tmp_path: Path
+) -> None:
+ context = await browser.new_context()
+ await context.tracing.start(screenshots=True, snapshots=True)
+ page = await context.new_page()
+ await page.goto(server.PREFIX + "/grid.html")
+ button = page.locator(".box").first
+
+ await context.tracing.start_chunk(title="foo")
+ await button.click()
+ await context.tracing.stop_chunk(path=tmp_path / "trace1.zip")
+ assert Path(tmp_path / "trace1.zip").exists()
+
+ await context.tracing.start_chunk(title="foo")
+ await button.click()
+ await context.tracing.stop_chunk(path=tmp_path / "trace2.zip")
+ assert Path(tmp_path / "trace2.zip").exists()
+
+
+async def test_should_collect_sources(
+ context: BrowserContext, page: Page, server: Server, tmp_path: Path
+) -> None:
+ await context.tracing.start(sources=True)
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content("Click ")
+ await page.click("button")
+ path = tmp_path / "trace.zip"
+ await context.tracing.stop(path=path)
+
+ (resources, events) = parse_trace(path)
+ current_file_content = Path(__file__).read_bytes()
+ found_current_file = False
+ for name, resource in resources.items():
+ if resource == current_file_content:
+ found_current_file = True
+ break
+ assert found_current_file
+
+
+async def test_should_collect_trace_with_resources_but_no_js(
+ context: BrowserContext, page: Page, server: Server, tmpdir: Path
+) -> None:
+ await context.tracing.start(screenshots=True, snapshots=True)
+ await page.goto(server.PREFIX + "/frames/frame.html")
+ await page.set_content("Click ")
+ await page.click('"Click"')
+ await page.mouse.move(20, 20)
+ await page.mouse.dblclick(30, 30)
+ await page.keyboard.insert_text("abc")
+ await page.wait_for_timeout(2000) # Give it some time to produce screenshots.
+ await page.route(
+ "**/empty.html", lambda route: route.continue_()
+ ) # should produce a route.continue_ entry.
+ await page.goto(server.EMPTY_PAGE)
+ await page.goto(
+ server.PREFIX + "/one-style.html"
+ ) # should not produce a route.continue_ entry since we continue all routes if no match.
+ await page.close()
+ trace_file_path = tmpdir / "trace.zip"
+ await context.tracing.stop(path=trace_file_path)
+
+ (_, events) = parse_trace(trace_file_path)
+ assert events[0]["type"] == "context-options"
+ assert get_trace_actions(events) == [
+ "Page.goto",
+ "Page.set_content",
+ "Page.click",
+ "Mouse.move",
+ "Mouse.dblclick",
+ "Keyboard.insert_text",
+ "Page.wait_for_timeout",
+ "Page.route",
+ "Page.goto",
+ "Route.continue_",
+ "Page.goto",
+ "Page.close",
+ ]
+
+ assert len(list(filter(lambda e: e["type"] == "frame-snapshot", events))) >= 1
+ assert len(list(filter(lambda e: e["type"] == "screencast-frame", events))) >= 1
+ style = list(
+ filter(
+ lambda e: e["type"] == "resource-snapshot"
+ and e["snapshot"]["request"]["url"].endswith("style.css"),
+ events,
+ )
+ )[0]
+ assert style
+ assert style["snapshot"]["response"]["content"]["_sha1"]
+ script = list(
+ filter(
+ lambda e: e["type"] == "resource-snapshot"
+ and e["snapshot"]["request"]["url"].endswith("script.js"),
+ events,
+ )
+ )[0]
+ assert script
+ assert script["snapshot"]["response"]["content"].get("_sha1") is None
+
+
+async def test_should_collect_two_traces(
+ context: BrowserContext, page: Page, server: Server, tmpdir: Path
+) -> None:
+ await context.tracing.start(screenshots=True, snapshots=True)
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content("Click ")
+ await page.click('"Click"')
+ tracing1_path = tmpdir / "trace1.zip"
+ await context.tracing.stop(path=tracing1_path)
+
+ await context.tracing.start(screenshots=True, snapshots=True)
+ await page.dblclick('"Click"')
+ await page.close()
+ tracing2_path = tmpdir / "trace2.zip"
+ await context.tracing.stop(path=tracing2_path)
+
+ (_, events) = parse_trace(tracing1_path)
+ assert events[0]["type"] == "context-options"
+ assert get_trace_actions(events) == [
+ "Page.goto",
+ "Page.set_content",
+ "Page.click",
+ ]
+
+ (_, events) = parse_trace(tracing2_path)
+ assert events[0]["type"] == "context-options"
+ assert get_trace_actions(events) == ["Page.dblclick", "Page.close"]
+
+
+async def test_should_not_throw_when_stopping_without_start_but_not_exporting(
+ context: BrowserContext,
+) -> None:
+ await context.tracing.stop()
+
+
+async def test_should_work_with_playwright_context_managers(
+ context: BrowserContext, page: Page, server: Server, tmpdir: Path
+) -> None:
+ await context.tracing.start(screenshots=True, snapshots=True)
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content("Click ")
+ async with page.expect_console_message() as message_info:
+ await page.evaluate('() => console.log("hello")')
+ await page.click('"Click"')
+ assert (await message_info.value).text == "hello"
+
+ async with page.expect_popup():
+ await page.evaluate("window._popup = window.open(document.location.href)")
+ trace_file_path = tmpdir / "trace.zip"
+ await context.tracing.stop(path=trace_file_path)
+
+ (_, events) = parse_trace(trace_file_path)
+ assert events[0]["type"] == "context-options"
+ assert get_trace_actions(events) == [
+ "Page.goto",
+ "Page.set_content",
+ "Page.expect_console_message",
+ "Page.evaluate",
+ "Page.click",
+ "Page.expect_popup",
+ "Page.evaluate",
+ ]
+
+
+async def test_should_display_wait_for_load_state_even_if_did_not_wait_for_it(
+ context: BrowserContext, page: Page, server: Server, tmpdir: Path
+) -> None:
+ await context.tracing.start(screenshots=True, snapshots=True)
+
+ await page.goto(server.EMPTY_PAGE)
+ await page.wait_for_load_state("load")
+ await page.wait_for_load_state("load")
+
+ trace_file_path = tmpdir / "trace.zip"
+ await context.tracing.stop(path=trace_file_path)
+
+ (_, events) = parse_trace(trace_file_path)
+ assert get_trace_actions(events) == [
+ "Page.goto",
+ "Page.wait_for_load_state",
+ "Page.wait_for_load_state",
+ ]
+
+
+async def test_should_respect_traces_dir_and_name(
+ browser_type: BrowserType,
+ server: Server,
+ tmpdir: Path,
+ launch_arguments: Dict,
+) -> None:
+ traces_dir = tmpdir / "traces"
+ browser = await browser_type.launch(traces_dir=traces_dir, **launch_arguments)
+ context = await browser.new_context()
+ page = await context.new_page()
+
+ await context.tracing.start(name="name1", snapshots=True)
+ await page.goto(server.PREFIX + "/one-style.html")
+ await context.tracing.stop_chunk(path=tmpdir / "trace1.zip")
+ assert (traces_dir / "name1.trace").exists()
+ assert (traces_dir / "name1.network").exists()
+
+ await context.tracing.start_chunk(name="name2")
+ await page.goto(server.PREFIX + "/har.html")
+ await context.tracing.stop(path=tmpdir / "trace2.zip")
+ assert (traces_dir / "name2.trace").exists()
+ assert (traces_dir / "name2.network").exists()
+
+ await browser.close()
+
+ def resource_names(resources: Dict[str, bytes]) -> List[str]:
+ return sorted(
+ [
+ re.sub(r"^resources/.*\.(html|css)$", r"resources/XXX.\g<1>", file)
+ for file in resources.keys()
+ ]
+ )
+
+ (resources, events) = parse_trace(tmpdir / "trace1.zip")
+ assert get_trace_actions(events) == ["Page.goto"]
+ assert resource_names(resources) == [
+ "resources/XXX.css",
+ "resources/XXX.html",
+ "trace.network",
+ "trace.stacks",
+ "trace.trace",
+ ]
+
+ (resources, events) = parse_trace(tmpdir / "trace2.zip")
+ assert get_trace_actions(events) == ["Page.goto"]
+ assert resource_names(resources) == [
+ "resources/XXX.css",
+ "resources/XXX.html",
+ "resources/XXX.html",
+ "trace.network",
+ "trace.stacks",
+ "trace.trace",
+ ]
diff --git a/tests/async/test_unroute_behavior.py b/tests/async/test_unroute_behavior.py
new file mode 100644
index 0000000..036423c
--- /dev/null
+++ b/tests/async/test_unroute_behavior.py
@@ -0,0 +1,453 @@
+# 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 re
+
+from playwright.async_api import BrowserContext, Error, Page, Route
+from tests.server import Server
+from tests.utils import must
+
+
+async def test_context_unroute_should_not_wait_for_pending_handlers_to_complete(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.continue_()
+
+ await context.route(
+ re.compile(".*"),
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ await route.fallback()
+
+ await context.route(
+ re.compile(".*"),
+ _handler2,
+ )
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ await context.unroute(
+ re.compile(".*"),
+ _handler2,
+ )
+ route_barrier_future.set_result(None)
+ await navigation_task
+ assert second_handler_called
+
+
+async def test_context_unroute_all_removes_all_handlers(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ await context.route(
+ "**/*",
+ lambda route: route.abort(),
+ )
+ await context.route(
+ "**/empty.html",
+ lambda route: route.abort(),
+ )
+ await context.unroute_all()
+ await page.goto(server.EMPTY_PAGE)
+
+
+async def test_context_unroute_all_should_not_wait_for_pending_handlers_to_complete(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.abort()
+
+ await context.route(
+ re.compile(".*"),
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ await route.fallback()
+
+ await context.route(
+ re.compile(".*"),
+ _handler2,
+ )
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ did_unroute = False
+
+ async def _unroute_promise() -> None:
+ nonlocal did_unroute
+ await context.unroute_all(behavior="wait")
+ did_unroute = True
+
+ unroute_task = asyncio.create_task(_unroute_promise())
+ await asyncio.sleep(0.5)
+ assert did_unroute is False
+ route_barrier_future.set_result(None)
+ await unroute_task
+ assert did_unroute
+ await navigation_task
+ assert second_handler_called is False
+
+
+async def test_context_unroute_all_should_not_wait_for_pending_handlers_to_complete_if_behavior_is_ignore_errors(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.abort()
+
+ await context.route(
+ re.compile(".*"),
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ raise Exception("Handler error")
+
+ await context.route(
+ re.compile(".*"),
+ _handler2,
+ )
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ did_unroute = False
+
+ async def _unroute_promise() -> None:
+ await context.unroute_all(behavior="ignoreErrors")
+ nonlocal did_unroute
+ did_unroute = True
+
+ unroute_task = asyncio.create_task(_unroute_promise())
+ await asyncio.sleep(0.5)
+ await unroute_task
+ assert did_unroute
+ route_barrier_future.set_result(None)
+ try:
+ await navigation_task
+ except Error:
+ pass
+ # The error in the unrouted handler should be silently caught and remaining handler called.
+ assert not second_handler_called
+
+
+async def test_page_close_should_not_wait_for_active_route_handlers_on_the_owning_context(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await context.route(
+ re.compile(".*"),
+ lambda route: route_future.set_result(route),
+ )
+ await page.route(
+ re.compile(".*"),
+ lambda route: route.fallback(),
+ )
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ await route_future
+ await page.close()
+
+
+async def test_context_close_should_not_wait_for_active_route_handlers_on_the_owned_pages(
+ page: Page, context: BrowserContext, server: Server
+) -> None:
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route(
+ re.compile(".*"),
+ lambda route: route_future.set_result(route),
+ )
+ await page.route(re.compile(".*"), lambda route: route.fallback())
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ await route_future
+ await context.close()
+
+
+async def test_page_unroute_should_not_wait_for_pending_handlers_to_complete(
+ page: Page, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.continue_()
+
+ await page.route(
+ re.compile(".*"),
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ await route.fallback()
+
+ await page.route(
+ re.compile(".*"),
+ _handler2,
+ )
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ await page.unroute(
+ re.compile(".*"),
+ _handler2,
+ )
+ route_barrier_future.set_result(None)
+ await navigation_task
+ assert second_handler_called
+
+
+async def test_page_unroute_all_removes_all_routes(page: Page, server: Server) -> None:
+ await page.route(
+ "**/*",
+ lambda route: route.abort(),
+ )
+ await page.route(
+ "**/empty.html",
+ lambda route: route.abort(),
+ )
+ await page.unroute_all()
+ response = must(await page.goto(server.EMPTY_PAGE))
+ assert response.ok
+
+
+async def test_page_unroute_should_wait_for_pending_handlers_to_complete(
+ page: Page, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.abort()
+
+ await page.route(
+ "**/*",
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ await route.fallback()
+
+ await page.route(
+ "**/*",
+ _handler2,
+ )
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ did_unroute = False
+
+ async def _unroute_promise() -> None:
+ await page.unroute_all(behavior="wait")
+ nonlocal did_unroute
+ did_unroute = True
+
+ unroute_task = asyncio.create_task(_unroute_promise())
+ await asyncio.sleep(0.5)
+ assert did_unroute is False
+ route_barrier_future.set_result(None)
+ await unroute_task
+ assert did_unroute
+ await navigation_task
+ assert second_handler_called is False
+
+
+async def test_page_unroute_all_should_not_wait_for_pending_handlers_to_complete_if_behavior_is_ignore_errors(
+ page: Page, server: Server
+) -> None:
+ second_handler_called = False
+
+ async def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+ await route.abort()
+
+ await page.route(re.compile(".*"), _handler1)
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ route_barrier_future: "asyncio.Future[None]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await route_barrier_future
+ raise Exception("Handler error")
+
+ await page.route(re.compile(".*"), _handler2)
+ navigation_task = asyncio.create_task(page.goto(server.EMPTY_PAGE))
+ await route_future
+ did_unroute = False
+
+ async def _unroute_promise() -> None:
+ await page.unroute_all(behavior="ignoreErrors")
+ nonlocal did_unroute
+ did_unroute = True
+
+ unroute_task = asyncio.create_task(_unroute_promise())
+ await asyncio.sleep(0.5)
+ await unroute_task
+ assert did_unroute
+ route_barrier_future.set_result(None)
+ try:
+ await navigation_task
+ except Error:
+ pass
+ # The error in the unrouted handler should be silently caught.
+ assert not second_handler_called
+
+
+async def test_page_close_does_not_wait_for_active_route_handlers(
+ page: Page, server: Server
+) -> None:
+ stalling_future: "asyncio.Future[None]" = asyncio.Future()
+ second_handler_called = False
+
+ def _handler1(route: Route) -> None:
+ nonlocal second_handler_called
+ second_handler_called = True
+
+ await page.route(
+ "**/*",
+ _handler1,
+ )
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+
+ async def _handler2(route: Route) -> None:
+ route_future.set_result(route)
+ await stalling_future
+
+ await page.route(
+ "**/*",
+ _handler2,
+ )
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ await route_future
+ await page.close()
+ await asyncio.sleep(0.5)
+ assert not second_handler_called
+ stalling_future.cancel()
+
+
+async def test_route_continue_should_not_throw_if_page_has_been_closed(
+ page: Page, server: Server
+) -> None:
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route(
+ re.compile(".*"),
+ lambda route: route_future.set_result(route),
+ )
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ route = await route_future
+ await page.close()
+ # Should not throw.
+ await route.continue_()
+
+
+async def test_route_fallback_should_not_throw_if_page_has_been_closed(
+ page: Page, server: Server
+) -> None:
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route(
+ re.compile(".*"),
+ lambda route: route_future.set_result(route),
+ )
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ route = await route_future
+ await page.close()
+ # Should not throw.
+ await route.fallback()
+
+
+async def test_route_fulfill_should_not_throw_if_page_has_been_closed(
+ page: Page, server: Server
+) -> None:
+ route_future: "asyncio.Future[Route]" = asyncio.Future()
+ await page.route(
+ "**/*",
+ lambda route: route_future.set_result(route),
+ )
+
+ async def _goto_ignore_exceptions() -> None:
+ try:
+ await page.goto(server.EMPTY_PAGE)
+ except Error:
+ pass
+
+ asyncio.create_task(_goto_ignore_exceptions())
+ route = await route_future
+ await page.close()
+ # Should not throw.
+ await route.fulfill()
diff --git a/tests/async/test_video.py b/tests/async/test_video.py
new file mode 100644
index 0000000..205537c
--- /dev/null
+++ b/tests/async/test_video.py
@@ -0,0 +1,82 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+from pathlib import Path
+from typing import Dict
+
+from playwright.async_api import Browser, BrowserType
+from tests.server import Server
+
+
+async def test_should_expose_video_path(browser: Browser, tmpdir: Path, server: Server) -> None:
+ page = await browser.new_page(record_video_dir=tmpdir)
+ await page.goto(server.PREFIX + "/grid.html")
+ assert page.video
+ path = await page.video.path()
+ assert str(tmpdir) in str(path)
+ await page.context.close()
+
+
+async def test_short_video_should_throw(browser: Browser, tmpdir: Path, server: Server) -> None:
+ page = await browser.new_page(record_video_dir=tmpdir)
+ await page.goto(server.PREFIX + "/grid.html")
+ assert page.video
+ path = await page.video.path()
+ assert str(tmpdir) in str(path)
+ await page.wait_for_timeout(1000)
+ await page.context.close()
+ assert os.path.exists(path)
+
+
+async def test_short_video_should_throw_persistent_context(
+ browser_type: BrowserType, tmpdir: Path, launch_arguments: Dict, server: Server
+) -> None:
+ context = await browser_type.launch_persistent_context(
+ str(tmpdir),
+ **launch_arguments,
+ viewport={"width": 320, "height": 240},
+ record_video_dir=str(tmpdir) + "1",
+ )
+ page = context.pages[0]
+ await page.goto(server.PREFIX + "/grid.html")
+ await page.wait_for_timeout(1000)
+ await context.close()
+
+ assert page.video
+ path = await page.video.path()
+ assert str(tmpdir) in str(path)
+
+
+async def test_should_not_error_if_page_not_closed_before_save_as(
+ browser: Browser, tmpdir: Path, server: Server
+) -> None:
+ page = await browser.new_page(record_video_dir=tmpdir)
+ await page.goto(server.PREFIX + "/grid.html")
+ await page.wait_for_timeout(1000) # make sure video has some data
+ out_path = tmpdir / "some-video.webm"
+ assert page.video
+ saved = page.video.save_as(out_path)
+ await page.close()
+ await saved
+ await page.context.close()
+ assert os.path.exists(out_path)
+
+
+async def test_should_be_None_if_not_recording(
+ browser: Browser, tmpdir: Path, server: Server
+) -> None:
+ page = await browser.new_page()
+ assert page.video is None
+ await page.close()
diff --git a/tests/async/test_wait_for_function.py b/tests/async/test_wait_for_function.py
new file mode 100644
index 0000000..9d11719
--- /dev/null
+++ b/tests/async/test_wait_for_function.py
@@ -0,0 +1,91 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from datetime import datetime
+
+import pytest
+
+from playwright.async_api import ConsoleMessage, Error, Page
+
+
+async def test_should_timeout(page: Page) -> None:
+ start_time = datetime.now()
+ timeout = 42
+ await page.wait_for_timeout(timeout)
+ assert ((datetime.now() - start_time).microseconds * 1000) >= timeout / 2
+
+
+async def test_should_accept_a_string(page: Page) -> None:
+ watchdog = page.wait_for_function("window.__FOO === 1")
+ await page.evaluate("window['__FOO'] = 1")
+ await watchdog
+
+
+async def test_should_work_when_resolved_right_before_execution_context_disposal(
+ page: Page,
+) -> None:
+ await page.add_init_script("window['__RELOADED'] = true")
+ await page.wait_for_function(
+ """() => {
+ if (!window['__RELOADED'])
+ window.location.reload();
+ return true;
+ }"""
+ )
+
+
+async def test_should_poll_on_interval(page: Page) -> None:
+ polling = 100
+ time_delta = await page.wait_for_function(
+ """() => {
+ if (!window['__startTime']) {
+ window['__startTime'] = Date.now();
+ return false;
+ }
+ return Date.now() - window['__startTime'];
+ }""",
+ polling=polling,
+ )
+ assert await time_delta.json_value() >= polling
+
+
+async def test_should_avoid_side_effects_after_timeout(page: Page) -> None:
+ counter = 0
+
+ async def on_console(message: ConsoleMessage) -> None:
+ nonlocal counter
+ counter += 1
+
+ page.on("console", on_console)
+ with pytest.raises(Error) as exc_info:
+ await page.wait_for_function(
+ """() => {
+ window['counter'] = (window['counter'] || 0) + 1;
+ console.log(window['counter']);
+ }""",
+ polling=1,
+ timeout=1000,
+ )
+
+ saved_counter = counter
+ await page.wait_for_timeout(2000) # Give it some time to produce more logs.
+
+ assert "Timeout 1000ms exceeded" in exc_info.value.message
+ assert counter == saved_counter
+
+
+async def test_should_throw_on_polling_mutation(page: Page) -> None:
+ with pytest.raises(Error) as exc_info:
+ await page.wait_for_function("() => true", polling="mutation") # type: ignore
+ assert "Unknown polling option: mutation" in exc_info.value.message
diff --git a/tests/async/test_wait_for_url.py b/tests/async/test_wait_for_url.py
new file mode 100644
index 0000000..b469d79
--- /dev/null
+++ b/tests/async/test_wait_for_url.py
@@ -0,0 +1,130 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+
+import pytest
+
+from playwright.async_api import Error, Page
+from tests.server import Server
+
+
+async def test_wait_for_url_should_work(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.evaluate("url => window.location.href = url", server.PREFIX + "/grid.html")
+ await page.wait_for_url("**/grid.html")
+ assert "grid.html" in page.url
+
+
+async def test_wait_for_url_should_respect_timeout(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ with pytest.raises(Error) as exc_info:
+ await page.wait_for_url("**/frame.html", timeout=2500)
+ assert "Timeout 2500ms exceeded" in exc_info.value.message
+
+
+async def test_wait_for_url_should_work_with_both_domcontentloaded_and_load(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.wait_for_url("**/*", wait_until="domcontentloaded")
+ await page.wait_for_url("**/*", wait_until="load")
+
+
+async def test_wait_for_url_should_work_with_clicking_on_anchor_links(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content('foobar ')
+ await page.click("a")
+ await page.wait_for_url("**/*#foobar")
+ assert page.url == server.EMPTY_PAGE + "#foobar"
+
+
+async def test_wait_for_url_should_work_with_history_push_state(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(
+ """
+ SPA
+
+ """
+ )
+ await page.click("a")
+ await page.wait_for_url("**/wow.html")
+ assert page.url == server.PREFIX + "/wow.html"
+
+
+async def test_wait_for_url_should_work_with_history_replace_state(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(
+ """
+ SPA
+
+ """
+ )
+ await page.click("a")
+ await page.wait_for_url("**/replaced.html")
+ assert page.url == server.PREFIX + "/replaced.html"
+
+
+async def test_wait_for_url_should_work_with_dom_history_back_forward(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(
+ """
+ back
+ forward
+
+ """
+ )
+
+ assert page.url == server.PREFIX + "/second.html"
+
+ await page.click("a#back")
+ await page.wait_for_url("**/first.html")
+ assert page.url == server.PREFIX + "/first.html"
+
+ await page.click("a#forward")
+ await page.wait_for_url("**/second.html")
+ assert page.url == server.PREFIX + "/second.html"
+
+
+async def test_wait_for_url_should_work_with_url_match_for_same_document_navigations(
+ page: Page, server: Server
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.evaluate("history.pushState({}, '', '/first.html')")
+ await page.evaluate("history.pushState({}, '', '/second.html')")
+ await page.evaluate("history.pushState({}, '', '/third.html')")
+ await page.wait_for_url(re.compile(r"third\.html"))
+ assert "/third.html" in page.url
+
+
+async def test_wait_for_url_should_work_with_commit(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.evaluate("url => window.location.href = url", server.PREFIX + "/grid.html")
+ await page.wait_for_url("**/grid.html", wait_until="commit")
+ assert "grid.html" in page.url
diff --git a/tests/async/test_websocket.py b/tests/async/test_websocket.py
new file mode 100644
index 0000000..7679c76
--- /dev/null
+++ b/tests/async/test_websocket.py
@@ -0,0 +1,193 @@
+# 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
+from typing import Union
+
+import pytest
+from flaky import flaky
+
+from playwright.async_api import Error, Page, WebSocket
+from tests.server import Server, WebSocketProtocol
+
+
+async def test_should_work(page: Page, server: Server) -> None:
+ server.send_on_web_socket_connection(b"incoming")
+ value = await page.evaluate(
+ """port => {
+ let cb;
+ const result = new Promise(f => cb = f);
+ const ws = new WebSocket('ws://localhost:' + port + '/ws');
+ ws.addEventListener('message', data => { ws.close(); cb(data.data); });
+ return result;
+ }""",
+ server.PORT,
+ )
+ assert value == "incoming"
+ pass
+
+
+async def test_should_emit_close_events(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ close_future: asyncio.Future[None] = asyncio.Future()
+ async with page.expect_websocket() as ws_info:
+ await page.evaluate(
+ """port => {
+ const ws = new WebSocket('ws://localhost:' + port + '/ws');
+ ws.addEventListener('open', data => ws.close());
+ }""",
+ server.PORT,
+ )
+ ws = await ws_info.value
+ ws.on("close", lambda ws: close_future.set_result(None))
+ assert ws.url == f"ws://localhost:{server.PORT}/ws"
+ assert repr(ws) == f""
+ await close_future
+ assert ws.is_closed()
+
+
+async def test_should_emit_frame_events(page: Page, server: Server) -> None:
+ def _handle_ws_connection(ws: WebSocketProtocol) -> None:
+ def _onMessage(payload: bytes, isBinary: bool) -> None:
+ ws.sendMessage(b"incoming", False)
+ ws.sendClose()
+
+ setattr(ws, "onMessage", _onMessage)
+
+ server.once_web_socket_connection(_handle_ws_connection)
+ log = []
+ socket_close_future: "asyncio.Future[None]" = asyncio.Future()
+
+ def on_web_socket(ws: WebSocket) -> None:
+ log.append("open")
+
+ def _on_framesent(payload: Union[bytes, str]) -> None:
+ assert isinstance(payload, str)
+ log.append(f"sent<{payload}>")
+
+ ws.on("framesent", _on_framesent)
+
+ def _on_framereceived(payload: Union[bytes, str]) -> None:
+ assert isinstance(payload, str)
+ log.append(f"received<{payload}>")
+
+ ws.on("framereceived", _on_framereceived)
+
+ def _handle_close(ws: WebSocket) -> None:
+ log.append("close")
+ socket_close_future.set_result(None)
+
+ ws.on("close", _handle_close)
+
+ page.on("websocket", on_web_socket)
+ async with page.expect_event("websocket"):
+ await page.evaluate(
+ """port => {
+ const ws = new WebSocket('ws://localhost:' + port + '/ws');
+ ws.addEventListener('open', () => ws.send('outgoing'));
+ ws.addEventListener('message', () => ws.close())
+ }""",
+ server.PORT,
+ )
+ await socket_close_future
+ assert log[0] == "open"
+ assert log[3] == "close"
+ log.sort()
+ assert log == ["close", "open", "received", "sent"]
+
+
+async def test_should_emit_binary_frame_events(page: Page, server: Server) -> None:
+ def _handle_ws_connection(ws: WebSocketProtocol) -> None:
+ ws.sendMessage(b"incoming")
+
+ def _onMessage(payload: bytes, isBinary: bool) -> None:
+ if payload == b"echo-bin":
+ ws.sendMessage(b"\x04\x02", True)
+ ws.sendClose()
+ if payload == b"echo-text":
+ ws.sendMessage(b"text", False)
+ ws.sendClose()
+
+ setattr(ws, "onMessage", _onMessage)
+
+ server.once_web_socket_connection(_handle_ws_connection)
+ done_task: "asyncio.Future[None]" = asyncio.Future()
+ sent = []
+ received = []
+
+ def on_web_socket(ws: WebSocket) -> None:
+ ws.on("framesent", lambda payload: sent.append(payload))
+ ws.on("framereceived", lambda payload: received.append(payload))
+ ws.on("close", lambda _: done_task.set_result(None))
+
+ page.on("websocket", on_web_socket)
+ async with page.expect_event("websocket"):
+ await page.evaluate(
+ """port => {
+ const ws = new WebSocket('ws://localhost:' + port + '/ws');
+ ws.addEventListener('open', () => {
+ const binary = new Uint8Array(5);
+ for (let i = 0; i < 5; ++i)
+ binary[i] = i;
+ ws.send(binary);
+ ws.send('echo-bin');
+ });
+ }""",
+ server.PORT,
+ )
+ await done_task
+ assert sent == [b"\x00\x01\x02\x03\x04", "echo-bin"]
+ assert received == ["incoming", b"\x04\x02"]
+
+
+@flaky
+async def test_should_reject_wait_for_event_on_close_and_error(page: Page, server: Server) -> None:
+ server.send_on_web_socket_connection(b"incoming")
+ async with page.expect_event("websocket") as ws_info:
+ await page.evaluate(
+ """port => {
+ window.ws = new WebSocket('ws://localhost:' + port + '/ws');
+ }""",
+ server.PORT,
+ )
+ ws = await ws_info.value
+ await ws.wait_for_event("framereceived")
+ with pytest.raises(Error) as exc_info:
+ async with ws.expect_event("framesent"):
+ await page.evaluate("window.ws.close()")
+ assert exc_info.value.message == "Socket closed"
+
+
+async def test_should_emit_error_event(page: Page, server: Server, browser_name: str) -> None:
+ future: "asyncio.Future[str]" = asyncio.Future()
+
+ def _on_ws_socket_error(err: str) -> None:
+ future.set_result(err)
+
+ def _on_websocket(websocket: WebSocket) -> None:
+ websocket.on("socketerror", _on_ws_socket_error)
+
+ page.on(
+ "websocket",
+ _on_websocket,
+ )
+ await page.evaluate(
+ """port => new WebSocket(`ws://localhost:${port}/bogus-ws`)""",
+ server.PORT,
+ )
+ err = await future
+ if browser_name == "firefox":
+ assert err == "CLOSE_ABNORMAL"
+ else:
+ assert ": 404" in err
diff --git a/tests/async/test_worker.py b/tests/async/test_worker.py
new file mode 100644
index 0000000..219fad1
--- /dev/null
+++ b/tests/async/test_worker.py
@@ -0,0 +1,191 @@
+# 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
+from asyncio.futures import Future
+
+import pytest
+from flaky import flaky
+
+from playwright.async_api import Browser, ConsoleMessage, Error, Page, Worker
+from tests.server import Server
+from tests.utils import TARGET_CLOSED_ERROR_MESSAGE
+
+
+async def test_workers_page_workers(page: Page, server: Server) -> None:
+ async with page.expect_worker() as worker_info:
+ await page.goto(server.PREFIX + "/worker/worker.html")
+ worker = await worker_info.value
+ assert "worker.js" in worker.url
+ assert repr(worker) == f""
+
+ assert await worker.evaluate('() => self["workerFunction"]()') == "worker function result"
+
+ await page.goto(server.EMPTY_PAGE)
+ assert len(page.workers) == 0
+
+
+async def test_workers_should_emit_created_and_destroyed_events(page: Page) -> None:
+ worker_obj = None
+ async with page.expect_event("worker") as event_info:
+ worker_obj = await page.evaluate_handle(
+ "() => new Worker(URL.createObjectURL(new Blob(['1'], {type: 'application/javascript'})))"
+ )
+ worker = await event_info.value
+ worker_this_obj = await worker.evaluate_handle("() => this")
+ worker_destroyed_promise: Future[Worker] = asyncio.Future()
+ worker.once("close", lambda w: worker_destroyed_promise.set_result(w))
+ await page.evaluate("workerObj => workerObj.terminate()", worker_obj)
+ assert await worker_destroyed_promise == worker
+ with pytest.raises(Error) as exc:
+ await worker_this_obj.get_property("self")
+ assert TARGET_CLOSED_ERROR_MESSAGE in exc.value.message
+
+
+async def test_workers_should_report_console_logs(page: Page) -> None:
+ async with page.expect_console_message() as message_info:
+ await page.evaluate(
+ '() => new Worker(URL.createObjectURL(new Blob(["console.log(1)"], {type: "application/javascript"})))'
+ )
+ message = await message_info.value
+ assert message.text == "1"
+
+
+async def test_workers_should_have_JSHandles_for_console_logs(
+ page: Page, browser_name: str
+) -> None:
+ log_promise: "asyncio.Future[ConsoleMessage]" = asyncio.Future()
+ page.on("console", lambda m: log_promise.set_result(m))
+ await page.evaluate(
+ "() => new Worker(URL.createObjectURL(new Blob(['console.log(1,2,3,this)'], {type: 'application/javascript'})))"
+ )
+ log = await log_promise
+ if browser_name != "firefox":
+ assert log.text == "1 2 3 DedicatedWorkerGlobalScope"
+ else:
+ assert log.text == "1 2 3 JSHandle@object"
+ assert len(log.args) == 4
+ assert await (await log.args[3].get_property("origin")).json_value() == "null"
+
+
+async def test_workers_should_evaluate(page: Page) -> None:
+ async with page.expect_event("worker") as event_info:
+ await page.evaluate(
+ "() => new Worker(URL.createObjectURL(new Blob(['console.log(1)'], {type: 'application/javascript'})))"
+ )
+ worker = await event_info.value
+ assert await worker.evaluate("1+1") == 2
+
+
+async def test_workers_should_report_errors(page: Page) -> None:
+ error_promise: "asyncio.Future[Error]" = asyncio.Future()
+ page.on("pageerror", lambda e: error_promise.set_result(e))
+ await page.evaluate(
+ """() => new Worker(URL.createObjectURL(new Blob([`
+ setTimeout(() => {
+ // Do a console.log just to check that we do not confuse it with an error.
+ console.log('hey');
+ throw new Error('this is my error');
+ })
+ `], {type: 'application/javascript'})))"""
+ )
+ error_log = await error_promise
+ assert "this is my error" in error_log.message
+
+
+@flaky # Upstream flaky
+async def test_workers_should_clear_upon_navigation(server: Server, page: Page) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_event("worker") as event_info:
+ await page.evaluate(
+ '() => new Worker(URL.createObjectURL(new Blob(["console.log(1)"], {type: "application/javascript"})))'
+ )
+ worker = await event_info.value
+ assert len(page.workers) == 1
+ destroyed = []
+ worker.once("close", lambda _: destroyed.append(True))
+ await page.goto(server.PREFIX + "/one-style.html")
+ assert destroyed == [True]
+ assert len(page.workers) == 0
+
+
+@flaky # Upstream flaky
+async def test_workers_should_clear_upon_cross_process_navigation(
+ server: Server, page: Page
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_event("worker") as event_info:
+ await page.evaluate(
+ "() => new Worker(URL.createObjectURL(new Blob(['console.log(1)'], {type: 'application/javascript'})))"
+ )
+ worker = await event_info.value
+ assert len(page.workers) == 1
+ destroyed = []
+ worker.once("close", lambda _: destroyed.append(True))
+ await page.goto(server.CROSS_PROCESS_PREFIX + "/empty.html")
+ assert destroyed == [True]
+ assert len(page.workers) == 0
+
+
+@pytest.mark.skip_browser("firefox") # https://github.com/microsoft/playwright/issues/21760
+async def test_workers_should_report_network_activity(page: Page, server: Server) -> None:
+ async with page.expect_worker() as worker_info:
+ await page.goto(server.PREFIX + "/worker/worker.html")
+ worker = await worker_info.value
+ url = server.PREFIX + "/one-style.css"
+ async with page.expect_request(url) as request_info, page.expect_response(url) as response_info:
+ await worker.evaluate(
+ "url => fetch(url).then(response => response.text()).then(console.log)", url
+ )
+ request = await request_info.value
+ response = await response_info.value
+ assert request.url == url
+ assert response.request == request
+ assert response.ok
+
+
+@pytest.mark.skip_browser("firefox") # https://github.com/microsoft/playwright/issues/21760
+async def test_workers_should_report_network_activity_on_worker_creation(
+ page: Page, server: Server
+) -> None:
+ # Chromium needs waitForDebugger enabled for this one.
+ await page.goto(server.EMPTY_PAGE)
+ url = server.PREFIX + "/one-style.css"
+ async with page.expect_request(url) as request_info, page.expect_response(url) as response_info:
+ await page.evaluate(
+ """url => new Worker(URL.createObjectURL(new Blob([`
+ fetch("${url}").then(response => response.text()).then(console.log);
+ `], {type: 'application/javascript'})))""",
+ url,
+ )
+ request = await request_info.value
+ response = await response_info.value
+ assert request.url == url
+ assert response.request == request
+ assert response.ok
+
+
+async def test_workers_should_format_number_using_context_locale(
+ browser: Browser, server: Server
+) -> None:
+ context = await browser.new_context(locale="ru-RU")
+ page = await context.new_page()
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_worker() as worker_info:
+ await page.evaluate(
+ "() => new Worker(URL.createObjectURL(new Blob(['console.log(1)'], {type: 'application/javascript'})))"
+ )
+ worker = await worker_info.value
+ assert await worker.evaluate("() => (10000.20).toLocaleString()") == "10\u00A0000,2"
+ await context.close()
diff --git a/tests/async/utils.py b/tests/async/utils.py
new file mode 100644
index 0000000..546952d
--- /dev/null
+++ b/tests/async/utils.py
@@ -0,0 +1,75 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+from typing import Any, List, cast
+
+from playwright.async_api import (
+ ElementHandle,
+ Error,
+ Frame,
+ Page,
+ Selectors,
+ ViewportSize,
+)
+
+
+class Utils:
+ async def attach_frame(self, page: Page, frame_id: str, url: str) -> Frame:
+ handle = await page.evaluate_handle(
+ """async ({ frame_id, url }) => {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ frame.id = frame_id;
+ document.body.appendChild(frame);
+ await new Promise(x => frame.onload = x);
+ return frame;
+ }""",
+ {"frame_id": frame_id, "url": url},
+ )
+ frame = await cast(ElementHandle, handle.as_element()).content_frame()
+ assert frame
+ return frame
+
+ async def detach_frame(self, page: Page, frame_id: str) -> None:
+ await page.evaluate("frame_id => document.getElementById(frame_id).remove()", frame_id)
+
+ def dump_frames(self, frame: Frame, indentation: str = "") -> List[str]:
+ indentation = indentation or ""
+ description = re.sub(r":\d+/", ":/", frame.url)
+ if frame.name:
+ description += " (" + frame.name + ")"
+ result = [indentation + description]
+ sorted_frames = sorted(frame.child_frames, key=lambda frame: frame.url + frame.name)
+ for child in sorted_frames:
+ result = result + utils.dump_frames(child, " " + indentation)
+ return result
+
+ async def verify_viewport(self, page: Page, width: int, height: int) -> None:
+ assert cast(ViewportSize, page.viewport_size)["width"] == width
+ assert cast(ViewportSize, page.viewport_size)["height"] == height
+ assert await page.evaluate("window.innerWidth") == width
+ assert await page.evaluate("window.innerHeight") == height
+
+ async def register_selector_engine(
+ self, selectors: Selectors, *args: Any, **kwargs: Any
+ ) -> None:
+ try:
+ await selectors.register(*args, **kwargs)
+ except Error as exc:
+ if "has been already registered" not in exc.message:
+ raise exc
+
+
+utils = Utils()
diff --git a/tests/async_imp/__init__.py b/tests/async_imp/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/async_imp/conftest.py b/tests/async_imp/conftest.py
new file mode 100644
index 0000000..268c8a4
--- /dev/null
+++ b/tests/async_imp/conftest.py
@@ -0,0 +1,139 @@
+# 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
+from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, Generator, List
+
+import pytest
+
+from playwright.async_api import (
+ Browser,
+ BrowserContext,
+ BrowserType,
+ Page,
+ Playwright,
+ Selectors,
+ async_playwright,
+)
+
+from .utils import Utils
+from .utils import utils as utils_object
+
+
+@pytest.fixture
+def utils() -> Generator[Utils, None, None]:
+ yield utils_object
+
+
+# Will mark all the tests as async
+def pytest_collection_modifyitems(items: List[pytest.Item]) -> None:
+ for item in items:
+ item.add_marker(pytest.mark.asyncio)
+
+
+@pytest.fixture(scope="session")
+async def playwright() -> AsyncGenerator[Playwright, None]:
+ async with async_playwright() as playwright_object:
+ yield playwright_object
+
+
+@pytest.fixture(scope="session")
+def browser_type(playwright: Playwright, browser_name: str) -> BrowserType:
+ if browser_name == "chromium":
+ return playwright.chromium
+ if browser_name == "firefox":
+ return playwright.firefox
+ if browser_name == "webkit":
+ return playwright.webkit
+ raise Exception(f"Invalid browser_name: {browser_name}")
+
+
+@pytest.fixture(scope="session")
+async def browser_factory(
+ launch_arguments: Dict, browser_type: BrowserType
+) -> AsyncGenerator[Callable[..., Awaitable[Browser]], None]:
+ browsers = []
+
+ async def launch(**kwargs: Any) -> Browser:
+ browser = await browser_type.launch(**launch_arguments, **kwargs)
+ browsers.append(browser)
+ return browser
+
+ yield launch
+ for browser in browsers:
+ await browser.close()
+
+
+@pytest.fixture(scope="session")
+async def browser(
+ browser_factory: "Callable[..., asyncio.Future[Browser]]",
+) -> AsyncGenerator[Browser, None]:
+ browser = await browser_factory()
+ yield browser
+ await browser.close()
+
+
+@pytest.fixture(scope="session")
+async def browser_version(browser: Browser) -> str:
+ return browser.version
+
+
+@pytest.fixture
+async def context_factory(
+ browser: Browser,
+) -> AsyncGenerator["Callable[..., Awaitable[BrowserContext]]", None]:
+ contexts = []
+
+ async def launch(**kwargs: Any) -> BrowserContext:
+ context = await browser.new_context(**kwargs)
+ contexts.append(context)
+ return context
+
+ yield launch
+ for context in contexts:
+ await context.close()
+
+
+@pytest.fixture(scope="session")
+async def default_same_site_cookie_value(browser_name: str, is_linux: bool) -> str:
+ if browser_name == "chromium":
+ return "Lax"
+ if browser_name == "firefox":
+ return "None"
+ if browser_name == "webkit" and is_linux:
+ return "Lax"
+ if browser_name == "webkit" and not is_linux:
+ return "None"
+ raise Exception(f"Invalid browser_name: {browser_name}")
+
+
+@pytest.fixture
+async def context(
+ context_factory: "Callable[..., asyncio.Future[BrowserContext]]",
+) -> AsyncGenerator[BrowserContext, None]:
+ context = await context_factory()
+ yield context
+ await context.close()
+
+
+@pytest.fixture
+async def page(context: BrowserContext) -> AsyncGenerator[Page, None]:
+ page = await context.new_page()
+ yield page
+ await page.close()
+
+
+@pytest.fixture(scope="session")
+def selectors(playwright: Playwright) -> Selectors:
+ return playwright.selectors
diff --git a/tests/async_imp/test_frames.py b/tests/async_imp/test_frames.py
new file mode 100644
index 0000000..8deb70c
--- /dev/null
+++ b/tests/async_imp/test_frames.py
@@ -0,0 +1,280 @@
+# 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
+from typing import Optional
+
+import pytest
+
+from playwright.async_api import Error, Page
+from tests.server import Server
+
+from .utils import Utils
+
+
+async def test_evaluate_handle(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ main_frame = page.main_frame
+ assert main_frame.page == page
+ window_handle = await main_frame.evaluate_handle("window")
+ assert window_handle
+
+
+async def test_frame_element(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ assert frame1
+ await utils.attach_frame(page, "frame2", server.EMPTY_PAGE)
+ frame3 = await utils.attach_frame(page, "frame3", server.EMPTY_PAGE)
+ assert frame3
+ frame1handle1 = await page.query_selector("#frame1")
+ assert frame1handle1
+ frame1handle2 = await frame1.frame_element()
+ frame3handle1 = await page.query_selector("#frame3")
+ assert frame3handle1
+ frame3handle2 = await frame3.frame_element()
+ assert await frame1handle1.evaluate("(a, b) => a === b", frame1handle2)
+ assert await frame3handle1.evaluate("(a, b) => a === b", frame3handle2)
+ assert await frame1handle1.evaluate("(a, b) => a === b", frame3handle1) is False
+
+
+async def test_frame_element_with_content_frame(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ frame = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ handle = await frame.frame_element()
+ content_frame = await handle.content_frame()
+ assert content_frame == frame
+
+
+async def test_frame_element_throw_when_detached(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ await page.eval_on_selector("#frame1", "e => e.remove()")
+ error: Optional[Error] = None
+ try:
+ await frame1.frame_element()
+ except Error as e:
+ error = e
+ assert error
+ assert error.message == "Frame.frame_element: Frame has been detached."
+
+
+async def test_evaluate_throw_for_detached_frames(page: Page, server: Server, utils: Utils) -> None:
+ frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ assert frame1
+ await utils.detach_frame(page, "frame1")
+ error: Optional[Error] = None
+ try:
+ await frame1.evaluate("7 * 8")
+ except Error as e:
+ error = e
+ assert error
+ assert "Frame was detached" in error.message
+
+
+async def test_evaluate_isolated_between_frames(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ assert len(page.frames) == 2
+ [frame1, frame2] = page.frames
+ assert frame1 != frame2
+
+ await asyncio.gather(frame1.evaluate("window.a = 1"), frame2.evaluate("window.a = 2"))
+ [a1, a2] = await asyncio.gather(frame1.evaluate("window.a"), frame2.evaluate("window.a"))
+ assert a1 == 1
+ assert a2 == 2
+
+
+async def test_should_handle_nested_frames(page: Page, server: Server, utils: Utils) -> None:
+ await page.goto(server.PREFIX + "/frames/nested-frames.html")
+ assert utils.dump_frames(page.main_frame) == [
+ "http://localhost:/frames/nested-frames.html",
+ " http://localhost:/frames/frame.html (aframe)",
+ " http://localhost:/frames/two-frames.html (2frames)",
+ " http://localhost:/frames/frame.html (dos)",
+ " http://localhost:/frames/frame.html (uno)",
+ ]
+
+
+async def test_should_send_events_when_frames_are_manipulated_dynamically(
+ page: Page, server: Server, utils: Utils
+) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ # validate frameattached events
+ attached_frames = []
+ page.on("frameattached", lambda frame: attached_frames.append(frame))
+ await utils.attach_frame(page, "frame1", "./assets/frame.html")
+ assert len(attached_frames) == 1
+ assert "/assets/frame.html" in attached_frames[0].url
+
+ # validate framenavigated events
+ navigated_frames = []
+ page.on("framenavigated", lambda frame: navigated_frames.append(frame))
+ await page.evaluate(
+ """() => {
+ frame = document.getElementById('frame1')
+ frame.src = './empty.html'
+ return new Promise(x => frame.onload = x)
+ }"""
+ )
+
+ assert len(navigated_frames) == 1
+ assert navigated_frames[0].url == server.EMPTY_PAGE
+
+ # validate framedetached events
+ detached_frames = []
+ page.on("framedetached", lambda frame: detached_frames.append(frame))
+ await utils.detach_frame(page, "frame1")
+ assert len(detached_frames) == 1
+ assert detached_frames[0].is_detached()
+
+
+async def test_framenavigated_when_navigating_on_anchor_urls(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ async with page.expect_event("framenavigated"):
+ await page.goto(server.EMPTY_PAGE + "#foo")
+ assert page.url == server.EMPTY_PAGE + "#foo"
+
+
+async def test_persist_main_frame_on_cross_process_navigation(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ main_frame = page.main_frame
+ await page.goto(server.CROSS_PROCESS_PREFIX + "/empty.html")
+ assert page.main_frame == main_frame
+
+
+async def test_should_not_send_attach_detach_events_for_main_frame(
+ page: Page, server: Server
+) -> None:
+ has_events = []
+ page.on("frameattached", lambda frame: has_events.append(True))
+ page.on("framedetached", lambda frame: has_events.append(True))
+ await page.goto(server.EMPTY_PAGE)
+ assert has_events == []
+
+
+async def test_detach_child_frames_on_navigation(page: Page, server: Server) -> None:
+ attached_frames = []
+ detached_frames = []
+ navigated_frames = []
+ page.on("frameattached", lambda frame: attached_frames.append(frame))
+ page.on("framedetached", lambda frame: detached_frames.append(frame))
+ page.on("framenavigated", lambda frame: navigated_frames.append(frame))
+ await page.goto(server.PREFIX + "/frames/nested-frames.html")
+ assert len(attached_frames) == 4
+ assert len(detached_frames) == 0
+ assert len(navigated_frames) == 5
+
+ attached_frames = []
+ detached_frames = []
+ navigated_frames = []
+ await page.goto(server.EMPTY_PAGE)
+ assert len(attached_frames) == 0
+ assert len(detached_frames) == 4
+ assert len(navigated_frames) == 1
+
+
+async def test_framesets(page: Page, server: Server) -> None:
+ attached_frames = []
+ detached_frames = []
+ navigated_frames = []
+ page.on("frameattached", lambda frame: attached_frames.append(frame))
+ page.on("framedetached", lambda frame: detached_frames.append(frame))
+ page.on("framenavigated", lambda frame: navigated_frames.append(frame))
+ await page.goto(server.PREFIX + "/frames/frameset.html")
+ assert len(attached_frames) == 4
+ assert len(detached_frames) == 0
+ assert len(navigated_frames) == 5
+
+ attached_frames = []
+ detached_frames = []
+ navigated_frames = []
+ await page.goto(server.EMPTY_PAGE)
+ assert len(attached_frames) == 0
+ assert len(detached_frames) == 4
+ assert len(navigated_frames) == 1
+
+
+async def test_frame_from_inside_shadow_dom(page: Page, server: Server) -> None:
+ await page.goto(server.PREFIX + "/shadow.html")
+ await page.evaluate(
+ """async url => {
+ frame = document.createElement('iframe');
+ frame.src = url;
+ document.body.shadowRoot.appendChild(frame);
+ await new Promise(x => frame.onload = x);
+ }""",
+ server.EMPTY_PAGE,
+ )
+ assert len(page.frames) == 2
+ assert page.frames[1].url == server.EMPTY_PAGE
+
+
+async def test_frame_name(page: Page, server: Server, utils: Utils) -> None:
+ await utils.attach_frame(page, "theFrameId", server.EMPTY_PAGE)
+ await page.evaluate(
+ """url => {
+ frame = document.createElement('iframe');
+ frame.name = 'theFrameName';
+ frame.src = url;
+ document.body.appendChild(frame);
+ return new Promise(x => frame.onload = x);
+ }""",
+ server.EMPTY_PAGE,
+ )
+ assert page.frames[0].name == ""
+ assert page.frames[1].name == "theFrameId"
+ assert page.frames[2].name == "theFrameName"
+
+
+async def test_frame_parent(page: Page, server: Server, utils: Utils) -> None:
+ await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ await utils.attach_frame(page, "frame2", server.EMPTY_PAGE)
+ assert page.frames[0].parent_frame is None
+ assert page.frames[1].parent_frame == page.main_frame
+ assert page.frames[2].parent_frame == page.main_frame
+
+
+async def test_should_report_different_frame_instance_when_frame_re_attaches(
+ page: Page, server: Server, utils: Utils
+) -> None:
+ frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE)
+ await page.evaluate(
+ """() => {
+ window.frame = document.querySelector('#frame1')
+ window.frame.remove()
+ }"""
+ )
+
+ assert frame1.is_detached()
+ async with page.expect_event("frameattached") as frame2_info:
+ await page.evaluate("() => document.body.appendChild(window.frame)")
+
+ frame2 = await frame2_info.value
+ assert frame2.is_detached() is False
+ assert frame1 != frame2
+
+
+async def test_strict_mode(page: Page, server: Server) -> None:
+ await page.goto(server.EMPTY_PAGE)
+ await page.set_content(
+ """
+ Hello
+ Hello
+ """
+ )
+ with pytest.raises(Error):
+ await page.text_content("button", strict=True)
+ with pytest.raises(Error):
+ await page.query_selector("button", strict=True)
diff --git a/tests/async_imp/utils.py b/tests/async_imp/utils.py
new file mode 100644
index 0000000..546952d
--- /dev/null
+++ b/tests/async_imp/utils.py
@@ -0,0 +1,75 @@
+# Copyright (c) Microsoft Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+from typing import Any, List, cast
+
+from playwright.async_api import (
+ ElementHandle,
+ Error,
+ Frame,
+ Page,
+ Selectors,
+ ViewportSize,
+)
+
+
+class Utils:
+ async def attach_frame(self, page: Page, frame_id: str, url: str) -> Frame:
+ handle = await page.evaluate_handle(
+ """async ({ frame_id, url }) => {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ frame.id = frame_id;
+ document.body.appendChild(frame);
+ await new Promise(x => frame.onload = x);
+ return frame;
+ }""",
+ {"frame_id": frame_id, "url": url},
+ )
+ frame = await cast(ElementHandle, handle.as_element()).content_frame()
+ assert frame
+ return frame
+
+ async def detach_frame(self, page: Page, frame_id: str) -> None:
+ await page.evaluate("frame_id => document.getElementById(frame_id).remove()", frame_id)
+
+ def dump_frames(self, frame: Frame, indentation: str = "") -> List[str]:
+ indentation = indentation or ""
+ description = re.sub(r":\d+/", ":/", frame.url)
+ if frame.name:
+ description += " (" + frame.name + ")"
+ result = [indentation + description]
+ sorted_frames = sorted(frame.child_frames, key=lambda frame: frame.url + frame.name)
+ for child in sorted_frames:
+ result = result + utils.dump_frames(child, " " + indentation)
+ return result
+
+ async def verify_viewport(self, page: Page, width: int, height: int) -> None:
+ assert cast(ViewportSize, page.viewport_size)["width"] == width
+ assert cast(ViewportSize, page.viewport_size)["height"] == height
+ assert await page.evaluate("window.innerWidth") == width
+ assert await page.evaluate("window.innerHeight") == height
+
+ async def register_selector_engine(
+ self, selectors: Selectors, *args: Any, **kwargs: Any
+ ) -> None:
+ try:
+ await selectors.register(*args, **kwargs)
+ except Error as exc:
+ if "has been already registered" not in exc.message:
+ raise exc
+
+
+utils = Utils()
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..e0e00ad
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,302 @@
+# 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 inspect
+import io
+import json
+import os
+import subprocess
+import sys
+from pathlib import Path
+from typing import Any, AsyncGenerator, Callable, Dict, Generator, List, Optional, cast
+
+import playwright
+import playwright._impl._path_utils
+import pytest
+from PIL import Image
+from pixelmatch import pixelmatch
+from pixelmatch.contrib.PIL import from_PIL_to_raw_data
+from playwright._impl._path_utils import get_file_dirname
+
+from .server import Server, test_server
+
+_dirname = get_file_dirname()
+
+
+"""
+Patch playwright to not rely on module path for assets.
+"""
+
+original_get_file_dirname = playwright._impl._path_utils.get_file_dirname
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_configure(config):
+ def patched_get_file_dirname():
+ return _dirname
+
+ playwright._impl._path_utils.get_file_dirname = patched_get_file_dirname
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_unconfigure(config):
+ playwright._impl._path_utils.get_file_dirname = original_get_file_dirname
+
+
+def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
+ if "browser_name" in metafunc.fixturenames:
+ browsers = ["firefox"]
+ metafunc.parametrize("browser_name", browsers, scope="session")
+
+
+"""
+Playwright fixtures.
+"""
+
+
+@pytest.fixture(scope="session")
+def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
+ loop = asyncio.get_event_loop()
+ yield loop
+ loop.close()
+
+
+@pytest.fixture(scope="session")
+def assetdir() -> Path:
+ return _dirname / "assets"
+
+
+@pytest.fixture(scope="session")
+def headless(pytestconfig: pytest.Config) -> bool:
+ return pytestconfig.getoption("--headless") or os.getenv("HEADLESS", False)
+
+
+@pytest.fixture(scope="session")
+def launch_arguments(pytestconfig: pytest.Config, headless: bool) -> Dict:
+ args = {
+ "headless": headless,
+ "channel": pytestconfig.getoption("--browser-channel"),
+ }
+ executable_path = os.getenv("CAMOUFOX_EXECUTABLE_PATH", None)
+ if executable_path:
+ args["executable_path"] = os.path.abspath(executable_path)
+ return args
+
+
+@pytest.fixture
+def server() -> Generator[Server, None, None]:
+ yield test_server.server
+
+
+@pytest.fixture
+def https_server() -> Generator[Server, None, None]:
+ yield test_server.https_server
+
+
+@pytest.fixture(autouse=True, scope="session")
+async def start_server() -> AsyncGenerator[None, None]:
+ test_server.start()
+ yield
+ test_server.stop()
+
+
+@pytest.fixture(autouse=True)
+def after_each_hook() -> Generator[None, None, None]:
+ yield
+ test_server.reset()
+
+
+@pytest.fixture(scope="session")
+def browser_name(pytestconfig: pytest.Config) -> str:
+ # Always use Firefox
+ return 'firefox'
+
+
+@pytest.fixture(scope="session")
+def browser_channel(pytestconfig: pytest.Config) -> Optional[str]:
+ return cast(Optional[str], pytestconfig.getoption("--browser-channel"))
+
+
+@pytest.fixture(scope="session")
+def is_webkit(browser_name: str) -> bool:
+ return browser_name == "webkit"
+
+
+@pytest.fixture(scope="session")
+def is_firefox(browser_name: str) -> bool:
+ return browser_name == "firefox"
+
+
+@pytest.fixture(scope="session")
+def is_chromium(browser_name: str) -> bool:
+ return browser_name == "chromium"
+
+
+@pytest.fixture(scope="session")
+def is_win() -> bool:
+ return sys.platform == "win32"
+
+
+@pytest.fixture(scope="session")
+def is_linux() -> bool:
+ return sys.platform == "linux"
+
+
+@pytest.fixture(scope="session")
+def is_mac() -> bool:
+ return sys.platform == "darwin"
+
+
+"""
+Helper to skip tests by browser or platform.
+"""
+
+
+def _get_skiplist(request: pytest.FixtureRequest, values: List[str], value_name: str) -> List[str]:
+ skipped_values = []
+ # Allowlist
+ only_marker = request.node.get_closest_marker(f"only_{value_name}")
+ if only_marker:
+ skipped_values = values
+ skipped_values.remove(only_marker.args[0])
+
+ # Denylist
+ skip_marker = request.node.get_closest_marker(f"skip_{value_name}")
+ if skip_marker:
+ skipped_values.append(skip_marker.args[0])
+
+ return skipped_values
+
+
+@pytest.fixture(autouse=True)
+def skip_by_browser(request: pytest.FixtureRequest, browser_name: str) -> None:
+ skip_browsers_names = _get_skiplist(request, ["chromium", "firefox", "webkit"], "browser")
+
+ if browser_name in skip_browsers_names:
+ pytest.skip(f"skipped for this browser: {browser_name}")
+
+
+@pytest.fixture(autouse=True)
+def skip_by_platform(request: pytest.FixtureRequest) -> None:
+ skip_platform_names = _get_skiplist(request, ["win32", "linux", "darwin"], "platform")
+
+ if sys.platform in skip_platform_names:
+ pytest.skip(f"skipped on this platform: {sys.platform}")
+
+
+def pytest_addoption(parser: pytest.Parser) -> None:
+ group = parser.getgroup("playwright", "Playwright")
+ parser.addoption(
+ "--headless",
+ action="store_true",
+ default=False,
+ help="Run tests in headless mode.",
+ )
+ group.addoption(
+ "--browser-channel",
+ action="store",
+ default=None,
+ help="Browser channel to be used.",
+ )
+
+
+@pytest.fixture(scope="session")
+def assert_to_be_golden(browser_name: str) -> Callable[[bytes, str], None]:
+ def compare(received_raw: bytes, golden_name: str) -> None:
+ golden_file_path = _dirname / f"golden-{browser_name}" / golden_name
+ try:
+ golden_file = golden_file_path.read_bytes()
+ received_image = Image.open(io.BytesIO(received_raw))
+ golden_image = Image.open(io.BytesIO(golden_file))
+
+ if golden_image.size != received_image.size:
+ pytest.fail("Image size differs to golden image")
+ return
+ diff_pixels = pixelmatch(
+ from_PIL_to_raw_data(received_image),
+ from_PIL_to_raw_data(golden_image),
+ golden_image.size[0],
+ golden_image.size[1],
+ threshold=0.2,
+ )
+ assert diff_pixels == 0
+ except Exception:
+ if os.getenv("PW_WRITE_SCREENSHOT"):
+ golden_file_path.parent.mkdir(parents=True, exist_ok=True)
+ golden_file_path.write_bytes(received_raw)
+ print(f"Wrote {golden_file_path}")
+ raise
+
+ return compare
+
+
+class RemoteServer:
+ def __init__(self, browser_name: str, launch_server_options: Dict, tmpfile: Path) -> None:
+ driver_dir = Path(inspect.getfile(playwright)).parent / "driver"
+ if sys.platform == "win32":
+ node_executable = driver_dir / "node.exe"
+ else:
+ node_executable = driver_dir / "node"
+ cli_js = driver_dir / "package" / "cli.js"
+ tmpfile.write_text(json.dumps(launch_server_options))
+ self.process = subprocess.Popen(
+ [
+ str(node_executable),
+ str(cli_js),
+ "launch-server",
+ "--browser",
+ browser_name,
+ "--config",
+ str(tmpfile),
+ ],
+ stdout=subprocess.PIPE,
+ stderr=sys.stderr,
+ cwd=driver_dir,
+ )
+ assert self.process.stdout
+ self.ws_endpoint = self.process.stdout.readline().decode().strip()
+ self.process.stdout.close()
+
+ def kill(self) -> None:
+ # Send the signal to all the process groups
+ if self.process.poll() is not None:
+ return
+ self.process.kill()
+ self.process.wait()
+
+
+@pytest.fixture
+def launch_server(
+ browser_name: str, launch_arguments: Dict, tmp_path: Path
+) -> Generator[Callable[..., RemoteServer], None, None]:
+ remotes: List[RemoteServer] = []
+
+ def _launch_server(**kwargs: Dict[str, Any]) -> RemoteServer:
+ remote = RemoteServer(
+ browser_name,
+ {
+ **launch_arguments,
+ **kwargs,
+ },
+ tmp_path / f"settings-{len(remotes)}.json",
+ )
+ remotes.append(remote)
+ return remote
+
+ yield _launch_server
+
+ for remote in remotes:
+ remote.kill()
+ remote.kill()
diff --git a/tests/golden-firefox/grid-cell-0.png b/tests/golden-firefox/grid-cell-0.png
new file mode 100644
index 0000000..4677bdb
Binary files /dev/null and b/tests/golden-firefox/grid-cell-0.png differ
diff --git a/tests/golden-firefox/mask-should-work-with-element-handle.png b/tests/golden-firefox/mask-should-work-with-element-handle.png
new file mode 100644
index 0000000..682da85
Binary files /dev/null and b/tests/golden-firefox/mask-should-work-with-element-handle.png differ
diff --git a/tests/golden-firefox/mask-should-work-with-locator.png b/tests/golden-firefox/mask-should-work-with-locator.png
new file mode 100644
index 0000000..682da85
Binary files /dev/null and b/tests/golden-firefox/mask-should-work-with-locator.png differ
diff --git a/tests/golden-firefox/mask-should-work-with-page.png b/tests/golden-firefox/mask-should-work-with-page.png
new file mode 100644
index 0000000..720828e
Binary files /dev/null and b/tests/golden-firefox/mask-should-work-with-page.png differ
diff --git a/tests/golden-firefox/mock-binary-response.png b/tests/golden-firefox/mock-binary-response.png
new file mode 100644
index 0000000..e7eaf59
Binary files /dev/null and b/tests/golden-firefox/mock-binary-response.png differ
diff --git a/tests/golden-firefox/mock-svg.png b/tests/golden-firefox/mock-svg.png
new file mode 100644
index 0000000..a9ee147
Binary files /dev/null and b/tests/golden-firefox/mock-svg.png differ
diff --git a/tests/golden-firefox/screenshot-element-bounding-box.png b/tests/golden-firefox/screenshot-element-bounding-box.png
new file mode 100644
index 0000000..9e208f8
Binary files /dev/null and b/tests/golden-firefox/screenshot-element-bounding-box.png differ
diff --git a/tests/golden-firefox/screenshot-sanity.png b/tests/golden-firefox/screenshot-sanity.png
new file mode 100644
index 0000000..ecab61f
Binary files /dev/null and b/tests/golden-firefox/screenshot-sanity.png differ
diff --git a/tests/local-requirements.txt b/tests/local-requirements.txt
new file mode 100644
index 0000000..92e78ae
--- /dev/null
+++ b/tests/local-requirements.txt
@@ -0,0 +1,25 @@
+playwright
+auditwheel==6.1.0
+autobahn==23.1.2
+black==24.8.0
+flake8==7.1.1
+flaky==3.8.1
+mypy==1.11.1
+objgraph==3.6.1
+Pillow==10.4.0
+pixelmatch==0.3.0
+pre-commit==3.4.0
+pyOpenSSL==24.2.1
+pytest==8.3.2
+pytest-asyncio==0.21.2
+pytest-cov==5.0.0
+pytest-repeat==0.9.3
+pytest-timeout==2.3.1
+pytest-xdist==3.6.1
+requests==2.32.3
+service_identity==24.1.0
+setuptools==72.1.0
+twisted==24.7.0
+types-pyOpenSSL==24.1.0.20240722
+types-requests==2.32.0.20240712
+wheel==0.42.0
diff --git a/tests/pyproject.toml b/tests/pyproject.toml
new file mode 100644
index 0000000..4878d64
--- /dev/null
+++ b/tests/pyproject.toml
@@ -0,0 +1,19 @@
+[tool.pytest.ini_options]
+addopts = "-Wall -rsx -vv -s"
+markers = [
+ "skip_browser",
+ "only_browser",
+ "skip_platform",
+ "only_platform"
+]
+junit_family = "xunit2"
+asyncio_mode = "auto"
+
+[tool.pyright]
+pythonVersion = "3.8"
+reportMissingImports = false
+reportTypedDictNotRequiredAccess = false
+reportCallInDefaultInitializer = true
+reportOptionalSubscript = false
+reportUnboundVariable = false
+strictParameterNoneValue = false
diff --git a/tests/run-tests.sh b/tests/run-tests.sh
new file mode 100644
index 0000000..17821ab
--- /dev/null
+++ b/tests/run-tests.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+
+# Function to check if an argument is valid
+check_arg() {
+ case "$1" in
+ --headful)
+ return 0
+ ;;
+ --executable-path)
+ if [ -z "$2" ]; then
+ echo "Error: --executable-path requires a path argument"
+ return 1
+ elif [ ! -e "$2" ]; then
+ echo "Error: The path specified for --executable-path does not exist: $2"
+ return 1
+ fi
+ return 0
+ ;;
+ *)
+ echo "Error: Invalid argument '$1'. Only --headful and --executable-path are allowed."
+ return 1
+ ;;
+ esac
+}
+
+# Check if venv exists, if not run setup-venv.sh
+if [ ! -d "venv" ]; then
+ echo "Virtual environment not found. Running setup-venv.sh..."
+ bash ./setup-venv.sh
+ if [ $? -ne 0 ]; then
+ echo "Failed to set up virtual environment. Exiting."
+ exit 1
+ fi
+fi
+
+# Validate arguments
+VALID_ARGS=("--headless") # Set headless as default
+i=1
+while [ $i -le $# ]; do
+ arg="$1"
+ case "$arg" in
+ --executable-path)
+ shift # move to the next argument
+ if [ -z "$1" ]; then
+ echo "Error: --executable-path requires a path argument"
+ exit 1
+ fi
+ if check_arg "--executable-path" "$1"; then
+ export CAMOUFOX_EXECUTABLE_PATH="$1"
+ shift
+ else
+ exit 1
+ fi
+ ;;
+ --headful)
+ if check_arg "$arg"; then
+ VALID_ARGS=() # Remove default --headless
+ shift
+ else
+ exit 1
+ fi
+ ;;
+ *)
+ echo "Error: Invalid argument '$arg'. Only --headful and --executable-path are allowed."
+ exit 1
+ ;;
+ esac
+done
+
+# Run pytest with validated arguments
+echo "Running pytest with arguments: ${VALID_ARGS[@]}"
+if [ -n "$CAMOUFOX_EXECUTABLE_PATH" ]; then
+ echo "CAMOUFOX_EXECUTABLE_PATH set to: $CAMOUFOX_EXECUTABLE_PATH"
+fi
+
+echo venv/bin/pytest -vv "${VALID_ARGS[@]}" async/
+venv/bin/pytest -vv "${VALID_ARGS[@]}" async/
\ No newline at end of file
diff --git a/tests/server.py b/tests/server.py
new file mode 100644
index 0000000..1366345
--- /dev/null
+++ b/tests/server.py
@@ -0,0 +1,299 @@
+# 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 abc
+import asyncio
+import contextlib
+import gzip
+import mimetypes
+import os
+import socket
+import threading
+from contextlib import closing
+from http import HTTPStatus
+from pathlib import Path
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Generator,
+ Generic,
+ List,
+ Optional,
+ Set,
+ Tuple,
+ TypeVar,
+ cast,
+)
+from urllib.parse import urlparse
+
+from autobahn.twisted.resource import WebSocketResource
+from autobahn.twisted.websocket import WebSocketServerFactory, WebSocketServerProtocol
+from OpenSSL import crypto
+from twisted.internet import reactor as _twisted_reactor
+from twisted.internet import ssl
+from twisted.internet.selectreactor import SelectReactor
+from twisted.web import http
+
+_dirname = Path(os.path.abspath(__file__)).parent
+reactor = cast(SelectReactor, _twisted_reactor)
+
+
+def find_free_port() -> int:
+ with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
+ s.bind(("", 0))
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ return s.getsockname()[1]
+
+
+T = TypeVar("T")
+
+
+class ExpectResponse(Generic[T]):
+ def __init__(self) -> None:
+ self._value: T
+
+ @property
+ def value(self) -> T:
+ if not hasattr(self, "_value"):
+ raise ValueError("no received value")
+ return self._value
+
+
+class TestServerRequest(http.Request):
+ __test__ = False
+ channel: "TestServerHTTPChannel"
+ post_body: Optional[bytes] = None
+
+ def process(self) -> None:
+ server = self.channel.factory.server_instance
+ if self.content:
+ self.post_body = self.content.read()
+ self.content.seek(0, 0)
+ else:
+ self.post_body = None
+ uri = urlparse(self.uri.decode())
+ path = uri.path
+
+ request_subscriber = server.request_subscribers.get(path)
+ if request_subscriber:
+ request_subscriber._loop.call_soon_threadsafe(request_subscriber.set_result, self)
+ server.request_subscribers.pop(path)
+
+ if path == "/ws":
+ server._ws_resource.render(self)
+ return
+
+ if server.auth.get(path):
+ authorization_header = self.requestHeaders.getRawHeaders("authorization")
+ creds_correct = False
+ if authorization_header:
+ creds_correct = server.auth.get(path) == (
+ self.getUser().decode(),
+ self.getPassword().decode(),
+ )
+ if not creds_correct:
+ self.setHeader(b"www-authenticate", 'Basic realm="Secure Area"')
+ self.setResponseCode(HTTPStatus.UNAUTHORIZED)
+ self.finish()
+ return
+ if server.csp.get(path):
+ self.setHeader(b"Content-Security-Policy", server.csp[path])
+ if server.routes.get(path):
+ server.routes[path](self)
+ return
+ file_content = None
+ try:
+ file_content = (server.static_path / path[1:]).read_bytes()
+ content_type = mimetypes.guess_type(path)[0]
+ if content_type and content_type.startswith("text/"):
+ content_type += "; charset=utf-8"
+ self.setHeader(b"Content-Type", content_type)
+ self.setHeader(b"Cache-Control", "no-cache, no-store")
+ if path in server.gzip_routes:
+ self.setHeader("Content-Encoding", "gzip")
+ self.write(gzip.compress(file_content))
+ else:
+ self.setHeader(b"Content-Length", str(len(file_content)))
+ self.write(file_content)
+ self.setResponseCode(HTTPStatus.OK)
+ except (FileNotFoundError, IsADirectoryError, PermissionError):
+ self.setResponseCode(HTTPStatus.NOT_FOUND)
+ self.finish()
+
+
+class TestServerHTTPChannel(http.HTTPChannel):
+ factory: "TestServerFactory"
+ requestFactory = TestServerRequest
+
+
+class TestServerFactory(http.HTTPFactory):
+ server_instance: "Server"
+ protocol = TestServerHTTPChannel
+
+
+class Server:
+ protocol = "http"
+
+ def __init__(self) -> None:
+ self.PORT = find_free_port()
+ self.EMPTY_PAGE = f"{self.protocol}://localhost:{self.PORT}/empty.html"
+ self.PREFIX = f"{self.protocol}://localhost:{self.PORT}"
+ self.CROSS_PROCESS_PREFIX = f"{self.protocol}://127.0.0.1:{self.PORT}"
+ # On Windows, this list can be empty, reporting text/plain for scripts.
+ mimetypes.add_type("text/html", ".html")
+ mimetypes.add_type("text/css", ".css")
+ mimetypes.add_type("application/javascript", ".js")
+ mimetypes.add_type("image/png", ".png")
+ mimetypes.add_type("font/woff2", ".woff2")
+
+ def __repr__(self) -> str:
+ return self.PREFIX
+
+ @abc.abstractmethod
+ def listen(self, factory: TestServerFactory) -> None:
+ pass
+
+ def start(self) -> None:
+ request_subscribers: Dict[str, asyncio.Future] = {}
+ auth: Dict[str, Tuple[str, str]] = {}
+ csp: Dict[str, str] = {}
+ routes: Dict[str, Callable[[TestServerRequest], Any]] = {}
+ gzip_routes: Set[str] = set()
+ self.request_subscribers = request_subscribers
+ self.auth = auth
+ self.csp = csp
+ self.routes = routes
+ self._ws_handlers: List[Callable[["WebSocketProtocol"], None]] = []
+ self.gzip_routes = gzip_routes
+ self.static_path = _dirname / "assets"
+ factory = TestServerFactory()
+ factory.server_instance = self
+
+ ws_factory = WebSocketServerFactory()
+ ws_factory.protocol = WebSocketProtocol
+ ws_factory.server_instance = self
+ self._ws_resource = WebSocketResource(ws_factory)
+
+ self.listen(factory)
+
+ async def wait_for_request(self, path: str) -> TestServerRequest:
+ if path in self.request_subscribers:
+ return await self.request_subscribers[path]
+ future: asyncio.Future["TestServerRequest"] = asyncio.Future()
+ self.request_subscribers[path] = future
+ return await future
+
+ @contextlib.contextmanager
+ def expect_request(self, path: str) -> Generator[ExpectResponse[TestServerRequest], None, None]:
+ future = asyncio.create_task(self.wait_for_request(path))
+
+ cb_wrapper: ExpectResponse[TestServerRequest] = ExpectResponse()
+
+ def done_cb(task: asyncio.Task) -> None:
+ cb_wrapper._value = future.result()
+
+ future.add_done_callback(done_cb)
+ yield cb_wrapper
+
+ def set_auth(self, path: str, username: str, password: str) -> None:
+ self.auth[path] = (username, password)
+
+ def set_csp(self, path: str, value: str) -> None:
+ self.csp[path] = value
+
+ def reset(self) -> None:
+ self.request_subscribers.clear()
+ self.auth.clear()
+ self.csp.clear()
+ self.gzip_routes.clear()
+ self.routes.clear()
+ self._ws_handlers.clear()
+
+ def set_route(self, path: str, callback: Callable[[TestServerRequest], Any]) -> None:
+ self.routes[path] = callback
+
+ def enable_gzip(self, path: str) -> None:
+ self.gzip_routes.add(path)
+
+ def set_redirect(self, from_: str, to: str) -> None:
+ def handle_redirect(request: http.Request) -> None:
+ request.setResponseCode(HTTPStatus.FOUND)
+ request.setHeader("location", to)
+ request.finish()
+
+ self.set_route(from_, handle_redirect)
+
+ def send_on_web_socket_connection(self, data: bytes) -> None:
+ self.once_web_socket_connection(lambda ws: ws.sendMessage(data))
+
+ def once_web_socket_connection(self, handler: Callable[["WebSocketProtocol"], None]) -> None:
+ self._ws_handlers.append(handler)
+
+
+class HTTPServer(Server):
+ def listen(self, factory: http.HTTPFactory) -> None:
+ reactor.listenTCP(self.PORT, factory, interface="127.0.0.1")
+ try:
+ reactor.listenTCP(self.PORT, factory, interface="::1")
+ except Exception:
+ pass
+
+
+class HTTPSServer(Server):
+ protocol = "https"
+
+ def listen(self, factory: http.HTTPFactory) -> None:
+ cert = ssl.PrivateCertificate.fromCertificateAndKeyPair(
+ ssl.Certificate.loadPEM((_dirname / "testserver" / "cert.pem").read_bytes()),
+ ssl.KeyPair.load(
+ (_dirname / "testserver" / "key.pem").read_bytes(), crypto.FILETYPE_PEM
+ ),
+ )
+ contextFactory = cert.options()
+ reactor.listenSSL(self.PORT, factory, contextFactory, interface="127.0.0.1")
+ try:
+ reactor.listenSSL(self.PORT, factory, contextFactory, interface="::1")
+ except Exception:
+ pass
+
+
+class WebSocketProtocol(WebSocketServerProtocol):
+ def onOpen(self) -> None:
+ for handler in self.factory.server_instance._ws_handlers.copy():
+ self.factory.server_instance._ws_handlers.remove(handler)
+ handler(self)
+
+
+class TestServer:
+ def __init__(self) -> None:
+ self.server = HTTPServer()
+ self.https_server = HTTPSServer()
+
+ def start(self) -> None:
+ self.server.start()
+ self.https_server.start()
+ self.thread = threading.Thread(target=lambda: reactor.run(installSignalHandlers=False))
+ self.thread.start()
+
+ def stop(self) -> None:
+ reactor.stop()
+ self.thread.join()
+
+ def reset(self) -> None:
+ self.server.reset()
+ self.https_server.reset()
+
+
+test_server = TestServer()
diff --git a/tests/setup-venv.sh b/tests/setup-venv.sh
new file mode 100644
index 0000000..0a4fb47
--- /dev/null
+++ b/tests/setup-venv.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+python3 -m venv venv
+venv/bin/pip3 install -r local-requirements.txt
+
+# Install Firefox as well (Playwright freaks out when it's missing)
+venv/bin/playwright install firefox
diff --git a/tests/testserver/cert.pem b/tests/testserver/cert.pem
new file mode 100644
index 0000000..fc692a6
--- /dev/null
+++ b/tests/testserver/cert.pem
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIEsjCCApoCCQCIPLvQDgoZojANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9w
+dXBwZXRlZXItdGVzdHMwIBcNMTkwMjEzMTkwNzQzWhgPMzAxODA2MTYxOTA3NDNa
+MBoxGDAWBgNVBAMMD3B1cHBldGVlci10ZXN0czCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAJue1yqA4qn0SJR3rgTd6sCYVHMKqUouD0No09H7qf+5ZaIb
+3yGpC5J9Bsf/ZbvD5xpgqbGEYkHj7Qh6Z/cPCSHA+ZpsUzDXVrLFXrdwwiK1FrIS
+rDI2RYsiP+e52XPC/acWC/7f+E54C62oMjYojaVaDn8gu06gyS1rXK2JITQ6CrKn
+b+PVSkjtPB4ku245u1qCKoblkNEZSkEmw8Csl+gw6ydGqOSQAoo8rsDte5zCMnPX
+7XzL6EhRqpiVx7PCuQWnXhL7j9N214Pit7s7F8TeAA6yZR9oswW+h0dWO+XwocJ1
+rwkODXOngbCqO+GUxyuavIl2m0d2MP8n6Wa9RVqYetmPQzafKkR5hjiV4mgCFqNQ
+bHMTjI6udcR+h5pYoWKxN9/gJaWwyAAzck0AiMeGVrvKR3JKACqlTMzy/Y30obRF
+dddURoFf2wjKJvuTK9hHI7pwM5tlPEwu9bTCWNA6XXs2Bq1f6N2OAKhpKOcihNem
+aeGUPmygLPb66z9JO75yZXM+1yk1ScXaNHWZLmluVpEPk7maWULpSpxPAlaN3PmK
+8lEihgfBBovampxZo8SvPEt+g5jGyPq9weNg8ic8476PuRVQdg7D8spVxl6whDlJ
+bcFojzgrX70t13jqZOtla4WK1vRnZAGplfoH0i5WvAVw+i5S/OVzsmNDtGFbAgMB
+AAEwDQYJKoZIhvcNAQELBQADggIBADUAjA/dH+b5UxDC5SL98w1hphw9PvD1cuGS
+sVnKPM236JoTiO3KVfm3NMBfSoBi1hPNkXzqr/R4xbyje4Kc4oYcdjGtpll3T5da
+wkx1+qumx6O2mEaOshxh76dfZfZne6SQphQKHw8PD10CfDb/NMnmdEbiOSENSqS4
+jGELuGviUl361oCBU45UEN7lfs7ANAhwSZyEO7deroyGdvsxfQUaqQrEQsG30jn3
+t0cCamYU6eK3bNR/yNXJrZFv3dzoquRY9H52YtVElRqdAIsNlnbxbqz0cm5xFKFt
+YTIrMSO1EvDTbB0PPwC5FJvONHhjwiWzgVXSnZrcs/05TsWWnSHH92S+wGCIBC+0
+6fcSKnjdBn9ks5TrDX0TRY6N890KyDQWxPRhHYrMVpn833WY8y/SguxqiMgLFgMD
+WLy6yZzJloW7NgpLGAfMA0nMG1O92hfKmQw82Pyf3SVXGTDiXiEOXn0vN6bsPaV/
+3Ws2LJQECnVfHj3TsuxdtwcO+VGcFCarMOqlhE6IlQzfK8ykYdP6wCkVgXEtiVCR
+T1OWUWCFowoFpwBFLf1lA065qsAymddnkrUEOMiScZ/3OZhmd+FvgQ+O0iYuqpeI
+xauiQ68+Jb4KjVWnu5QBVq8n1vUJ5+gAzowNMN9G+1+A282Ox23T48dce22BTS6B
+3Taaccm+
+-----END CERTIFICATE-----
diff --git a/tests/testserver/key.pem b/tests/testserver/key.pem
new file mode 100644
index 0000000..e2ed680
--- /dev/null
+++ b/tests/testserver/key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCbntcqgOKp9EiU
+d64E3erAmFRzCqlKLg9DaNPR+6n/uWWiG98hqQuSfQbH/2W7w+caYKmxhGJB4+0I
+emf3DwkhwPmabFMw11ayxV63cMIitRayEqwyNkWLIj/nudlzwv2nFgv+3/hOeAut
+qDI2KI2lWg5/ILtOoMkta1ytiSE0Ogqyp2/j1UpI7TweJLtuObtagiqG5ZDRGUpB
+JsPArJfoMOsnRqjkkAKKPK7A7XucwjJz1+18y+hIUaqYlcezwrkFp14S+4/TdteD
+4re7OxfE3gAOsmUfaLMFvodHVjvl8KHCda8JDg1zp4GwqjvhlMcrmryJdptHdjD/
+J+lmvUVamHrZj0M2nypEeYY4leJoAhajUGxzE4yOrnXEfoeaWKFisTff4CWlsMgA
+M3JNAIjHhla7ykdySgAqpUzM8v2N9KG0RXXXVEaBX9sIyib7kyvYRyO6cDObZTxM
+LvW0wljQOl17NgatX+jdjgCoaSjnIoTXpmnhlD5soCz2+us/STu+cmVzPtcpNUnF
+2jR1mS5pblaRD5O5mllC6UqcTwJWjdz5ivJRIoYHwQaL2pqcWaPErzxLfoOYxsj6
+vcHjYPInPOO+j7kVUHYOw/LKVcZesIQ5SW3BaI84K1+9Ldd46mTrZWuFitb0Z2QB
+qZX6B9IuVrwFcPouUvzlc7JjQ7RhWwIDAQABAoICAFUvM5SejHR/taMfh/A+EZxv
+RfrbISPr5or9vMU6vymuMIX2P8PLJvx+19Fuah/H8p8rvnffgXGT9FIpvvMsFdGW
+MotnNHqNxXWCOICthnc9LTk4o22w64xnqReNUgzd9b8agGJ58w/xAmOCqEmhFTgn
+/bt1DVLTDIyCMm8Dm1tdUjHNGaBbRph40+mkLbz+eSHoEqNY0lbDQzQ6pfi4AUcm
+T/Jl6VmDwwAsi3QsCvgaDUgAMI2ZiILdwUZY5sHtmx4PKZ22elpEuWAGIJCqni4z
+X1CsMlJpG2XPj3lrKMqLV+B8Tt3kBVUDoig0ZybqK8QgpYeRlxodBmEFVevZOzar
+r/qDRh+vrQQxjpFYfrMkPiueRmz0+K1a7KiKSmrjHIb9CTi3BpgEhawbsOB7M+9z
+G5Q7YtGbVyJPmEAAva89ZZqYvyAwxZk3V4pwpoUYzjgPiHm6Oq0vzKPuCgQxsYzx
+UrCVRo7pSE4tTin4SRThY2/yHiMJl8QY//MkahgY8KEHtXE36km6pMRH/ssdSm+C
+SNCOtzUDY8wpaDQ++aB29NWqgnSgwoBrRUXr5NHq+wNpWtmD7L0wDSKUCPfiCduR
+DoSHBIno5U2jgPrH5Wk7X7loG2XxiDR0qtNOiH24SCI+C1nsLRGBS2Tmo0Qby/Ll
+oYYCZ0U3S7wk9UY5HcuRAoIBAQDIl8HTaIuzyOrrpsRdUv8jAxnkdhVjYhWGp7mU
+5concRazcEO5/vDJlsIuQI/w7U+xS7PCBPRq7NxUtaUntlQ00s224Ws0sPHIWUD0
+NBsodTX8hik2PdmZ5ZbBHVaeVbMV/5zV8eOPGGCsAn//7l4YIp2I2Abs79leqSDI
+7tBpF4IsUq7xqcVZ1QhWBZmTqE4gYDVqFEVe9O6OmAdkM1qxVSur2E5Ib+islnu8
+yKlu0QlXg596zLVAjxajKYf20NXxh7O+xt5QDEy3dmJEhz5viS6eI7QECM7Lid2T
+c22mABSKzYfbwQroM9yBiI3p0zjwRhha0hRKocLkSiNUlOWHAoIBAQDGmwRM6Xmu
+j7/lV3KvrOmvcNIbUwMbYY/ATY3Wuph6GFjwdliRiju9F3ZUHMV+yNlVDqH3DeRC
+QwGKIcFiVk+4fq1AbWVCWk2MOf49akeJwqFzgF8nkxVXF9PS73VdEvreSvy7g89t
+ABqJN6pmGHWVkE1mf/3LJyS2Y7WCZqSaWG3TBZ2SRb2t/t/DnX1L6tzNMkuNAizA
+sDB3J1hH1eGcWn/24NB9sc7i2Bk+Cpi0S/xDn9FfoBo5U0p/lpopgfFoSeQZXq1A
+KIQdtUPLp3KR9EG/ItimfW875zqFe8bekB9/gakyLsbIyINz1iQQS1L1FFmOO8zN
+RtRmm3MrG9qNAoIBAQC1v2rLFgqeVwkjgvKgbDbnjkPDkIpIhfJjE00+8AV+PyUG
+aE21FJ0uyf4e0jiZXyu5xJGW1c5vozTvO7XsiXM6eVYSwaPVFg28LcKAgUWqHqlP
+qG9myhuDKVaymtaEl7mv0O5VmtlIKhpNP+aiCWQQEi0SdEmyHI+jCTK/XEJRNg+o
+ATKpm91IS5FF/8Tq2LAQ/ZroBn3kT6BmarEnxLADxNvQ1Cf50gvLdH2gy19ZHOWN
++aBiL2B6oissotCifQ2bzgy6ao27kalhAU6AMNoNTQqEFm1gymo0WTH+C7PpmGEE
+cr0KC5rKUVMVuph6p/sLGTev8nCYPoDLP7FLTa25AoIBAGmP559B0c141pR+AJVj
+oOoBW4vueY5KMvARyLxDfdwXqN5W6QiiotIE8H4QtOCIvQu6tVftaE/X8a+L9Y/h
+NIppuoiuHM5B1UodYQcfwFp2uv37U5hjU0pxfcN2R7lq5zDURrUcgFn9Xh1lGwsd
+IRKYGqvKiAk9CwRuxwFCsWbgba9mIrSmoQUknacJxJlfgnEGtKWEbGkWvQv4O7Ii
++sHyUGXWZLsKkV59Yh1Z4ISkhrci8VSUcpvZq5VZZSN+z+OQss7RReD+KArqV9id
+bgYp//AqA2Gq9j6uzqo4eiG+FR/euSHVPw9llIkzXwPSJYvifx9cpaTOawMGyRY2
+vdkCggEBAJECl41qbQE7OIPsSmhcz0nK6L4aIkdOxZ6hs6xO7fPwh7EntojPIB6J
+bMuvfujqW4SZ+ZpwZkCc8p8j2VuQvlXLI+s6923IdYAOK5ND9q3Xj7AqgJjUKbhH
+lpYUtfDmIjqVADoiIYXmZBPAo7QvzkX7A2qclV7VL/Dc94bBS6M+v/JGh7QyTCsO
+oPK6IOlGL1yg+CdZIzdSiJKVeESPMOBhNtPhm+vOXvRV08ECEILD1j52rUKcPs+I
+uINxopeXePgekedm7nyAW3IMHFKa4EiuEU3LQOaWeKEnaxNdOh12Pyyd6w2iAmrr
+rj/p/2CWVN8OTi7CY5cOTCadHZRyjYA=
+-----END PRIVATE KEY-----
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 0000000..c6c10a8
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,78 @@
+# 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 json
+import zipfile
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple, TypeVar
+
+
+def parse_trace(path: Path) -> Tuple[Dict[str, bytes], List[Any]]:
+ resources: Dict[str, bytes] = {}
+ with zipfile.ZipFile(path, "r") as zip:
+ for name in zip.namelist():
+ resources[name] = zip.read(name)
+ action_map: Dict[str, Any] = {}
+ events: List[Any] = []
+ for name in ["trace.trace", "trace.network"]:
+ for line in resources[name].decode().splitlines():
+ if not line:
+ continue
+ event = json.loads(line)
+ if event["type"] == "before":
+ event["type"] = "action"
+ action_map[event["callId"]] = event
+ events.append(event)
+ elif event["type"] == "input":
+ pass
+ elif event["type"] == "after":
+ existing = action_map[event["callId"]]
+ existing["error"] = event.get("error", None)
+ else:
+ events.append(event)
+ return (resources, events)
+
+
+def get_trace_actions(events: List[Any]) -> List[str]:
+ action_events = sorted(
+ list(
+ filter(
+ lambda e: e["type"] == "action",
+ events,
+ )
+ ),
+ key=lambda e: e["startTime"],
+ )
+ return [e["apiName"] for e in action_events]
+
+
+TARGET_CLOSED_ERROR_MESSAGE = "Target page, context or browser has been closed"
+
+MustType = TypeVar("MustType")
+
+
+def must(value: Optional[MustType]) -> MustType:
+ assert value
+ return value
+
+
+def chromium_version_less_than(a: str, b: str) -> bool:
+ left = list(map(int, a.split(".")))
+ right = list(map(int, b.split(".")))
+ for i in range(4):
+ if left[i] > right[i]:
+ return False
+ if left[i] < right[i]:
+ return True
+ return False