Skip to content

Chrome fakes

The fakes are installed by the Vitest preset or by calling installChromeFakes() directly. Each namespace is wrapped in a Proxy that throws on access to unmodeled members.


Import: import { createRuntimeFake } from 'extforge/testing';

MemberTypeNotes
chrome.runtime.idstringFixed to 'extforge-test-extension-id'
chrome.runtime.onInstalledevent.addListener / .removeListener
chrome.runtime.onStartupevent.addListener / .removeListener
chrome.runtime.onMessageevent.addListener / .removeListener
chrome.runtime.sendMessagespyResolves undefined by default
chrome.runtime.reloadspyNo-op

getURL, connect, onConnect, getManifest, openOptionsPage, and all other runtime.* members throw the not-modeled trap error.

ControlDescription
fakes.runtime.fireOnInstalled(details?)Calls all onInstalled listeners. Default details: { reason: 'install' }.
fakes.runtime.fireOnStartup()Calls all onStartup listeners.
fakes.runtime.fireOnMessage(message, sender?)Calls all onMessage listeners and returns a Promise that resolves to the sendResponse argument. If no listener calls sendResponse, resolves undefined.
fakes.runtime.reset()Clears all listeners and resets spy call counts.
import { fakes } from 'extforge/testing/vitest';
import { expect, it } from 'vitest';
it('fires onInstalled with reason update', () => {
let receivedReason = '';
chrome.runtime.onInstalled.addListener(({ reason }) => {
receivedReason = reason;
});
fakes.runtime.fireOnInstalled({ reason: 'update' });
expect(receivedReason).toBe('update');
});

Import: import { createStorageFake } from 'extforge/testing';

Three storage areas: chrome.storage.local, chrome.storage.sync, and chrome.storage.session. Each area models:

MethodBehavior
get(keys?)Returns matching keys from in-memory state. Pass null or omit for all keys.
set(items)Merges items into in-memory state.
remove(keys)Deletes keys from state.
clear()Empties the state object.

All methods are spies with .calls and .reset().

chrome.storage.managed, chrome.storage.onChanged, and any method other than the four above.

ControlDescription
fakes.storage.chrome.local.__state()Returns a snapshot of the current local storage state. Same for sync and session.
fakes.storage.reset()Clears state and resets call counts for all three areas.
import { fakes } from 'extforge/testing/vitest';
import { expect, it } from 'vitest';
it('persists storage across calls within a test', async () => {
await chrome.storage.local.set({ theme: 'dark' });
await chrome.storage.local.set({ font: 'mono' });
const all = fakes.storage.chrome.local.__state();
expect(all).toEqual({ theme: 'dark', font: 'mono' });
});

Import: import { createTabsFake } from 'extforge/testing';

MethodBehavior
chrome.tabs.query(info)Filters the seeded tab list by active and url.
chrome.tabs.sendMessage(tabId, message)Spy; resolves undefined.
chrome.tabs.create(props)Adds a new tab to the internal list; returns the tab record.
chrome.tabs.reload(tabId)Spy; no-op.

get, update, remove, onActivated, onUpdated, onRemoved, and all other tabs.* members.

ControlDescription
fakes.tabs.__seed(tabs)Replaces the tab list. Each entry: { id, url, active? }. Repeated calls overwrite.
fakes.tabs.reset()Clears the tab list and resets all spy call counts.
import { fakes } from 'extforge/testing/vitest';
import { expect, it, beforeEach } from 'vitest';
beforeEach(() => {
fakes.tabs.__seed([
{ id: 100, url: 'https://example.com/', active: true },
{ id: 101, url: 'https://other.com/', active: false },
]);
});
it('finds the active tab', async () => {
const [tab] = await chrome.tabs.query({ active: true });
expect(tab!.id).toBe(100);
});

Import: import { createActionFake } from 'extforge/testing';

MethodBehavior
chrome.action.setBadgeText({ text, tabId? })Stores text keyed by tabId (or 'global').
chrome.action.getBadgeText({ tabId? })Returns stored text for that key.
chrome.action.setIcon(details)Spy; no-op.
chrome.action.enable(tabId?)Spy; no-op.
chrome.action.disable(tabId?)Spy; no-op.

setPopup, getPopup, setBadgeBackgroundColor, openPopup, and all other action.* members.

ControlDescription
fakes.action.reset()Clears badge state and resets spy call counts.
import { fakes } from 'extforge/testing/vitest';
import { expect, it } from 'vitest';
it('sets and reads badge text', async () => {
await chrome.action.setBadgeText({ text: '5' });
const text = await chrome.action.getBadgeText({});
expect(text).toBe('5');
});

Import: import { createScriptingFake } from 'extforge/testing';

MethodBehavior
chrome.scripting.executeScript(injection)Returns [{ result: undefined, frameId: 0 }] by default. Use __nextResult to queue a specific return value.

insertCSS, removeCSS, registerContentScripts, getRegisteredContentScripts, and all other scripting.* members.

ControlDescription
fakes.scripting.__nextResult(value)Queues value as the result field of the next executeScript call. Multiple calls queue up in order.
fakes.scripting.reset()Clears the queue and resets spy call counts.
import { fakes } from 'extforge/testing/vitest';
import { expect, it } from 'vitest';
it('returns the queued executeScript result', async () => {
fakes.scripting.__nextResult(42);
const [frame] = await chrome.scripting.executeScript({
target: { tabId: 1 },
func: () => 42,
});
expect(frame!.result).toBe(42);
});

Accessing any member not listed above throws:

chrome.<ns>.<method> is not modeled by extforge/testing v1;
supply your own mock or extend the fake.
Docs: https://extforge.arshadshah.com/testing#unmodeled

To add a stub for an unmodeled method in a test file:

// Extend globally for the duration of the test file
(globalThis as any).chrome.history = {
search: vi.fn().mockResolvedValue([]),
};

Or, use the per-namespace factory and spread your method before passing the fake to your code:

import { createTabsFake } from 'extforge/testing';
const tabs = createTabsFake();
const extendedChrome = {
...tabs.chrome,
get: vi.fn().mockResolvedValue({ id: 1, url: 'https://example.com' }),
};