sf-lwc-testing
LWC Jest testing — component mounting, wire/Apex mocking, user interaction simulation, toast/navigation verification. Use when writing or debugging LWC Jest tests. Do NOT use for Apex or Flow testing.
LWC Testing with Jest
LWC uses Jest as its test runner. Salesforce provides @salesforce/sfdx-lwc-jest to handle Salesforce-specific imports. Tests run in Node.js — no browser, no Salesforce org.
When to Use
- When writing Jest unit tests for Lightning Web Components
- When mocking Wire adapters, Apex imperative calls, or navigation in LWC tests
- When debugging flaky or async LWC tests
- When setting up LWC test infrastructure for a new project
@../_reference/LWC_PATTERNS.md @../_reference/TESTING_STANDARDS.md
Setup Procedure
Step 1 — Install
npm install --save-dev @salesforce/sfdx-lwc-jest
Step 2 — jest.config.js
const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
module.exports = {
...jestConfig,
modulePathIgnorePatterns: ['<rootDir>/.localdevserver'],
testEnvironment: 'jsdom',
testMatch: ['**/__tests__/**/*.test.js'],
setupFiles: ['<rootDir>/jest.setup.js']
};
Step 3 — jest.setup.js
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(), unobserve: jest.fn(), disconnect: jest.fn()
}));
Step 4 — Test File Location
lwc/accountSearch/
accountSearch.html
accountSearch.js
__tests__/
accountSearch.test.js
Basic Component Rendering
import { createElement } from 'lwc';
import AccountSearch from 'c/accountSearch';
describe('c-account-search', () => {
afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
jest.clearAllMocks();
});
it('renders the search input', () => {
const element = createElement('c-account-search', { is: AccountSearch });
document.body.appendChild(element);
const input = element.shadowRoot.querySelector('lightning-input[type="search"]');
expect(input).not.toBeNull();
expect(input.label).toBe('Search Accounts');
});
it('renders with public @api property', () => {
const element = createElement('c-account-search', { is: AccountSearch });
element.maxRecords = 25;
document.body.appendChild(element);
expect(element.maxRecords).toBe(25);
});
});
Wire Adapter Mocking (Modern Pattern)
Use jest.mock() with the wire adapter directly. The deprecated registerApexTestWireAdapter pattern should not be used in new projects.
import { createElement } from 'lwc';
import AccountDetails from 'c/accountDetails';
import getAccountDetails from '@salesforce/apex/AccountsController.getAccountDetails';
jest.mock(
'@salesforce/apex/AccountsController.getAccountDetails',
() => ({ default: jest.fn() }),
{ virtual: true }
);
describe('c-account-details wire', () => {
afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
jest.clearAllMocks();
});
it('displays account name when wire returns data', async () => {
getAccountDetails.mockResolvedValue({
Id: '001000000000001AAA', Name: 'Acme Corporation'
});
const element = createElement('c-account-details', { is: AccountDetails });
element.recordId = '001000000000001AAA';
document.body.appendChild(element);
await Promise.resolve();
await Promise.resolve();
expect(element.shadowRoot.querySelector('.account-name').textContent)
.toBe('Acme Corporation');
});
it('displays error state when wire returns error', async () => {
getAccountDetails.mockRejectedValue({
body: { message: 'Record not found' }, status: 404
});
const element = createElement('c-account-details', { is: AccountDetails });
element.recordId = '001000000000001AAA';
document.body.appendChild(element);
await Promise.resolve();
await Promise.resolve();
expect(element.shadowRoot.querySelector('.error-container')).not.toBeNull();
});
});
Apex Imperative Call Mocking
jest.mock(
'@salesforce/apex/AccountSearchController.searchAccounts',
() => jest.fn(), // imperative: module IS the function
{ virtual: true }
);
import searchAccounts from '@salesforce/apex/AccountSearchController.searchAccounts';
it('calls Apex on search and displays results', async () => {
searchAccounts.mockResolvedValue([
{ Id: '001000000000001AAA', Name: 'Acme Corp' }
]);
const element = createElement('c-account-search', { is: AccountSearch });
document.body.appendChild(element);
// Trigger search
const input = element.shadowRoot.querySelector('lightning-input');
input.dispatchEvent(new CustomEvent('change', { detail: { value: 'Acme' } }));
element.shadowRoot.querySelector('lightning-button[label="Search"]').click();
await flushPromises();
expect(searchAccounts).toHaveBeenCalledWith({ searchTerm: 'Acme' });
const rows = element.shadowRoot.querySelectorAll('.account-row');
expect(rows).toHaveLength(1);
});
Key distinction: for imperative Apex, mock as () => jest.fn(). For wired Apex, mock as () => ({ default: jest.fn() }).
Async Testing — flushPromises
LWC re-renders are asynchronous. Use flushPromises instead of chaining multiple Promise.resolve() calls.
function flushPromises() {
return new Promise(resolve => setTimeout(resolve, 0));
}
User Interaction Simulation
Click Events and Custom Event Assertions
it('dispatches select event when button clicked', () => {
const element = createElement('c-account-card', { is: AccountCard });
element.account = { Id: '001000000000001AAA', Name: 'Test Corp' };
document.body.appendChild(element);
const handler = jest.fn();
element.addEventListener('accountselect', handler);
element.shadowRoot.querySelector('[data-id="view-btn"]').click();
expect(handler).toHaveBeenCalledTimes(1);
expect(handler.mock.calls[0][0].detail).toEqual({
accountId: '001000000000001AAA', accountName: 'Test Corp'
});
});
Input Value Changes
const input = element.shadowRoot.querySelector('lightning-input[type="search"]');
input.dispatchEvent(new CustomEvent('change', { detail: { value: 'Acme' } }));
await Promise.resolve();
Toast Notification Testing
import { ShowToastEventName } from 'lightning/platformShowToastEvent';
it('shows success toast after save', async () => {
const toastHandler = jest.fn();
element.addEventListener(ShowToastEventName, toastHandler);
element.shadowRoot.querySelector('[data-id="save-btn"]').click();
await flushPromises();
expect(toastHandler).toHaveBeenCalledTimes(1);
expect(toastHandler.mock.calls[0][0].detail.variant).toBe('success');
});
Navigation Mocking
it('navigates to record page on view', () => {
const { navigate } = require('lightning/navigation');
const element = createElement('c-account-card', { is: AccountCard });
element.account = { Id: '001000000000001AAA' };
document.body.appendChild(element);
element.shadowRoot.querySelector('[data-id="view"]').click();
expect(navigate).toHaveBeenCalledWith(
expect.objectContaining({
type: 'standard__recordPage',
attributes: expect.objectContaining({ recordId: '001000000000001AAA' })
})
);
});
What to Test vs What Not to Test
Test These
- Business logic in computed properties and event handlers
- Correct Apex method called with correct arguments
- Component response to wire data and errors
- User interactions and their effects on component state
- Edge cases: empty arrays, null values, error states
Do Not Test These
- LWC framework behavior (that
for:eachrenders items) - Base component rendering (that
lightning-buttonshows text) - CSS styling or implementation details (internal variable names)
Spring '26: Local Component Testing
Test LWC components in isolation without deploying to a scratch org.
npm install --save-dev @lwc/jest-preset
npx lwc-jest --watchAll=false
TypeScript LWC Testing (Spring '26 Experimental)
Spring '26 introduces experimental TypeScript support for LWC. Test .ts component files with these adjustments.
Jest Configuration
// jest.config.js — add ts transform
const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
module.exports = {
...jestConfig,
transform: {
...jestConfig.transform,
'^.+\\.ts$': ['@swc/jest'] // or 'ts-jest'
},
moduleFileExtensions: ['ts', 'js', 'html'],
testMatch: ['**/__tests__/**/*.test.(js|ts)']
};
Install: npm install --save-dev @swc/jest @swc/core (faster) or npm install --save-dev ts-jest typescript.
Typed Wire Adapter Mocking
import { createElement } from 'lwc';
import AccountList from 'c/accountList';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
jest.mock('@salesforce/apex/AccountController.getAccounts',
() => ({ default: jest.fn() }), { virtual: true });
const mockGetAccounts = getAccounts as jest.MockedFunction<typeof getAccounts>;
describe('c-account-list (TypeScript)', () => {
afterEach(() => {
while (document.body.firstChild) document.body.removeChild(document.body.firstChild);
jest.clearAllMocks();
});
it('renders typed account data', async () => {
mockGetAccounts.mockResolvedValue([
{ Id: '001xx0001', Name: 'Typed Corp', Industry: 'Technology' }
]);
const element = createElement('c-account-list', { is: AccountList });
document.body.appendChild(element);
await Promise.resolve();
await Promise.resolve();
const rows = element.shadowRoot.querySelectorAll('.account-row');
expect(rows).toHaveLength(1);
});
});
Typed Event Detail Assertions
it('dispatches typed custom event', () => {
const element = createElement('c-account-card', { is: AccountCard });
document.body.appendChild(element);
const handler = jest.fn();
element.addEventListener('select', handler);
element.shadowRoot.querySelector('[data-id="select-btn"]').click();
const detail = handler.mock.calls[0][0].detail as { accountId: string };
expect(detail.accountId).toBeDefined();
});
Note: TypeScript LWC support is experimental in Spring '26. Type definitions and tooling may change in future releases. Pin
@salesforce/sfdx-lwc-jestto a known-good version.
Related
Guardrails
- sf-testing-constraints — Enforced rules for test coverage, assertions, and test data patterns
Agents
- sf-lwc-agent — For interactive, in-depth LWC review guidance