Guides LWC Jest unit testing: component rendering/mounting, wire adapter/Apex mocking, user interaction simulation, toast/navigation verification. For writing/debugging LWC tests.
npx claudepluginhub jiten-singh-shahi/salesforce-claude-code --plugin salesforce-claude-codeThis skill uses the workspace's default tool permissions.
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.
Explains Salesforce LWC Winter '26 features: lightning/graphql module migration from uiGraphQLApi, sf lightning dev component for local development, and platform module access in previews.
Enforces quality standards for Salesforce LWC, Aura, and Visualforce components: SLDS 2 compliance, WCAG 2.1 AA accessibility, data access patterns, security (XSS/CSRF/FLS/CRUD), and Jest tests. Use when building or reviewing UI components.
Builds Lightning Web Components for Salesforce B2B Commerce storefronts using @api/@track/@wire decorators, commerce-specific wire adapters, custom events, Lightning Web Security, Jest unit testing, and component lifecycle.
Share bugs, ideas, or general feedback.
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.
@../_reference/LWC_PATTERNS.md @../_reference/TESTING_STANDARDS.md
npm install --save-dev @salesforce/sfdx-lwc-jest
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']
};
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(), unobserve: jest.fn(), disconnect: jest.fn()
}));
lwc/accountSearch/
accountSearch.html
accountSearch.js
__tests__/
accountSearch.test.js
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);
});
});
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();
});
});
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() }).
LWC re-renders are asynchronous. Use flushPromises instead of chaining multiple Promise.resolve() calls.
function flushPromises() {
return new Promise(resolve => setTimeout(resolve, 0));
}
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'
});
});
const input = element.shadowRoot.querySelector('lightning-input[type="search"]');
input.dispatchEvent(new CustomEvent('change', { detail: { value: 'Acme' } }));
await Promise.resolve();
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');
});
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' })
})
);
});
for:each renders items)lightning-button shows text)Test LWC components in isolation without deploying to a scratch org.
npm install --save-dev @lwc/jest-preset
npx lwc-jest --watchAll=false
Spring '26 introduces experimental TypeScript support for LWC. Test .ts component files with these adjustments.
// 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.
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);
});
});
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.