.cursor/skills/sf-lwc-testing/SKILL.md
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.
npx skillsauth add jiten-singh-shahi/salesforce-claude-code sf-lwc-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
development
Update Salesforce platform reference docs with latest release features and deprecation announcements. Use when SessionStart hook warns docs are outdated or a new Salesforce release has shipped. Do NOT use for Apex or LWC development.
development
Use when syncing documentation after Salesforce Apex code changes. Update README, API docs, and deploy metadata references to match the current org codebase.
development
Use when managing context during long Salesforce Apex development sessions. Suggests manual compaction at logical intervals to preserve deploy and org context across phases.
tools
Visualforce development — pages, controllers, extensions, ViewState, JS Remoting, LWC migration. Use when maintaining VF pages, building PDFs, or planning VF-to-LWC migration. Do NOT use for LWC, Aura, or Flow.