Skip to content

Unit Testing ​

Vitest ​

WXT provides first class support for Vitest for unit testing:

ts
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { WxtVitest } from 'wxt/testing/vitest-plugin';

export default defineConfig({
  plugins: [WxtVitest()],
});

This plugin does several things:

  • Polyfills the extension API, browser, with an in-memory implementation using @webext-core/fake-browser
  • Adds all vite config or plugins in wxt.config.ts
  • Configures auto-imports (if enabled)
  • Sets up global variables provided by WXT (import.meta.env.BROWSER, import.meta.env.MANIFEST_VERSION, import.meta.env.IS_CHROME, etc)
  • Configures aliases (@/*, @@/*, etc) so imports can be resolved

Here are real projects with unit testing setup. Look at the code and tests to see how they're written.

Example Tests ​

This example demonstrates that you don't have to mock browser.storage (used by wxt/utils/storage) in tests - @webext-core/fake-browser implements storage in-memory so it behaves like it would in a real extension!

ts
import { describe, it, expect } from 'vitest';
import { fakeBrowser } from 'wxt/testing/fake-browser';

const accountStorage = storage.defineItem<Account>('local:account');

async function isLoggedIn(): Promise<Account> {
  const value = await accountStorage.getValue();
  return value != null;
}

describe('isLoggedIn', () => {
  beforeEach(() => {
    // See https://webext-core.aklinker1.io/fake-browser/reseting-state
    fakeBrowser.reset();
  });

  it('should return true when the account exists in storage', async () => {
    const account: Account = {
      username: '...',
      preferences: {
        // ...
      },
    };
    await accountStorage.setValue(account);

    expect(await isLoggedIn()).toBe(true);
  });

  it('should return false when the account does not exist in storage', async () => {
    await accountStorage.deleteValue();

    expect(await isLoggedIn()).toBe(false);
  });
});

Mocking WXT APIs ​

First, you need to understand how the #imports module works. When WXT (and vitest) sees this import during a preprocessing step, the import is replaced with multiple imports pointing to their "real" import path.

For example, this is what your write in your source code:

ts
// What you write
import { injectScript, createShadowRootUi } from '#imports';

But Vitest sees this:

ts
import { injectScript } from 'wxt/utils/inject-script';
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';

So in this case, if you wanted to mock injectScript, you need to pass in "wxt/utils/inject-script", not "#imports".

ts
vi.mock("wxt/utils/inject-script", () => ({
  injectScript: ...
}))

Refer to your project's .wxt/types/imports-module.d.ts file to lookup real import paths for #imports. If the file doesn't exist, run wxt prepare.

Other Testing Frameworks ​

To use a different framework, you will likely have to disable auto-imports, setup import aliases, manually mock the extension APIs, and setup the test environment to support all of WXT's features that you use.

It is possible to do, but will require a bit more setup. Refer to Vitest's setup for an example of how to setup a test environment:

https://github.com/wxt-dev/wxt/blob/main/packages/wxt/src/testing/wxt-vitest-plugin.ts