chore: update chrome extensions doc and tests (#34236)

This commit is contained in:
Dmitry Gozman 2025-01-08 17:24:29 +00:00 committed by GitHub
parent d6d5944797
commit 7ee7e018fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 18 additions and 52 deletions

View file

@ -9,7 +9,9 @@ title: "Chrome extensions"
Extensions only work in Chrome / Chromium launched with a persistent context. Use custom browser args at your own risk, as some of them may break Playwright functionality. Extensions only work in Chrome / Chromium launched with a persistent context. Use custom browser args at your own risk, as some of them may break Playwright functionality.
::: :::
The following is code for getting a handle to the [background page](https://developer.chrome.com/extensions/background_pages) of a [Manifest v2](https://developer.chrome.com/docs/extensions/mv2/) extension whose source is located in `./my-extension`: The snippet below retrieves the [background page](https://developer.chrome.com/extensions/background_pages) of a [Manifest v2](https://developer.chrome.com/docs/extensions/mv2/) extension whose source is located in `./my-extension`.
Note the use of the `chromium` channel that allows to run extensions in headless mode. Alternatively, you can launch the browser in headed mode.
```js ```js
const { chromium } = require('playwright'); const { chromium } = require('playwright');
@ -18,7 +20,7 @@ const { chromium } = require('playwright');
const pathToExtension = require('path').join(__dirname, 'my-extension'); const pathToExtension = require('path').join(__dirname, 'my-extension');
const userDataDir = '/tmp/test-user-data-dir'; const userDataDir = '/tmp/test-user-data-dir';
const browserContext = await chromium.launchPersistentContext(userDataDir, { const browserContext = await chromium.launchPersistentContext(userDataDir, {
headless: false, channel: 'chromium',
args: [ args: [
`--disable-extensions-except=${pathToExtension}`, `--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}` `--load-extension=${pathToExtension}`
@ -44,7 +46,7 @@ user_data_dir = "/tmp/test-user-data-dir"
async def run(playwright: Playwright): async def run(playwright: Playwright):
context = await playwright.chromium.launch_persistent_context( context = await playwright.chromium.launch_persistent_context(
user_data_dir, user_data_dir,
headless=False, channel="chromium",
args=[ args=[
f"--disable-extensions-except={path_to_extension}", f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}", f"--load-extension={path_to_extension}",
@ -78,7 +80,7 @@ user_data_dir = "/tmp/test-user-data-dir"
def run(playwright: Playwright): def run(playwright: Playwright):
context = playwright.chromium.launch_persistent_context( context = playwright.chromium.launch_persistent_context(
user_data_dir, user_data_dir,
headless=False, channel="chromium",
args=[ args=[
f"--disable-extensions-except={path_to_extension}", f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}", f"--load-extension={path_to_extension}",
@ -101,6 +103,8 @@ with sync_playwright() as playwright:
To have the extension loaded when running tests you can use a test fixture to set the context. You can also dynamically retrieve the extension id and use it to load and test the popup page for example. To have the extension loaded when running tests you can use a test fixture to set the context. You can also dynamically retrieve the extension id and use it to load and test the popup page for example.
Note the use of the `chromium` channel that allows to run extensions in headless mode. Alternatively, you can launch the browser in headed mode.
First, add fixtures that will load the extension: First, add fixtures that will load the extension:
```js title="fixtures.ts" ```js title="fixtures.ts"
@ -114,7 +118,7 @@ export const test = base.extend<{
context: async ({ }, use) => { context: async ({ }, use) => {
const pathToExtension = path.join(__dirname, 'my-extension'); const pathToExtension = path.join(__dirname, 'my-extension');
const context = await chromium.launchPersistentContext('', { const context = await chromium.launchPersistentContext('', {
headless: false, channel: 'chromium',
args: [ args: [
`--disable-extensions-except=${pathToExtension}`, `--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`, `--load-extension=${pathToExtension}`,
@ -155,7 +159,7 @@ def context(playwright: Playwright) -> Generator[BrowserContext, None, None]:
path_to_extension = Path(__file__).parent.joinpath("my-extension") path_to_extension = Path(__file__).parent.joinpath("my-extension")
context = playwright.chromium.launch_persistent_context( context = playwright.chromium.launch_persistent_context(
"", "",
headless=False, channel="chromium",
args=[ args=[
f"--disable-extensions-except={path_to_extension}", f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}", f"--load-extension={path_to_extension}",
@ -211,33 +215,3 @@ def test_popup_page(page: Page, extension_id: str) -> None:
page.goto(f"chrome-extension://{extension_id}/popup.html") page.goto(f"chrome-extension://{extension_id}/popup.html")
expect(page.locator("body")).to_have_text("my-extension popup") expect(page.locator("body")).to_have_text("my-extension popup")
``` ```
## Headless mode
By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using [channel `chromium`](./browsers.md#chromium-new-headless-mode):
```js title="fixtures.ts"
// ...
const pathToExtension = path.join(__dirname, 'my-extension');
const context = await chromium.launchPersistentContext('', {
channel: 'chromium',
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
});
// ...
```
```python title="conftest.py"
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}",
],
)
```

View file

@ -52,19 +52,17 @@ it('should open devtools when "devtools: true" option is given', async ({ browse
await browser.close(); await browser.close();
}); });
it('should return background pages', async ({ browserType, createUserDataDir, asset, isHeadlessShell }) => { it('should return background pages', async ({ browserType, asset, isHeadlessShell }) => {
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions'); it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir();
const extensionPath = asset('simple-extension'); const extensionPath = asset('simple-extension');
const extensionOptions = { const extensionOptions = {
headless: false,
args: [ args: [
`--disable-extensions-except=${extensionPath}`, `--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`, `--load-extension=${extensionPath}`,
], ],
}; };
const context = await browserType.launchPersistentContext(userDataDir, extensionOptions); const context = await browserType.launchPersistentContext('', extensionOptions);
const backgroundPages = context.backgroundPages(); const backgroundPages = context.backgroundPages();
const backgroundPage = backgroundPages.length const backgroundPage = backgroundPages.length
? backgroundPages[0] ? backgroundPages[0]
@ -77,13 +75,11 @@ it('should return background pages', async ({ browserType, createUserDataDir, as
expect(context.backgroundPages().length).toBe(0); expect(context.backgroundPages().length).toBe(0);
}); });
it('should return background pages when recording video', async ({ browserType, createUserDataDir, asset, isHeadlessShell }, testInfo) => { it('should return background pages when recording video', async ({ browserType, asset, isHeadlessShell }, testInfo) => {
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions'); it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir();
const extensionPath = asset('simple-extension'); const extensionPath = asset('simple-extension');
const extensionOptions = { const extensionOptions = {
headless: false,
args: [ args: [
`--disable-extensions-except=${extensionPath}`, `--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`, `--load-extension=${extensionPath}`,
@ -92,7 +88,7 @@ it('should return background pages when recording video', async ({ browserType,
dir: testInfo.outputPath(''), dir: testInfo.outputPath(''),
}, },
}; };
const context = await browserType.launchPersistentContext(userDataDir, extensionOptions); const context = await browserType.launchPersistentContext('', extensionOptions);
const backgroundPages = context.backgroundPages(); const backgroundPages = context.backgroundPages();
const backgroundPage = backgroundPages.length const backgroundPage = backgroundPages.length
? backgroundPages[0] ? backgroundPages[0]
@ -103,23 +99,21 @@ it('should return background pages when recording video', async ({ browserType,
await context.close(); await context.close();
}); });
it('should support request/response events when using backgroundPage()', async ({ browserType, createUserDataDir, asset, server, isHeadlessShell }) => { it('should support request/response events when using backgroundPage()', async ({ browserType, asset, server, isHeadlessShell }) => {
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions'); it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
server.setRoute('/empty.html', (req, res) => { server.setRoute('/empty.html', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html', 'x-response-foobar': 'BarFoo' }); res.writeHead(200, { 'Content-Type': 'text/html', 'x-response-foobar': 'BarFoo' });
res.end(`<span>hello world!</span>`); res.end(`<span>hello world!</span>`);
}); });
const userDataDir = await createUserDataDir();
const extensionPath = asset('simple-extension'); const extensionPath = asset('simple-extension');
const extensionOptions = { const extensionOptions = {
headless: false,
args: [ args: [
`--disable-extensions-except=${extensionPath}`, `--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`, `--load-extension=${extensionPath}`,
], ],
}; };
const context = await browserType.launchPersistentContext(userDataDir, extensionOptions); const context = await browserType.launchPersistentContext('', extensionOptions);
const backgroundPages = context.backgroundPages(); const backgroundPages = context.backgroundPages();
const backgroundPage = backgroundPages.length const backgroundPage = backgroundPages.length
? backgroundPages[0] ? backgroundPages[0]
@ -154,19 +148,17 @@ it('should support request/response events when using backgroundPage()', async (
it('should report console messages from content script', { it('should report console messages from content script', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32762' } annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32762' }
}, async ({ browserType, createUserDataDir, asset, server, isHeadlessShell }) => { }, async ({ browserType, asset, server, isHeadlessShell }) => {
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions'); it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir();
const extensionPath = asset('extension-with-logging'); const extensionPath = asset('extension-with-logging');
const extensionOptions = { const extensionOptions = {
headless: false,
args: [ args: [
`--disable-extensions-except=${extensionPath}`, `--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`, `--load-extension=${extensionPath}`,
], ],
}; };
const context = await browserType.launchPersistentContext(userDataDir, extensionOptions); const context = await browserType.launchPersistentContext('', extensionOptions);
const page = await context.newPage(); const page = await context.newPage();
const consolePromise = page.waitForEvent('console', e => e.text().includes('Test console log from a third-party execution context')); const consolePromise = page.waitForEvent('console', e => e.text().includes('Test console log from a third-party execution context'));
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);