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.


runtime

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

What it models

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

What it does not model

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

Test-side controls

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.

Example

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');
});

storage

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

What it models

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().

What it does not model

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

Test-side controls

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.

Example

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' });
});

tabs

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

What it models

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.

What it does not model

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

Test-side controls

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.

Example

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);
});

action

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

What it models

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.

What it does not model

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

Test-side controls

ControlDescription
fakes.action.reset()Clears badge state and resets spy call counts.

Example

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');
});

scripting

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

What it models

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

What it does not model

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

Test-side controls

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.

Example

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);
});

The not-modeled trap

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' }),
};