Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DRIVER_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b4695e3eb2e6736d1a7b1fd255c4e94f78553660
f33f0b751be0dd559d86c1ed27ca8baa5f9cb2b2
2 changes: 1 addition & 1 deletion NODE_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24.22.1
26.13.0
4 changes: 4 additions & 0 deletions tests/assets/serviceworkers/slow-fallback/sw.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<script>
window.registrationPromise = navigator.serviceWorker.register('sw.js');
window.activationPromise = new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve);
</script>
9 changes: 9 additions & 0 deletions tests/assets/serviceworkers/slow-fallback/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
self.addEventListener('install', () => self.skipWaiting());

self.addEventListener('activate', event => event.waitUntil(self.clients.claim()));

self.addEventListener('fetch', () => {
// Hold the request for a while, then let it fall back to the network.
const start = Date.now();
while (Date.now() - start < 500) {}
});
47 changes: 47 additions & 0 deletions tests/async/library/chromium/test_connect_over_cdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,3 +904,50 @@ async def test_CD_035_no_defaults_should_not_affect_new_contexts(
await browser.close()
finally:
await browser_server.close()


async def test_CD_053_should_connect_when_another_client_inspects_a_terminated_shared_worker(
browser_type: BrowserType, server: Server, launch_arguments: Dict, port: int
) -> None:
pytest.skip("fixme")
browser = await launch(browser_type, launch_arguments, port)
try:
# Another client inspects the shared worker.
page = await browser.new_page()
inspector = await browser.new_browser_cdp_session()
worker_created: asyncio.Future[str] = asyncio.get_running_loop().create_future()

def on_target_created(event: Dict[str, Any]) -> None:
if event["targetInfo"]["type"] == "shared_worker":
worker_created.set_result(event["targetInfo"]["targetId"])

inspector.on("Target.targetCreated", on_target_created)
await inspector.send("Target.setDiscoverTargets", {"discover": True})
await page.goto(server.PREFIX + "/shared-worker/shared-worker.html")
assert await page.evaluate("window.sharedWorkerResponsePromise") == "echo:hello"
worker_target_id = await worker_created
await inspector.send(
"Target.attachToTarget", {"targetId": worker_target_id, "flatten": True}
)

# The last client of the shared worker goes away, so the worker is terminated.
# The inspector stays attached, so the worker target lingers without a browser context.
await page.close()
# Python: poll the CDP target list instead of expect.poll.
deadline = asyncio.get_running_loop().time() + 5
while True:
targets = await inspector.send("Target.getTargets")
if not any(
info["targetId"] == worker_target_id for info in targets["targetInfos"]
):
break
assert asyncio.get_running_loop().time() < deadline
await asyncio.sleep(0.1)

# A new client auto-attaches to the lingering worker target.
cdp_browser = await browser_type.connect_over_cdp(f"http://127.0.0.1:{port}/")
page2 = await cdp_browser.contexts[0].new_page()
assert await page2.evaluate("1 + 1") == 2
await cdp_browser.close()
finally:
await browser.close()
2 changes: 2 additions & 0 deletions tests/async/library/chromium/test_oopif.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,13 @@ async def test_OO_012_should_take_screenshot(
browser: Browser,
server: Server,
assert_to_be_golden: Callable[[bytes, str], None],
utils: Utils,
) -> None:
await page.set_viewport_size({"width": 500, "height": 500})
await page.goto(server.PREFIX + "/dynamic-oopif.html")
assert len(page.frames) == 2
await assert_oopif_count(browser, 1)
await utils.rafraf(page.frames[1])
assert_to_be_golden(await page.screenshot(), "screenshot-oopif.png")


Expand Down
19 changes: 19 additions & 0 deletions tests/async/library/test_browsercontext_har.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

"""Port of tests/library/browsercontext-har.spec.ts."""

import io
import json
import re
import zipfile
Expand Down Expand Up @@ -316,6 +317,24 @@ async def test_HA_085_should_round_trip_har_zip(
)


async def test_HA_114_should_reject_a_malformed_har_zip(
context_factory: Callable[..., Awaitable[BrowserContext]], tmp_path: Path
) -> None:
# Python: zipfile builds the in-memory archive instead of yazl's output stream.
output = io.BytesIO()
with zipfile.ZipFile(output, "w") as zip_file:
zip_file.writestr("aa/x.har", b"{}")
# yauzl reports '..' in an entry name as an 'error' event while reading entries.
zip_data = output.getvalue().replace(b"aa/x.har", b"../x.har")
har_path = tmp_path / "malformed.har.zip"
har_path.write_bytes(zip_data)

context = await context_factory()
with pytest.raises(Error) as exc_info:
await context.route_from_har(har_path)
assert "invalid relative path: ../x.har" in exc_info.value.message


async def test_HA_086_should_produce_extracted_zip(
context_factory: Callable[..., Awaitable[BrowserContext]],
server: Server,
Expand Down
21 changes: 20 additions & 1 deletion tests/async/library/test_browsercontext_user_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import pytest

from playwright.async_api import Browser, BrowserContext, Error, Playwright
from playwright.async_api import Browser, BrowserContext, Error, Playwright, Route
from tests.server import Server, TestServerRequest
from tests.utils import chromium_version_less_than, must

Expand All @@ -41,6 +41,25 @@ async def test_UA_001_should_work(browser: Browser, server: Server) -> None:
await context.close()


async def test_UA_007_should_preserve_custom_user_agent_in_route_fetch(
context_factory: Callable[..., Awaitable[BrowserContext]], server: Server
) -> None:
context = await context_factory(user_agent="my-custom-user-agent")
page = await context.new_page()

async def handle(route: Route) -> None:
await route.fulfill(response=await route.fetch())

await page.route("**/empty.html", handle)
request, server_request, _ = await asyncio.gather(
page.wait_for_event("request", predicate=lambda r: r.url == server.EMPTY_PAGE),
server.wait_for_request("/empty.html"),
page.goto(server.EMPTY_PAGE),
)
assert request.headers["user-agent"] == "my-custom-user-agent"
assert server_request.getHeader("user-agent") == "my-custom-user-agent"


async def test_UA_002_should_work_for_subframes(
browser: Browser, server: Server, utils: Utils
) -> None:
Expand Down
24 changes: 8 additions & 16 deletions tests/async/library/test_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,27 +183,19 @@ async def test_CA_011_should_not_crash_on_show_directory_picker(
await page.goto(server.EMPTY_PAGE)
# "User activation is required to show a file picker." - so we click first.
await page.locator("body").click()
picker = asyncio.create_task(
page.evaluate(
"""async () => {
const dir = await window.showDirectoryPicker();
return dir.name;
// In headless it throws (aborted), in headed it stalls (Test ended) and waits for the picker to be accepted.
await page.evaluate(
"""() => {
window.showDirectoryPicker().catch(e => { window.__pickerError = String(e); });
}"""
)
)
# The dialog will not be accepted, so we just wait for some time to
# to give the browser a chance to crash.
await page.wait_for_timeout(3_000)
if picker.done():
error = picker.exception()
assert isinstance(error, Error)
assert re.search(
r"(DOMException|AbortError): .*The user aborted a request", error.message
)
else:
# In headed mode the picker stalls until the test ends.
picker.add_done_callback(lambda task: task.exception())
# In headless it throws (aborted), in headed it stalls and waits for the picker to be accepted.
error = await page.evaluate("() => window.__pickerError ?? 'stalled'")
assert re.search(
r"((DOMException|AbortError): .*The user aborted a request|stalled)", error
)


async def test_CA_012_should_not_crash_on_storage_get_directory(
Expand Down
47 changes: 47 additions & 0 deletions tests/async/library/test_multiclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,3 +563,50 @@ async def test_MC_018_should_stop_tracing_upon_disconnect(
await disconnect(playwright, page_a)

await page_b.context.tracing.start()


@pytest.mark.slow
async def test_MC_020_should_run_locator_handlers_after_reconnecting_with_a_pending_callback(
playwright: Playwright,
connect: Callable[..., Awaitable[Browser]],
remote_server: RemoteServer,
server: Server,
) -> None:
browser = await connect(remote_server.ws_endpoint)
page = await browser.new_page()
await page.goto(server.PREFIX + "/input/handle-locator.html")

called = 0

async def handler() -> None:
nonlocal called
called += 1
await asyncio.Future()

await page.add_locator_handler(
page.get_by_text("This interstitial covers the button"), handler
)
await page.evaluate(
"""() => {
window.setupAnnoyingInterstitial('none', 1);
}"""
)
with pytest.raises(Error) as exc_info:
await page.locator("#target").click(timeout=1000)
assert "Timeout 1000ms exceeded" in exc_info.value.message
assert called == 1

await disconnect(playwright, page)

reconnected_browser = await connect(remote_server.ws_endpoint)
reconnected_page = reconnected_browser.contexts[0].pages[0]

async def reconnected_handler() -> None:
await reconnected_page.locator("#close").click()

await reconnected_page.add_locator_handler(
reconnected_page.get_by_text("This interstitial covers the button"),
reconnected_handler,
)
await reconnected_page.locator("#target").click(timeout=3000)
assert await reconnected_page.evaluate("window.clicked") == 1
23 changes: 23 additions & 0 deletions tests/async/library/test_popup.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,29 @@ async def handle(route: Route) -> None:
await context.close()


async def test_PO_029_should_load_subresources_of_an_about_blank_popup_when_the_opener_has_routes(
browser: Browser, server: Server, browser_name: str
) -> None:
if browser_name == "chromium":
pytest.skip(
"Subresources of an about:blank popup are paused on the opener target and never continued"
)
context = await browser.new_context()
page = await context.new_page()
await page.route("**/*", lambda route: route.continue_())
await page.goto(server.EMPTY_PAGE)
async with page.expect_popup() as popup_info:
await page.evaluate(
"""url => window.open('').document.write(`<img src="${url}">`)""",
server.PREFIX + "/pptr.png",
)
popup = await popup_info.value
await popup.wait_for_function(
"() => document.querySelector('img')?.naturalWidth > 0"
)
await context.close()


async def test_PO_023_browser_context_add_init_script_should_apply_to_an_in_process_popup(
browser: Browser, server: Server
) -> None:
Expand Down
23 changes: 9 additions & 14 deletions tests/async/library/test_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,23 +1233,17 @@ async def test_TR_041_should_not_emit_after_w_o_before(
page = await context.new_page()

await context.tracing.start(name="name1", snapshots=True)
evaluate_promise = await run_in_background(
page,
lambda: page.evaluate(
"""() => {
console.log('started');
return new Promise(f => window.callback = f);
}"""
),
# The wait spans the chunks: its "before" is in the first chunk, and its "after" must not get into the second one.
console_promise = await run_in_background(
page, lambda: page.wait_for_event("console")
)
async with page.expect_event("console"):
pass
await page.evaluate("() => 1")
await context.tracing.stop_chunk(path=tmp_path / "trace1.zip")
assert (traces_dir / "name1.trace").exists() is True

await context.tracing.start_chunk(name="name2")
await page.evaluate_handle("() => window.callback()")
await evaluate_promise
await page.evaluate("() => console.log('started')")
await console_promise
await context.tracing.stop(path=tmp_path / "trace2.zip")
assert (traces_dir / "name2.trace").exists() is True

Expand All @@ -1272,12 +1266,13 @@ def sanitize(e: Dict[str, Any]) -> Optional[Dict[str, Any]]:
events = parse_trace_raw(tmp_path / "trace1.zip").events
sanitized = [s for s in map(sanitize, events) if s]
assert [{k: v for k, v in s.items() if k != "callId"} for s in sanitized] == [
{"type": "before"},
{"type": "before", "title": 'Wait for event "console"'},
{"type": "before"},
{"type": "after"},
]
call1 = sanitized[0]["callId"]
assert sanitized[1]["callId"] > sanitized[0]["callId"]
assert sanitized[1]["callId"] == sanitized[2]["callId"]
call1 = sanitized[1]["callId"]

events = parse_trace_raw(tmp_path / "trace2.zip").events
sanitized = [s for s in map(sanitize, events) if s]
Expand Down
12 changes: 9 additions & 3 deletions tests/async/library/test_webmcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""Port of tests/library/webmcp.spec.ts."""

import asyncio
import re
from typing import AsyncGenerator, Dict

import pytest
Expand Down Expand Up @@ -201,9 +202,14 @@ async def test_WM_005_should_throw_when_the_tool_throws(
with pytest.raises(Error) as exc_info:
await page.webmcp.call_tool("throwing")
# Chromium does not report the error that the tool threw.
assert (
"the invocation failed" if browser_name == "chromium" else "boom"
) in exc_info.value.message
assert re.search(
(
r"the invocation failed|Tool execution failed"
if browser_name == "chromium"
else r"boom"
),
exc_info.value.message,
)


async def test_WM_006_should_throw_for_an_unknown_tool(
Expand Down
34 changes: 34 additions & 0 deletions tests/async/page/test_elementhandle_scroll_into_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,40 @@ async def test_EH_096_should_scroll_display_contents_into_view(
assert abs(scroll_top - 350) < 1


async def test_EH_124_should_scroll_nested_display_contents_into_view(
page: Page, browser_name: str, browser_major_version: int
) -> None:
await page.set_content(
"""
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; }
::-webkit-scrollbar { display: none; }
* { scrollbar-width: none; }
</style>
<div id=container style="width:200px;height:200px;overflow:scroll;border:1px solid black;">
<div style="margin-top:500px;background:red;">
<div id=outer style="display:contents"><div style="display:contents"><div id=target style="height:50px;width:100px;background:cyan;">Hello</div></div><div style="height:2000px;"></div><div>Sibling</div></div>
</div>
</div>
"""
)
div = await page.query_selector("#outer")
assert div
await div.scroll_into_view_if_needed()
# The first box in the flattened subtree of #outer is #target, two levels down,
# not the tall sibling. No whitespace text nodes inside #outer: in Firefox they
# get boxes of their own, which would hide a fallback that only looks one level down.
target_is_visible = await page.eval_on_selector(
"#target",
"""target => {
const container = document.getElementById('container').getBoundingClientRect();
const rect = target.getBoundingClientRect();
return rect.top >= container.top && rect.bottom <= container.bottom;
}""",
)
assert target_is_visible is True


async def test_EH_097_should_work_for_visibility_hidden_element(page: Page) -> None:
await page.set_content('<div style="visibility:hidden">Hello</div>')
div = await page.query_selector("div")
Expand Down
Loading
Loading