|
| 1 | +# webext-core - Fake Browser |
| 2 | + |
| 3 | +Package: `@webext-core/fake-browser` |
| 4 | + |
| 5 | +An in-memory implementation of `webextension-polyfill` for unit testing. Implements the `browser` APIs without requiring a real browser environment. |
| 6 | + |
| 7 | +## Setup (Standalone / Non-WXT) |
| 8 | + |
| 9 | +```ts |
| 10 | +// vitest.setup.ts |
| 11 | +import { fakeBrowser } from '@webext-core/fake-browser'; |
| 12 | + |
| 13 | +// Replace the global browser mock with the fake |
| 14 | +vi.mock('webextension-polyfill'); |
| 15 | +globalThis.browser = fakeBrowser; |
| 16 | +``` |
| 17 | + |
| 18 | +## Setup (WXT) |
| 19 | + |
| 20 | +WXT's `WxtVitest()` plugin configures `fakeBrowser` automatically. Import it from: |
| 21 | + |
| 22 | +```ts |
| 23 | +import { fakeBrowser } from 'wxt/testing'; |
| 24 | +``` |
| 25 | + |
| 26 | +## Usage in Tests |
| 27 | + |
| 28 | +```ts |
| 29 | +import { describe, it, expect, beforeEach, vi } from 'vitest'; |
| 30 | +import { fakeBrowser } from '@webext-core/fake-browser'; |
| 31 | + |
| 32 | +vi.mock('webextension-polyfill'); |
| 33 | + |
| 34 | +describe('my service', () => { |
| 35 | + beforeEach(() => { |
| 36 | + fakeBrowser.reset(); // Always reset state between tests |
| 37 | + }); |
| 38 | + |
| 39 | + it('stores and retrieves data', async () => { |
| 40 | + await browser.storage.local.set({ key: 'value' }); |
| 41 | + const result = await browser.storage.local.get('key'); |
| 42 | + expect(result).toEqual({ key: 'value' }); |
| 43 | + }); |
| 44 | + |
| 45 | + it('creates and reads alarms', async () => { |
| 46 | + await browser.alarms.create('test', { delayInMinutes: 1 }); |
| 47 | + const alarm = await browser.alarms.get('test'); |
| 48 | + expect(alarm?.name).toBe('test'); |
| 49 | + }); |
| 50 | +}); |
| 51 | +``` |
| 52 | + |
| 53 | +## Implemented APIs |
| 54 | + |
| 55 | +| API | Status | Notes | |
| 56 | +|---|---|---| |
| 57 | +| `browser.alarms` | Full | `create`, `get`, `getAll`, `clear`, `clearAll`, `onAlarm` | |
| 58 | +| `browser.notifications` | Full | `onClosed`, `onClicked`, `onButtonClicked`, `onShown` | |
| 59 | +| `browser.runtime` | Partial | `id`, `getURL`, `sendMessage`, `onMessage`, `onInstalled`, `onStartup`, `onSuspend`, `onSuspendCanceled`, `onUpdateAvailable` | |
| 60 | +| `browser.storage` | Full | `local`, `session`, `sync`, `managed`; all four support `get`, `set`, `remove`, `clear`, `onChanged` | |
| 61 | +| `browser.tabs` | Partial | `get`, `getCurrent`, `create`, `duplicate`, `query`, `highlight`, `update`, `remove`; `onCreated`, `onUpdated`, `onActivated`, `onHighlighted`, `onRemoved` | |
| 62 | +| `browser.windows` | Partial | `get`, `getAll`, `create`, `getCurrent`, `getLastFocused`, `update`, `remove`; `onCreated`, `onRemoved`, `onFocusChanged` | |
| 63 | +| `browser.webNavigation` | Events only | `onBeforeNavigate`, `onCommitted`, `onDOMContentLoaded`, `onCompleted`, `onErrorOccurred`, `onCreatedNavigationTarget`, `onReferenceFragmentUpdated`, `onTabReplaced`, `onHistoryStateUpdated` | |
| 64 | + |
| 65 | +## `EventForTesting` — Triggering Events in Tests |
| 66 | + |
| 67 | +Every implemented event has a `.trigger(...args)` method that manually fires the event and returns a `Promise` resolving to an array of all listener return values. |
| 68 | + |
| 69 | +```ts |
| 70 | +import { fakeBrowser } from '@webext-core/fake-browser'; |
| 71 | +import { vi, expect, it } from 'vitest'; |
| 72 | + |
| 73 | +it('handles tab creation', async () => { |
| 74 | + const handler = vi.fn(); |
| 75 | + browser.tabs.onCreated.addListener(handler); |
| 76 | + |
| 77 | + // Manually fire the event |
| 78 | + await fakeBrowser.tabs.onCreated.trigger({ |
| 79 | + id: 1, |
| 80 | + url: 'https://example.com', |
| 81 | + active: true, |
| 82 | + index: 0, |
| 83 | + pinned: false, |
| 84 | + highlighted: false, |
| 85 | + windowId: 1, |
| 86 | + incognito: false, |
| 87 | + }); |
| 88 | + |
| 89 | + expect(handler).toHaveBeenCalledOnce(); |
| 90 | +}); |
| 91 | + |
| 92 | +it('handles alarm firing', async () => { |
| 93 | + const handler = vi.fn(); |
| 94 | + browser.alarms.onAlarm.addListener(handler); |
| 95 | + |
| 96 | + await fakeBrowser.alarms.onAlarm.trigger({ name: 'my-alarm', scheduledTime: Date.now() }); |
| 97 | + |
| 98 | + expect(handler).toHaveBeenCalledWith({ name: 'my-alarm', scheduledTime: expect.any(Number) }); |
| 99 | +}); |
| 100 | + |
| 101 | +it('handles runtime.onInstalled', async () => { |
| 102 | + const handler = vi.fn(); |
| 103 | + browser.runtime.onInstalled.addListener(handler); |
| 104 | + |
| 105 | + await fakeBrowser.runtime.onInstalled.trigger({ reason: 'install' }); |
| 106 | + |
| 107 | + expect(handler).toHaveBeenCalledWith({ reason: 'install' }); |
| 108 | +}); |
| 109 | +``` |
| 110 | + |
| 111 | +## `reset()` Between Tests |
| 112 | + |
| 113 | +`fakeBrowser.reset()` clears all in-memory state: |
| 114 | + |
| 115 | +- All stored data in `storage.local`, `storage.session`, `storage.sync`, `storage.managed` |
| 116 | +- All registered alarms |
| 117 | +- All tabs (resets to a default tab) |
| 118 | +- All windows (resets to a default window) |
| 119 | +- All event listeners on every implemented API |
| 120 | +- Resets `runtime.id` to `'test-extension-id'` |
| 121 | + |
| 122 | +```ts |
| 123 | +beforeEach(() => { |
| 124 | + fakeBrowser.reset(); |
| 125 | +}); |
| 126 | +``` |
| 127 | + |
| 128 | +Omitting this causes state to bleed between tests, producing hard-to-diagnose failures. |
| 129 | + |
| 130 | +## `runtime.id` and `runtime.getURL` |
| 131 | + |
| 132 | +The fake runtime uses `'test-extension-id'` as the extension ID by default: |
| 133 | + |
| 134 | +```ts |
| 135 | +browser.runtime.id; // 'test-extension-id' |
| 136 | +browser.runtime.getURL('/icons/icon.png'); |
| 137 | +// 'chrome-extension://test-extension-id/icons/icon.png' |
| 138 | +``` |
0 commit comments