Skip to content
This repository was archived by the owner on Jun 17, 2024. It is now read-only.

Latest commit

 

History

History
202 lines (152 loc) · 6.14 KB

File metadata and controls

202 lines (152 loc) · 6.14 KB

Puppeteer

Puppeteer's Chrome Extension docs: https://pptr.dev/guides/chrome-extensions

This example will walk you through adding E2E tests written with Puppeteer and Vitest. Since Puppeteer just provides APIs for browser automation, we need to use Vitest to setup and run the actual tests.

Setup

First, install both dependencies and add a e2e convenience script:

package.json
@@ -9,13 +9,16 @@
     "dev:firefox": "wxt -b firefox",
     "build": "wxt build",
     "build:firefox": "wxt build -b firefox",
     "zip": "wxt zip",
     "zip:firefox": "wxt zip -b firefox",
     "compile": "tsc --noEmit",
-    "postinstall": "wxt prepare"
+    "postinstall": "wxt prepare",
+    "e2e": "vitest -r e2e"
   },
   "devDependencies": {
+    "puppeteer": "^21.6.1",
     "typescript": "^5.3.3",
+    "vitest": "^1.1.0",
     "wxt": "^0.16.0"
   }
 }

Next, create a e2e/ folder in the root of the project. This is an arbitrary but fairly standard name for a folder containing all the E2E tests, their utils, and config.

If you're using TypeScript, the first file you'll want to create is a tsconfig.json file. E2E tests can be considered their own "project", running in a different environment with different globals.

e2e/tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Bundler"
  }
}

And exclude the e2e/ folder from the root tsconfig.json.

tsconfig.json
@@ -1,3 +1,4 @@
 {
-  "extends": "./.wxt/tsconfig.json"
+  "extends": "./.wxt/tsconfig.json",
+  "exclude": ["e2e"]
 }

Next, we're gonna setup Vitest. Our new e2e script, vitest -r e2e, will look for an e2e/vitest.config.ts file, so we need to create that:

e2e/vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: './vitest-environment-puppeteer.ts',
  },
});

Last step of the setup: we need to create a custom Vitest environment responsible for managing Puppeteer. It will need to launch a browser before each test file, define a few global variables, and close the browser after the tests run:

e2e/vitest-environment-puppeteer.ts
import type { Environment } from 'vitest';
import puppeteer from 'puppeteer';
import path from 'node:path';

export default <Environment>{
  name: 'puppeteer',
  transformMode: 'ssr',

  async setup(global, options) {
    // Puppeteer only supports chrome, so you can hard-code this path to any chromium output
    const pathToExtension = path.resolve('.output/chrome-mv3');
    const browser = await puppeteer.launch({
      headless: false,
      ...(options ?? {}),
      args: [
        `--disable-extensions-except=${pathToExtension}`,
        `--load-extension=${pathToExtension}`,
        ...(options?.args ?? []),
      ],
    });

    const expectedType = pathToExtension.endsWith('-mv3')
      ? 'service_worker'
      : 'background_page';
    const background = await browser.waitForTarget(
      (target) => target.type() === expectedType,
    );

    // Assign any global variables that will be used in the tests
    global.browser = browser;
    global.extensionId = new URL(background.url()).hostname;

    return {
      teardown: () => browser.close(),
    };
  },
};

Warning

If you don't have a background script, you'll have to make the ID consistent and hardcode it instead.

And finally, if you're using TypeScript, add a declaration file to add types for the two globals defined in the custom environment:

e2e/globals.d.ts
declare const browser: import('puppeteer').Browser;
declare const extensionId: string;

Writing Tests

We're going to write a test to make sure the Popup's counter increments when pressed.

Frameworks often recommend creating an abstraction around their APIs and writing helper function to interact with the page more naturally during tests. We'll follow this approach and create a openPopup util, which returns natural helper functions.

e2e/pages/popup.ts
export async function openPopup() {
  const page = await browser.newPage();
  await page.goto(`chrome-extension://${extensionId}/popup.html`);

  await page.waitForSelector('#counter');

  const popup = {
    getCounter: () => page.waitForSelector('#counter'),
    clickCounter: async () => {
      const counter = await popup.getCounter();
      await counter.click();
    },
    getCounterText: async () => {
      const counter = await popup.getCounter();
      return await counter.evaluate((el) => el.textContent);
    },
  };
  return popup;
}

Chromium doesn't support opening popups in the native UI location under the action during testing. Instead, you have to open the popup's URL in a new tab.

And after all that work, we can write a simple test:

e2e/tests/popup-counter.test.ts
import { test, expect } from 'vitest';
import { openPopup } from '../pages/popup';

test('Popup counter increments when clicked', async () => {
  const popup = await openPopup();
  expect(await popup.getCounterText()).toEqual('count is 0');

  await popup.clickCounter();
  expect(await popup.getCounterText()).toEqual('count is 1');

  await popup.clickCounter();
  expect(await popup.getCounterText()).toEqual('count is 2');
});

Running Tests

To run the tests, build the extension for production and run the e2e script we added at the very beginning of this walkthrough.

pnpm build && pnpm e2e

The test should pass!

Closing Remarks

  • The custom environment is designed to reset your application state completely between test files, following best practices for E2E testing
  • Chromium doesn't support installing extensions in headless mode, meaning you always have to open a window
    • To run the tests in CI, you can integrate xvfb into the custom environment
  • You may want to consider adding the testTimeout and maxWorkers options in Vitest