1"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[36879],{22002:(e,n,o)=>{o.d(n,{R:()=>r,x:()=>a});var t=o(7378);const i={},s=t.createContext(i);function r(e){const n=t.useContext(s);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:r(e.components),t.createElement(s.Provider,{value:n},e.children)}},39169:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>h,frontMatter:()=>r,metadata:()=>t,toc:()=>d});const t=JSON.parse('{"id":"extension-testing/web-extensions","title":"Web Extension Testing","description":"WebdriverIO is the ideal tool to automate a browser. Web Extensions are a part of the browser and can be automated in the same way. Whenever your web extension uses content scripts to run JavaScript on websites or offer a popup modal, you can run an e2e test for that using WebdriverIO.","source":"@site/docs/extension-testing/WebExtension.md","sourceDirName":"extension-testing","slug":"/extension-testing/web-extensions","permalink":"/docs/extension-testing/web-extensions","draft":false,"unlisted":false,"editUrl":"https://github.com/webdriverio/webdriverio/edit/main/website/docs/extension-testing/WebExtension.md","tags":[],"version":"current","frontMatter":{"id":"web-extensions","title":"Web Extension Testing"},"sidebar":"docs","previous":{"title":"Mocking","permalink":"/docs/component-testing/mocking"},"next":{"title":"VS Code Extension Testing","permalink":"/docs/extension-testing/vscode-extensions"}}');var i=o(86106),s=o(22002);const r={id:"web-extensions",title:"Web Extension Testing"},a=void 0,c={},d=[{value:"Loading a Web Extension into the Browser",id:"loading-a-web-extension-into-the-browser",level:2},{value:"Chrome",id:"chrome",level:3},{value:"Firefox",id:"firefox",level:3},{value:"Tips & Tricks",id:"tips--tricks",level:2},{value:"Test Popup Modal in Chrome",id:"test-popup-modal-in-chrome",level:3}];function l(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"WebdriverIO is the ideal tool to automate a browser. Web Extensions are a part of the browser and can be automated in the same way. Whenever your web extension uses content scripts to run JavaScript on websites or offer a popup modal, you can run an e2e test for that using WebdriverIO."}),"\n",(0,i.jsx)(n.h2,{id:"loading-a-web-extension-into-the-browser",children:"Loading a Web Extension into the Browser"}),"\n",(0,i.jsx)(n.p,{children:"As a first step we have to load the extension under test into the browser as part of our session. This works differently for Chrome and Firefox."}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["These docs leave out Safari web extensions as their support for it is way behind and user demand not high. If you are building a web extension for Safari, please ",(0,i.jsx)(n.a,{href:"https://github.com/webdriverio/webdriverio/issues/new?assignees=&labels=Docs+%F0%9F%93%96%2CNeeds+Triaging+%E2%8F%B3&template=documentation.yml&title=%5B%F0%9F%93%96+Docs%5D%3A+%3Ctitle%3E",children:"raise an issue"})," and collaborate on including it here as well."]})}),"\n",(0,i.jsx)(n.h3,{id:"chrome",children:"Chrome"}),"\n",(0,i.jsxs)(n.p,{children:["Loading a web extension in Chrome can be done through providing a ",(0,i.jsx)(n.code,{children:"base64"})," encoded string of the ",(0,i.jsx)(n.code,{children:"crx"})," file or by providing a path to the web extension folder. The easiest is just to do the latter by defining your Chrome capabilities as following:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",metastring:"wdio.conf.js",children:"import path from 'node:path'\nimport url from 'node:url'\n\nconst __dirname = url.fileURLToPath(new URL('.', import.meta.url))\n\nexport const config = {\n // ...\n capabilities: [{\n browserName,\n 'goog:chromeOptions': {\n // given your wdio.c
1onf.js is in the root directory and your compiled\n // web extension files are located in the `./dist` folder\n args: [`--load-extension=${path.join(__dirname, '..', '..', 'dist')}`]\n }\n }]\n}\n"})}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["If you automate a different browser than Chrome, e.g. Brave, Edge or Opera, chances are that the browser option match with the example above, just using a different capability name, e.g. ",(0,i.jsx)(n.code,{children:"ms:edgeOptions"}),"."]})}),"\n",(0,i.jsxs)(n.p,{children:["If you compile your extension as ",(0,i.jsx)(n.code,{children:".crx"})," file using e.g. the ",(0,i.jsx)(n.a,{href:"https://www.npmjs.com/package/crx",children:"crx"})," NPM package, you can also inject the bundled extension via:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",metastring:"wdio.conf.js",children:"import path from 'node:path'\nimport url from 'node:url'\n\nconst __dirname = url.fileURLToPath(new URL('.', import.meta.url))\nconst extPath = path.join(__dirname, `web-extension-chrome.crx`)\nconst chromeExtension = (await fs.readFile(extPath)).toString('base64')\n\nexport const config = {\n // ...\n capabilities: [{\n browserName,\n 'goog:chromeOptions': {\n extensions: [chromeExtension]\n }\n }]\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"firefox",children:"Firefox"}),"\n",(0,i.jsxs)(n.p,{children:["To create a Firefox profile that includes extensions you can use the ",(0,i.jsx)(n.a,{href:"/docs/firefox-profile-service",children:"Firefox Profile Service"})," to set up your session accordingly. However you might run into issues where your local developed extension can't be loaded due to signing issues. In this case you can also load an extension in the ",(0,i.jsx)(n.code,{children:"before"})," hook via the ",(0,i.jsx)(n.a,{href:"/docs/api/gecko#installaddon",children:(0,i.jsx)(n.code,{children:"installAddOn"})})," command, e.g.:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",metastring:"wdio.conf.js",children:"import path from 'node:path'\nimport url from 'node:url'\n\nconst __dirname = url.fileURLToPath(new URL('.', import.meta.url))\nconst extensionPath = path.resolve(__dirname, `web-extension.xpi`)\n\nexport const config = {\n // ...\n before: async (capabilities) => {\n const browserName = (capabilities as WebdriverIO.Capabilities).browserName\n if (browserName === 'firefox') {\n const extension = await fs.readFile(extensionPath)\n await browser.installAddOn(extension.toString('base64'), true)\n }\n }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["In order to generate an ",(0,i.jsx)(n.code,{children:".xpi"})," file, it is recommended to use the ",(0,i.jsx)(n.a,{href:"https://www.npmjs.com/package/web-ext",children:(0,i.jsx)(n.code,{children:"web-ext"})})," NPM package. You can bundle your extension using the following example command:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-sh",children:"npx web-ext build -s dist/ -a . -n web-extension-firefox.xpi\n"})}),"\n",(0,i.jsx)(n.h2,{id:"tips--tricks",children:"Tips & Tricks"}),"\n",(0,i.jsx)(n.p,{children:"The following section contains a set useful tips and tricks that can be helpful when testing a web extension."}),"\n",(0,i.jsx)(n.h3,{id:"test-popup-modal-in-chrome",children:"Test Popup Modal in Chrome"}),"\n",(0,i.jsxs)(n.p,{children:["If you define a ",(0,i.jsx)(n.code,{children:"default_popup"})," browser action entry in your ",(0,i.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_action",children:"extension manifest"})," you can test that HTML page directly, since clicking on the extension icon in the browser top bar won't work. Instead, you have to open the popup html file directly."]}),"\n",(0,i.jsxs)(n.p,{children:["In Chrome this works by retrieving the extension ID and opening the popup page through ",(0,i.jsx)(n.code,{children:"browser.url('...')"}),". The behavior on that page will be the same as within the popup. To do so we recommend to write the following custom command:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"customCommand.ts",children:"export async function openExtensionPopup (this: WebdriverIO.Browser, extensionName: string, popupUrl = 'index.html') {\n if ((this.capabilities as WebdriverIO.Capabilities).browserName !== 'chrome') {\n throw new Error('This command only w
1orks with Chrome')\n }\n await this.url('chrome://extensions/')\n\n const extensions = await this.$$('extensions-item')\n const extension = await extensions.find(async (ext) => (\n await ext.$('#name').getText()) === extensionName\n )\n\n if (!extension) {\n const installedExtensions = await extensions.map((ext) => ext.$('#name').getText())\n throw new Error(`Couldn't find extension \"${extensionName}\", available installed extensions are \"${installedExtensions.join('\", \"')}\"`)\n }\n\n const extId = await extension.getAttribute('id')\n await this.url(`chrome-extension://${extId}/popup/${popupUrl}`)\n}\n\ndeclare global {\n namespace WebdriverIO {\n interface Browser {\n openExtensionPopup: typeof openExtensionPopup\n }\n }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["In your ",(0,i.jsx)(n.code,{children:"wdio.conf.js"})," you can import this file and register the custom command in your ",(0,i.jsx)(n.code,{children:"before"})," hook, e.g.:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"wdio.conf.ts",children:"import { browser } from '@wdio/globals'\n\nimport { openExtensionPopup } from './support/customCommands'\n\nexport const config: WebdriverIO.Config = {\n // ...\n before: () => {\n browser.addCommand('openExtensionPopup', openExtensionPopup)\n }\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"Now, in your test, you can access the popup page via:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"await browser.openExtensionPopup('My Web Extension')\n"})})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(l,{...e})}):l(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.