Unit Testing
Vitest
WXT provides first class support for Vitest for unit testing:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { WxtVitest } from 'wxt/testing';
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)
- Applies internal WXT vite plugins for things like bundling remote code
- 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!
import { describe, it, expect } from 'vitest';
import { fakeBrowser } from 'wxt/testing';
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:
// What you write
import { injectScript, createShadowRootUi } from '#imports';
But Vitest sees this:
import { injectScript } from 'wxt/browser';
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"
.
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