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 @@
eac236da288cdc3d0891278a61e1a876348950a0
0ea36910324ed8aa2a5f460317cbd35881ee88d4
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->158.0.8127.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->27.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->158.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->159.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |

## Documentation

Expand Down
7 changes: 4 additions & 3 deletions playwright/_impl/browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,8 @@ def _on_request_finished(
async def _on_route(self, route: network.Route) -> None:
route._context = self
page = route.request._safe_page()
route_handlers = self._routes[:]
for route_handler in route_handlers:
route_handlers = self._routes
for route_handler in route_handlers[:]:
# If the page or the context was closed we stall all requests right away.
if (page and page._close_was_called) or self.is_closed():
return
Expand All @@ -404,7 +404,8 @@ async def _on_route(self, route: network.Route) -> None:
if route_handler.will_expire():
del self._routes[index]
handled = await route_handler.handle(route)
if not self._routes:
# unrouteAll() replaces _routes and updates interception after waiting for handlers.
if route_handlers is self._routes and not self._routes:
fire_and_forget(self._disable_interception())
if handled:
return
Expand Down
24 changes: 18 additions & 6 deletions playwright/_impl/event_emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,15 @@ def emit(self, type: EventType, *args: Any) -> bool:
return True

def _call_handler(self, type: EventType, handler: Listener, args: Any) -> None:
promise = handler(*args)
if not inspect.isawaitable(promise):
try:
promise = handler(*args)
if not inspect.isawaitable(promise):
return
# Python: an awaitable is the counterpart of a JS promise or thenable.
future = asyncio.ensure_future(promise)
except Exception as error:
self._handle_listener_error(type, error)
return
# Python: the coroutine of an async def handler runs as a task.
future = asyncio.ensure_future(promise)
pending = self._pending_handlers.get(type)
if pending is None:
pending = set()
Expand All @@ -103,11 +107,19 @@ def on_done(f: "asyncio.Future[Any]") -> None:
if self._rejection_handler:
self._rejection_handler(e)
else:
# Python: the counterpart of an unhandled rejection.
self._report_unhandled_error(e)
try:
self._handle_listener_error(type, e)
except Exception as error:
# Python: report an unhandled rejection outside the task callback.
self._report_unhandled_error(error)

future.add_done_callback(on_done)

def _handle_listener_error(self, type: EventType, error: BaseException) -> None:
if type == "error" or not self.listener_count("error"):
raise error
self.emit("error", error)

def _report_unhandled_error(self, error: BaseException) -> None:
# Python: an unhandled rejection crashes Node.js; see report_unhandled_error().
report_unhandled_error(error)
Expand Down
7 changes: 4 additions & 3 deletions playwright/_impl/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,8 @@ def _on_frame_detached(self, frame: Frame) -> None:

async def _on_route(self, route: Route) -> None:
route._context = self.context
route_handlers = list(self._routes)
for route_handler in route_handlers:
route_handlers = self._routes
for route_handler in route_handlers[:]:
# If the page was closed we stall all requests right away.
if self._close_was_called or self._browser_context.is_closed():
return
Expand All @@ -346,7 +346,8 @@ async def _on_route(self, route: Route) -> None:
if route_handler.will_expire():
self._routes.pop(index)
handled = await route_handler.handle(route)
if not self._routes:
# unrouteAll() replaces _routes and updates interception after waiting for handlers.
if route_handlers is self._routes and not self._routes:
fire_and_forget(self._disable_interception())
if handled:
return
Expand Down
16 changes: 11 additions & 5 deletions playwright/async_api/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -4009,6 +4009,10 @@ async def connect_over_cdp(
endpoint_url : str
A CDP websocket endpoint or http url to connect to. For example `http://localhost:9222/` or
`ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`.

Alternatively, a browser channel name such as `chrome` or `msedge` to connect to a browser with remote debugging
enabled in its default user data directory. On Linux, Playwright uses `CHROME_USER_DATA_DIR` when set. Otherwise,
it uses `CHROME_CONFIG_HOME`, then `XDG_CONFIG_HOME`, then `~/.config` to locate that directory.
artifacts_dir : Union[pathlib.Path, str, None]
If specified, browser artifacts (such as traces and downloads) are saved into this directory.
headers : Union[Dict[str, str], None]
Expand All @@ -4018,10 +4022,10 @@ async def connect_over_cdp(
upon the file system being the same between Playwright and the Browser.
no_defaults : Union[bool, None]
When true, Playwright will not apply its default overrides to the existing default browser context. Specifically,
`accept_downloads` is left at the browser's setting, focus emulation is not enabled, and media emulation options
(such as `color_scheme`, `reduced_motion`, `forced_colors`, and `contrast`) are not applied. Useful when attaching
to a user's daily-driver browser where these overrides would interfere with existing browser state. New contexts
created via `browser.new_context()` are not affected. Defaults to `false`.
`accept_downloads` and font families are left at the browser's settings, focus emulation is not enabled, and media
emulation options (such as `color_scheme`, `reduced_motion`, `forced_colors`, and `contrast`) are not applied.
Useful when attaching to a user's daily-driver browser where these overrides would interfere with existing browser
state. New contexts created via `browser.new_context()` are not affected. Defaults to `false`.
slow_mo : Union[float, None]
Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going
on. Defaults to 0.
Expand Down Expand Up @@ -10735,7 +10739,9 @@ async def aria_snapshot(

An AI-optimized snapshot, controlled by `mode`, is different from a default snapshot:
1. Includes element references `[ref=e2]`. 2. Does not wait for an element matching the locator, and throws when
no elements match. 3. Includes snapshots of `<iframe>`s inside the target.
no elements match. 3. Includes snapshots of `<iframe>`s inside the target. 4. Can omit names that repeat
printed descendants, such as a heading containing a link. Names that identify dialogs, landmarks, groups,
tables and table headers are retained.

Parameters
----------
Expand Down
16 changes: 11 additions & 5 deletions playwright/sync_api/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -3935,6 +3935,10 @@ def connect_over_cdp(
endpoint_url : str
A CDP websocket endpoint or http url to connect to. For example `http://localhost:9222/` or
`ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`.

Alternatively, a browser channel name such as `chrome` or `msedge` to connect to a browser with remote debugging
enabled in its default user data directory. On Linux, Playwright uses `CHROME_USER_DATA_DIR` when set. Otherwise,
it uses `CHROME_CONFIG_HOME`, then `XDG_CONFIG_HOME`, then `~/.config` to locate that directory.
artifacts_dir : Union[pathlib.Path, str, None]
If specified, browser artifacts (such as traces and downloads) are saved into this directory.
headers : Union[Dict[str, str], None]
Expand All @@ -3944,10 +3948,10 @@ def connect_over_cdp(
upon the file system being the same between Playwright and the Browser.
no_defaults : Union[bool, None]
When true, Playwright will not apply its default overrides to the existing default browser context. Specifically,
`accept_downloads` is left at the browser's setting, focus emulation is not enabled, and media emulation options
(such as `color_scheme`, `reduced_motion`, `forced_colors`, and `contrast`) are not applied. Useful when attaching
to a user's daily-driver browser where these overrides would interfere with existing browser state. New contexts
created via `browser.new_context()` are not affected. Defaults to `false`.
`accept_downloads` and font families are left at the browser's settings, focus emulation is not enabled, and media
emulation options (such as `color_scheme`, `reduced_motion`, `forced_colors`, and `contrast`) are not applied.
Useful when attaching to a user's daily-driver browser where these overrides would interfere with existing browser
state. New contexts created via `browser.new_context()` are not affected. Defaults to `false`.
slow_mo : Union[float, None]
Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going
on. Defaults to 0.
Expand Down Expand Up @@ -10733,7 +10737,9 @@ def aria_snapshot(

An AI-optimized snapshot, controlled by `mode`, is different from a default snapshot:
1. Includes element references `[ref=e2]`. 2. Does not wait for an element matching the locator, and throws when
no elements match. 3. Includes snapshots of `<iframe>`s inside the target.
no elements match. 3. Includes snapshots of `<iframe>`s inside the target. 4. Can omit names that repeat
printed descendants, such as a heading containing a link. Names that identify dialogs, landmarks, groups,
tables and table headers are retained.

Parameters
----------
Expand Down
59 changes: 55 additions & 4 deletions tests/async/library/chromium/test_connect_over_cdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@
import json
import os
import signal
import sys
import urllib.request
from contextlib import suppress
from pathlib import Path
from typing import Any, Dict, Generator, List
from typing import Any, Dict, Generator, List, Optional

import pytest

Expand All @@ -35,7 +36,7 @@
expect,
)
from tests.helpers.proxy_server import ProxyServer
from tests.helpers.remote_server import RemoteServer
from tests.helpers.remote_server import ChildProcess, RemoteServer
from tests.server import Server, TestServerRequest, WebSocketProtocol, find_free_port

pytestmark = pytest.mark.only_browser("chromium")
Expand Down Expand Up @@ -83,16 +84,66 @@ async def start_playwright_with_env(


async def test_CD_001_should_connect_to_an_existing_cdp_session(
browser_type: BrowserType, launch_arguments: Dict, port: int
browser_type: BrowserType,
launch_arguments: Dict,
tmp_path: Path,
is_linux: bool,
) -> None:
browser_server = await launch(browser_type, launch_arguments, port)
config_home = tmp_path / "config"
user_data_dir = config_home / "google-chrome"
browser_server = await browser_type.launch_persistent_context(
user_data_dir, **launch_arguments, args=["--remote-debugging-port=0"]
)
try:
port = (user_data_dir / "DevToolsActivePort").read_text().splitlines()[0]
cdp_browser = await browser_type.connect_over_cdp(
endpoint_url=f"http://127.0.0.1:{port}/",
)
contexts = cdp_browser.contexts
assert len(contexts) == 1
await cdp_browser.close()

if is_linux:
environments: List[Dict[str, Optional[str]]] = [
{
"CHROME_USER_DATA_DIR": "",
"CHROME_CONFIG_HOME": "",
"XDG_CONFIG_HOME": str(config_home),
},
{
"CHROME_USER_DATA_DIR": "",
"CHROME_CONFIG_HOME": str(config_home),
"XDG_CONFIG_HOME": str(tmp_path / "unused-config"),
},
{
"CHROME_USER_DATA_DIR": str(user_data_dir),
"CHROME_CONFIG_HOME": str(tmp_path / "unused-config"),
"XDG_CONFIG_HOME": str(tmp_path / "unused-xdg-config"),
},
]
for env in environments:
# Python: use the Python client in the child process.
child = ChildProcess(
[
sys.executable,
"-c",
"""
import asyncio
from playwright.async_api import async_playwright

async def main():
async with async_playwright() as playwright:
browser = await playwright.chromium.connect_over_cdp('chrome')
print('contexts: ' + str(len(browser.contexts)))
await browser.close()

asyncio.run(main())
""",
],
env=env,
)
assert await asyncio.to_thread(child.exit_code) == 0, child.output
assert "contexts: 1" in child.output
finally:
await browser_server.close()

Expand Down
33 changes: 32 additions & 1 deletion tests/async/library/test_page_clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,15 +663,26 @@ async def test_CK_044_while_on_pause_run_for_should_not_run_nested_immediate_fro
async def test_CK_045_while_on_pause_should_stay_paused_on_the_next_page_when_an_init_script_reads_the_time(
page: Page, server: Server
) -> None:
def handler(request: TestServerRequest) -> None:
request.setHeader("Content-Type", "text/html")
request.write(
b"<script>setTimeout(() => document.documentElement.dataset.fired = 'true', 0);</script>"
)
request.finish()

server.set_route("/timer.html", handler)
await page.clock.install(time=0)
await page.clock.pause_at(1)
await page.add_init_script("(() => Date.now())()")
await page.goto(server.EMPTY_PAGE)
await page.clock.run_for(1000)
await page.goto(server.PREFIX + "/title.html")
await page.goto(server.PREFIX + "/timer.html")
assert await page.evaluate("() => Date.now()") == 2000
await page.wait_for_timeout(500)
assert await page.evaluate("() => Date.now()") == 2000
assert await page.evaluate("() => document.documentElement.dataset.fired") is None
await page.clock.run_for(0)
assert await page.evaluate("() => document.documentElement.dataset.fired") == "true"


async def test_CK_046_while_on_pause_should_not_advance_performance_now_before_the_first_clock_call_on_a_new_page(
Expand All @@ -683,6 +694,26 @@ async def test_CK_046_while_on_pause_should_not_advance_performance_now_before_t
assert await page.evaluate("() => [performance.now(), Date.now()]") == [0, 1000]


async def test_CK_220_while_on_pause_should_keep_timers_scheduled_during_load_pending_when_paused(
page: Page, server: Server
) -> None:
def handler(request: TestServerRequest) -> None:
request.setHeader("Content-Type", "text/html")
request.write(
b"<script>setTimeout(() => document.documentElement.dataset.fired = 'true', 0);</script>"
)
request.finish()

server.set_route("/timer.html", handler)
await page.clock.install(time=0)
await page.clock.pause_at(1)
await page.goto(server.PREFIX + "/timer.html")
await page.wait_for_timeout(500)
assert await page.evaluate("() => document.documentElement.dataset.fired") is None
await page.clock.run_for(0)
assert await page.evaluate("() => document.documentElement.dataset.fired") == "true"


async def test_CK_047_date_now_check_date_now_is_an_integer(page: Page) -> None:
await page.clock.install()
await page.goto("data:text/html,")
Expand Down
14 changes: 12 additions & 2 deletions tests/async/library/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import pytest

from playwright.async_api import BrowserType, Error
from playwright.async_api import BrowserType, Error, Request
from tests.helpers.proxy_server import ProxyRequest, ProxyServer
from tests.helpers.socks_server import MockSocksServer, SocksForwardingServer
from tests.server import Server, TestServerRequest
Expand Down Expand Up @@ -71,8 +71,18 @@ async def test_PR_002_should_use_proxy_smoke(
**launch_arguments, proxy={"server": server.HOST}
)
page = await browser.new_page()
await page.goto("http://non-existent.test/target.html")
url = "http://staging.example.org/target.html"
requests: List[str] = []

def on_request(request: Request) -> None:
if request.is_navigation_request():
requests.append(request.url)

page.on("request", on_request)
await page.goto(url)
assert await page.title() == "Served by the proxy"
assert page.url == url
assert requests == [url]
await browser.close()


Expand Down
Loading
Loading