1"use strict";(self.webpackChunkplaywright_dev=self.webpackChunkplaywright_dev||[]).push([["5541"],{98833(e,n,t){t.r(n),t.d(n,{metadata:()=>s,default:()=>h,frontMatter:()=>i,contentTitle:()=>a,toc:()=>l,assets:()=>c});var s=JSON.parse('{"id":"chrome-extensions","title":"Chrome extensions","description":"Introduction","source":"@site/versioned_docs/version-stable/chrome-extensions.mdx","sourceDirName":".","slug":"/chrome-extensions","permalink":"/docs/chrome-extensions","draft":false,"unlisted":false,"tags":[],"version":"stable","frontMatter":{"id":"chrome-extensions","title":"Chrome extensions"},"sidebar":"docs","previous":{"title":"Browsers","permalink":"/docs/browsers"},"next":{"title":"Clock","permalink":"/docs/clock"}}'),o=t(74848),r=t(28453);t(13554),t(41647),t(83137);let i={id:"chrome-extensions",title:"Chrome extensions"},a,c={},l=[{value:"Introduction",id:"introduction",level:2},{value:"Service worker idle suspension (MV3)",id:"service-worker-idle-suspension-mv3",level:2},{value:"Testing",id:"testing",level:2}];function d(e){let n={a:"a",admonition:"admonition",code:"code",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,o.jsxs)(n.admonition,{type:"note",children:[(0,o.jsx)(n.p,{children:"Extensions only work in Chromium when launched with a persistent context. Use custom browser args at your own risk, as some of them may break Playwright functionality."}),(0,o.jsxs)(n.p,{children:["Google Chrome and Microsoft Edge ",(0,o.jsx)(n.a,{href:"https://groups.google.com/a/chromium.org/g/chromium-extensions/c/FxMU1TvxWWg/m/daZVTYNlBQAJ",children:"removed the command-line flags needed to side-load extensions"}),", so use Chromium that comes bundled with Playwright."]})]}),"\n",(0,o.jsxs)(n.p,{children:["The snippet below retrieves the ",(0,o.jsx)(n.a,{href:"https://developer.chrome.com/docs/extensions/develop/concepts/service-workers",children:"service worker"})," of a ",(0,o.jsx)(n.a,{href:"https://developer.chrome.com/docs/extensions/develop/migrate",children:"Manifest v3"})," extension whose source is located in ",(0,o.jsx)(n.code,{children:"./my-extension"}),"."]}),"\n",(0,o.jsxs)(n.p,{children:["Note the use of the ",(0,o.jsx)(n.code,{children:"chromium"})," channel that allows to run extensions in headless mode. Alternatively, you can launch the browser in headed mode."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const { chromium } = require('playwright');\n\n(async () => {\n const pathToExtension = require('path').join(__dirname, 'my-extension');\n const userDataDir = '/tmp/test-user-data-dir';\n const browserContext = await chromium.launchPersistentContext(userDataDir, {\n channel: 'chromium',\n args: [\n `--disable-extensions-except=${pathToExtension}`,\n `--load-extension=${pathToExtension}`\n ]\n });\n let [serviceWorker] = browserContext.serviceWorkers();\n if (!serviceWorker)\n serviceWorker = await browserContext.waitForEvent('serviceworker');\n\n // Test the service worker as you would any other worker.\n await browserContext.close();\n})();\n"})}),"\n",(0,o.jsx)(n.h2,{id:"service-worker-idle-suspension-mv3",children:"Service worker idle suspension (MV3)"}),"\n",(0,o.jsxs)(n.p,{children:["Chrome MV3 service workers are automatically suspended after ~30 seconds of inactivity and restarted on demand. When this happens, Playwright keeps the ",(0,o.jsxs)(n.strong,{children:["same ",(0,o.jsx)(n.a,{href:"/docs/api/class-worker",title:"Worker",children:"Worker"})," object alive"]})," \u2014 no new ",(0,o.jsx)(n.code,{children:"'serviceworker'"})," event is emitted. New ",(0,o.jsx)(n.code,{children:"evaluate()"})," calls issued during the restart window are stalled until the new context is ready and then resume automatically:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"const sw = await context.waitForEvent('serviceworker');\n\n// ... SW suspends after 30 s of inactivity and is restarted by the browser ...\n\n// The existing handle is transparent across the restart.\nawait sw.evaluate(() => sendMessage({ type: 'ping' })); // just works\n"})}),"\n",(0,o.jsx)(n.admonition,{type:"note",children:(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.code,{children:"evaluate()"})," calls that were already in-flight at the exact moment of suspension will throw with ",(0,o.jsx)(n.code,{children:'"Service worker restarted"'}),", matching the behaviour of page navigations mid-flight."]})}),"\n",(0,o.jsx)(n.h2,{id:"testing",children:"Testing"}),"\n",(0,o.jsx)(n.p,{children:"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."}),"\n",(0,o.jsxs)(n.p,{children:["Note the use of the ",(0,o.jsx)(n.code,{children:"chromium"})," channel that allows to run extensions in headless mode. Alternatively, you can launch the browser in headed mode."]}),"\n",(0,o.jsx)(n.p,{children:"First, add fixtures that will load the extension:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",metastring:'title="fixtures.ts"',children:"import { test as base, chromium, type BrowserContext } from '@playwright/test';\nimport path from 'path';\n\nexport const test = base.extend<{\n context: BrowserContext;\n extensionId: string;\n}>({\n context: async ({ }, use) => {\n const pathToExtension = path.join(__dirname, 'my-extension');\n const context = await chromium.launchPersistentContext('', {\n channel: 'chromium',\n args: [\n `--disable-extensions-except=${pathToExtension}`,\n `--load-extension=${pathToExtension}`,\n ],\n });\n await use(context);\n await context.close();\n },\n extensionId: async ({ context }, use) => {\n // for manifest v3:\n let [serviceWorker] = context.serviceWorkers();\n if (!serviceWorker)\n serviceWorker = await context.waitForEvent('serviceworker');\n\n const extensionId = serviceWorker.url().split('/')[2];\n await use(extensionId);\n },\n});\nexport const expect = test.expect;\n"})}
1),"\n",(0,o.jsx)(n.p,{children:"Then use these fixtures in a test:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"import { test, expect } from './fixtures';\n\ntest('example test', async ({ page }) => {\n await page.goto('https://example.com');\n await expect(page.locator('body')).toHaveText('Changed by my-extension');\n});\n\ntest('popup page', async ({ page, extensionId }) => {\n await page.goto(`chrome-extension://${extensionId}/popup.html`);\n await expect(page.locator('body')).toHaveText('my-extension popup');\n});\n"})})]})}function h(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.