Chrome 扩展
简介
扩展程序仅在以持久上下文(persistent context)启动时才能在 Chromium 中工作。使用自定义浏览器参数需自行承担风险,因为其中一些可能会破坏 Playwright 的功能。
Google Chrome 和 Microsoft Edge 移除了侧载(side-load)扩展程序所需的命令行标志,因此请使用 Playwright 捆绑的 Chromium。
下面的代码片段获取了源文件位于 ./my-extension 的 Manifest v3 扩展程序的 Service Worker。
注意使用了 chromium 通道,它允许在无头模式(headless mode)下运行扩展程序。或者,您也可以以有头模式(headed mode)启动浏览器。
- 同步
- 异步
from playwright.sync_api import sync_playwright, Playwright
path_to_extension = "./my-extension"
user_data_dir = "/tmp/test-user-data-dir"
def run(playwright: Playwright):
context = playwright.chromium.launch_persistent_context(
user_data_dir,
channel="chromium",
args=[
f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}",
],
)
if len(context.service_workers) == 0:
service_worker = context.wait_for_event('serviceworker')
else:
service_worker = context.service_workers[0]
# Test the service worker as you would any other worker.
context.close()
with sync_playwright() as playwright:
run(playwright)
import asyncio
from playwright.async_api import async_playwright, Playwright
path_to_extension = "./my-extension"
user_data_dir = "/tmp/test-user-data-dir"
async def run(playwright: Playwright):
context = await playwright.chromium.launch_persistent_context(
user_data_dir,
channel="chromium",
args=[
f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}",
],
)
if len(context.service_workers) == 0:
service_worker = await context.wait_for_event('serviceworker')
else:
service_worker = context.service_workers[0]
# Test the service worker as you would any other worker.
await context.close()
async def main():
async with async_playwright() as playwright:
await run(playwright)
asyncio.run(main())
Service Worker 空闲挂起 (MV3)
Chrome MV3 Service Worker 在不活动约 30 秒后会自动挂起,并按需重启。当这种情况发生时,Playwright 会保持相同的 Worker 对象存活——不会触发新的 'serviceworker' 事件。在重启窗口期间发出的新的 evaluate() 调用会处于停滞状态,直到新上下文准备就绪,然后自动恢复。
- 同步
- 异步
sw = context.wait_for_event('serviceworker')
# ... SW suspends after 30 s of inactivity and is restarted by the browser ...
# The existing handle is transparent across the restart.
sw.evaluate("sendMessage({ type: 'ping' })") # just works
sw = await context.wait_for_event('serviceworker')
# ... SW suspends after 30 s of inactivity and is restarted by the browser ...
# The existing handle is transparent across the restart.
await sw.evaluate("sendMessage({ type: 'ping' })") # just works
在挂起瞬间已经处于运行中的 evaluate() 调用会抛出 "Service worker restarted" 错误,这与页面导航中途被打断的行为一致。
测试
要在运行测试时加载扩展程序,您可以使用测试夹具(test fixture)来设置上下文。您还可以动态获取扩展程序 ID,并使用它来加载和测试弹出页面(popup page)等。
注意使用了 chromium 通道,它允许在无头模式(headless mode)下运行扩展程序。或者,您也可以以有头模式(headed mode)启动浏览器。
首先,添加将加载扩展程序的夹具
from typing import Generator
from pathlib import Path
from playwright.sync_api import Playwright, BrowserContext
import pytest
@pytest.fixture()
def context(playwright: Playwright) -> Generator[BrowserContext, None, None]:
path_to_extension = Path(__file__).parent.joinpath("my-extension")
context = playwright.chromium.launch_persistent_context(
"",
channel="chromium",
args=[
f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}",
],
)
yield context
context.close()
@pytest.fixture()
def extension_id(context) -> Generator[str, None, None]:
# for manifest v3:
service_worker = context.service_workers[0]
if not service_worker:
service_worker = context.wait_for_event("serviceworker")
extension_id = service_worker.url.split("/")[2]
yield extension_id
然后在测试中使用这些夹具
from playwright.sync_api import expect, Page
def test_example_test(page: Page) -> None:
page.goto("https://example.com")
expect(page.locator("body")).to_contain_text("Changed by my-extension")
def test_popup_page(page: Page, extension_id: str) -> None:
page.goto(f"chrome-extension://{extension_id}/popup.html")
expect(page.locator("body")).to_have_text("my-extension popup")