Covers unit testing Lightning Web Components with @salesforce/sfdx-lwc-jest - jest.config.js setup and the sfdx-lwc-jest preset, createElement/appendChild test anatomy, DOM cleanup, flushing async rerenders, querying shadow and light DOM, asserting dispatched CustomEvents, mocking Apex methods and @salesforce scoped imports with jest.mock, driving wire adapters with createApexTestWireAdapter/createLdsTestWireAdapter/createTestWireAdapter, stubbing lightning/navigation, lightning/messageServic...
Scanned 9/19/2026
Install to Claude Code
npx -y skills add grzmol/vibe-force --skill sf-lwc-jest-testing --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Sf Lwc Jest Testing?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/grzmol-sf-lwc-jest-testing)More formats (shields.io, HTML) on the badges page.
---
name: sf-lwc-jest-testing
description: Covers unit testing Lightning Web Components with @salesforce/sfdx-lwc-jest - jest.config.js setup and the sfdx-lwc-jest preset, createElement/appendChild test anatomy, DOM cleanup, flushing async rerenders, querying shadow and light DOM, asserting dispatched CustomEvents, mocking Apex methods and @salesforce scoped imports with jest.mock, driving wire adapters with createApexTestWireAdapter/createLdsTestWireAdapter/createTestWireAdapter, stubbing lightning/navigation, lightning/messageService, toasts and base components, coverage configuration against the jestCoverageMin gate, and debugging failing suites. Use this skill whenever a __tests__ directory under force-app/**/lwc/** is created or changed, when vf-check jest fails, when a component test needs wire or Apex data, or when deciding what an LWC test should assert.
---
# LWC Jest Testing
## When to use
| Situation | Use this skill |
| --- | --- |
| Adding or fixing a `__tests__/*.test.js` file for an LWC | Yes |
| `vf-check jest` fails or coverage is below the gate | Yes |
| A component needs wire data, Apex results, or navigation asserted in a test | Yes |
| Writing the component itself | Skill `sf-lwc-development` |
| Apex unit tests (`@IsTest`) | Skill `sf-apex-testing` |
| Verifying behaviour in a real org after deploy | Skill `sf-post-deploy-verification` |
| Sharing one set of states between the spec and a `sf lightning dev component` preview | Skill `sf-local-development`, pattern 9 |
| Specs generated by DX MCP tools or the `experience-lwc-test` skill | Yes - review them against this skill; see `sf-lwc-development/references/mcp-and-skills.md` |
Jest runs entirely offline: no org, no authentication, no network. It is the cheapest gate in the
pipeline and runs in wave 2 as part of `vf-check local`.
## Quick reference
| Task | API |
| --- | --- |
| Instantiate | `const el = createElement('c-foo', { is: Foo }); document.body.appendChild(el);` |
| Set a public property | Assign before `appendChild` for initial render, after for a rerender |
| Clean up | `while (document.body.firstChild) document.body.removeChild(document.body.firstChild);` in `afterEach` |
| Wait for rerender | `await Promise.resolve();` or a `flushPromises()` helper |
| Query shadow DOM | `el.shadowRoot.querySelector('.name')`, `querySelectorAll` |
| Query light DOM component | `el.querySelector(...)` - there is no `shadowRoot` |
| Assert a dispatched event | `el.addEventListener('rowselect', handler)` then assert `handler.mock.calls[0][0].detail` |
| Mock an Apex method | `jest.mock('@salesforce/apex/Cls.method', () => ({ default: jest.fn() }), { virtual: true })` |
| Emit wire data | `adapter.emit(data)` from a `__mocks__` module built with `createLdsTestWireAdapter` |
| Emit a wire error | `adapter.error(body, status, statusText)` or `adapter.emitError({...})` |
| Inspect the resolved wire config | `adapter.getLastConfig()` |
| Control time | `jest.useFakeTimers()` + `jest.advanceTimersByTime(ms)` |
| Run one file | `npm run test:unit -- src/path/foo.test.js` |
## Core patterns
### 1. Project setup
```json
{
"devDependencies": {
"@salesforce/sfdx-lwc-jest": "^7.0.0"
},
"scripts": {
"test": "npm run test:unit",
"test:unit": "sfdx-lwc-jest",
"test:unit:watch": "sfdx-lwc-jest --watch",
"test:unit:debug": "sfdx-lwc-jest --debug",
"test:unit:coverage": "sfdx-lwc-jest --coverage"
}
}
```
```javascript
// jest.config.js at the root of the Salesforce DX project
const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
module.exports = {
...jestConfig,
moduleNameMapper: {
'^@salesforce/apex$': '<rootDir>/force-app/test/jest-mocks/apex',
'^lightning/navigation$': '<rootDir>/force-app/test/jest-mocks/lightning/navigation',
'^lightning/platformShowToastEvent$':
'<rootDir>/force-app/test/jest-mocks/lightning/platformShowToastEvent',
'^lightning/messageService$': '<rootDir>/force-app/test/jest-mocks/lightning/messageService',
'^lightning/uiRecordApi$': '<rootDir>/force-app/test/jest-mocks/lightning/uiRecordApi'
},
collectCoverageFrom: ['force-app/main/default/lwc/**/*.js', '!**/__tests__/**'],
coverageThreshold: { global: { lines: 80, statements: 80 } },
testTimeout: 10000
};
```
`sfdx-lwc-jest` configures Jest for the DX workspace out of the box; `jest.config.js` is only needed
to override. Stubs for every `lightning` namespace base component ship with the package and are used
automatically. The `coverageThreshold` mirrors `gates.jestCoverageMin` (80) from
`.vibeforce/config.json`; `vf-check jest` enforces the same number independently.
### 2. Test anatomy
```javascript
// force-app/main/default/lwc/hello/__tests__/hello.test.js
import { createElement } from 'lwc';
import Hello from 'c/hello';
describe('c-hello', () => {
afterEach(() => {
// The jsdom instance is shared across test cases in a single file, so reset the DOM
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
jest.clearAllMocks();
});
it('displays the greeting', () => {
const element = createElement('c-hello', { is: Hello });
document.body.appendChild(element);
const div = element.shadowRoot.querySelector('div');
expect(div.textContent).toBe('Hello, World!');
});
});
```
Rules:
- The tag name passed to `createElement` must be the kebab-case name with the `c-` namespace.
- Nothing renders until `document.body.appendChild(element)`.
- Never share an element between `it` blocks.
### 3. Asynchronous rerenders
```javascript
// helper used across the suite
async function flushPromises() {
return Promise.resolve();
}
it('updates the label when the property changes', async () => {
const element = createElement('c-counter', { is: Counter });
document.body.appendChild(element);
element.count = 5; // property set after insertion
await flushPromises(); // the rerender is enqueued as a microtask
expect(element.shadowRoot.querySelector('.label').textContent).toBe('5');
});
```
One `await Promise.resolve()` flushes one microtask turn. Chained promises (an imperative Apex call
that then triggers a rerender) need one flush per turn, or `await Promise.resolve().then(() => Promise.resolve())`.
Never use `setTimeout` to "wait for" a rerender.
### 4. Mocking Apex
```javascript
import { createElement } from 'lwc';
import CaseList from 'c/caseList';
import getCases from '@salesforce/apex/CaseController.getCases';
// The LWC Jest transformer resolves @salesforce/apex/* modules; declare the mock as virtual.
jest.mock(
'@salesforce/apex/CaseController.getCases',
() => ({ default: jest.fn() }),
{ virtual: true }
);
const MOCK_CASES = [{ Id: '500xx0000000001', Subject: 'Broken widget' }];
describe('c-case-list imperative Apex', () => {
afterEach(() => {
while (document.body.firstChild) document.body.removeChild(document.body.firstChild);
jest.clearAllMocks();
});
it('renders rows on success', async () => {
getCases.mockResolvedValue(MOCK_CASES);
const element = createElement('c-case-list', { is: CaseList });
document.body.appendChild(element);
element.shadowRoot.querySelector('lightning-button').click();
await Promise.resolve();
await Promise.resolve();
expect(getCases).toHaveBeenCalledWith({ ownerId: undefined, maxRows: 25 });
expect(element.shadowRoot.querySelectorAll('tr')).toHaveLength(1);
});
it('renders an error panel on failure', async () => {
getCases.mockRejectedValue({ body: { message: 'Insufficient access' }, ok: false, status: 400 });
const element = createElement('c-case-list', { is: CaseList });
document.body.appendChild(element);
element.shadowRoot.querySelector('lightning-button').click();
await Promise.resolve();
await Promise.resolve();
expect(element.shadowRoot.querySelector('c-error-panel')).not.toBeNull();
});
});
```
Assert both the success and the failure path for every server call - the error branch is where
production bugs live.
### 5. Driving wire adapters
`@salesforce/sfdx-lwc-jest` re-exports the `@salesforce/wire-service-jest-util` APIs, so no extra
dependency is required.
| Factory | Use for | Emitted shape |
| --- | --- | --- |
| `createLdsTestWireAdapter(fn)` | `getRecord`, `getObjectInfo`, `getPicklistValues`, `graphql` | `{ data, error }`, default error `404 NOT_FOUND` |
| `createApexTestWireAdapter(fn)` | `@wire(apexMethod)` | `{ data, error }`, default error `400 Bad Request` |
| `createTestWireAdapter(fn)` | `CurrentPageReference`, `MessageContext`, custom adapters | Raw value, as emitted |
```javascript
import { createElement } from 'lwc';
import AccountCard from 'c/accountCard';
import { getRecord } from 'lightning/uiRecordApi';
import getOpenCases from '@salesforce/apex/AccountCaseController.getOpenCases';
const mockGetRecord = require('./data/getRecord.json');
jest.mock(
'@salesforce/apex/AccountCaseController.getOpenCases',
() => {
const { createApexTestWireAdapter } = require('@salesforce/sfdx-lwc-jest');
return { default: createApexTestWireAdapter(jest.fn()) };
},
{ virtual: true }
);
it('renders the account name from the wire', async () => {
const element = createElement('c-account-card', { is: AccountCard });
element.recordId = '001xx000003DGg0AAG';
document.body.appendChild(element);
getRecord.emit(mockGetRecord); // lightning/uiRecordApi is stubbed as a test adapter
getOpenCases.emit([{ Id: '500xx1', Subject: 'Late shipment' }]);
await Promise.resolve();
expect(element.shadowRoot.querySelector('.name').textContent).toBe('Acme');
expect(getRecord.getLastConfig().recordId).toBe('001xx000003DGg0AAG');
});
```
Error paths: `getRecord.error()` emits the default not-found response;
`getRecord.emitError({ body: { message: 'Boom' }, status: 500, statusText: 'Server Error' })` emits
a specific one.
### 6. Asserting dispatched events
```javascript
it('dispatches rowselect with the record id', async () => {
const element = createElement('c-selector', { is: Selector });
document.body.appendChild(element);
const handler = jest.fn();
element.addEventListener('rowselect', handler);
element.shadowRoot.querySelector('[data-id="003xx1"]').click();
await Promise.resolve();
expect(handler).toHaveBeenCalledTimes(1);
expect(handler.mock.calls[0][0].detail).toEqual({ recordId: '003xx1' });
expect(handler.mock.calls[0][0].bubbles).toBe(false);
});
```
For toasts, listen for `ShowToastEventName` imported from `lightning/platformShowToastEvent`.
### 7. What to assert, and what not to
| Assert | Do not assert |
| --- | --- |
| Rendered text, element counts, and attributes a user perceives | Internal field names or private method calls |
| Which branch renders for a given input (loading / data / empty / error) | That a getter returns what the field holds |
| Event name, `detail`, and propagation flags | That `dispatchEvent` was called |
| Apex/LDS call arguments (they are the cross-slice contract) | Base component internals - they are stubs |
| Accessibility attributes the template promises (`alternative-text`, `aria-*`) | CSS class strings with no behavioural meaning |
A test that would still pass after the component is broken is worse than no test. Snapshot tests
are allowed only for stable presentational markup; a snapshot that is regenerated with `-u` every
sprint is noise and must be deleted.
### 8. Timers and intervals
```javascript
it('ticks every second', () => {
jest.useFakeTimers();
const element = createElement('c-clock', { is: Clock });
document.body.appendChild(element);
jest.advanceTimersByTime(3000);
expect(element.shadowRoot.querySelector('.tick').textContent).toBe('3');
document.body.removeChild(element);
jest.advanceTimersByTime(3000); // proves disconnectedCallback cleared the interval
expect(element.shadowRoot.querySelector('.tick').textContent).toBe('3');
jest.useRealTimers();
});
```
## Anti-patterns
| Anti-pattern | Consequence | Fix |
| --- | --- | --- |
| No `afterEach` DOM cleanup | jsdom is shared per file; the second test sees the first component | Remove every child of `document.body` |
| Asserting immediately after setting a property | Rerender is a microtask; assertion runs too early | `await Promise.resolve()` first |
| `setTimeout(..., 0)` to wait for a rerender | Flaky and slow | Flush promises, or fake timers for real timers |
| `jest.mock('@salesforce/apex/...')` without `{ virtual: true }` | Module not found - the path is synthetic | Add the virtual option |
| Mocking `lightning-button` with a hand-written stub | Base component stubs ship with the package | Use the built-in stubs; override only via `moduleNameMapper` |
| Testing a wire by calling the Apex mock directly | The adapter never provisions; the component stays empty | Emit through the test wire adapter |
| Leaving `console.error` noise from unhandled promises | Hides real failures | Await every promise the test triggers |
| `expect(component.privateField)` | Couples the test to the implementation | Assert rendered output or dispatched events |
| Snapshot-only test files | Zero diagnostic value on failure | Assert the specific nodes that matter |
| Tests that require org data | Jest has no org | Mock the adapter; move org assertions to `vf-check apex` / `smoke` |
## Verification
```bash
# All LWC tests
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" jest
# Only tests affected by the current branch
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" jest --changed
# Full local gate (format + lint + analyzer + jest)
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" local --changed
# Direct invocations while iterating
npm run test:unit -- force-app/main/default/lwc/accountCard/__tests__/accountCard.test.js
npm run test:unit:watch
npm run test:unit:coverage
npm run test:unit:debug # then open chrome://inspect
sfdx-lwc-jest -- --runInBand # everything after -- goes straight to Jest
```
`vf-check jest` fails the wave-2 gate when any test fails, when coverage is below
`gates.jestCoverageMin` (80), or when `gates.requireJestForLwc` is true and a bundle has no
`__tests__` directory. No org is contacted; exit code 1 means a real gate failure, 2 means the
toolchain is missing (run `npm install` in the consumer project).
## References
- [`references/jest-setup.md`](references/jest-setup.md) - full `jest.config.js`, npm scripts, `__mocks__` layout, CI invocation, coverage configuration.
- [`references/mocking-cookbook.md`](references/mocking-cookbook.md) - Apex, LDS, GraphQL, navigation, LMS, toast, labels, base components, fetch.
- [`references/test-recipes.md`](references/test-recipes.md) - complete test files for the component shapes this harness builds.
- [`references/troubleshooting-jest.md`](references/troubleshooting-jest.md) - failure-to-cause table and debugging workflow.
- Official: [sfdx-lwc-jest README](https://github.com/salesforce/sfdx-lwc-jest), [wire-service-jest-util README](https://github.com/salesforce/wire-service-jest-util), [Test Lightning Web Components](https://developer.salesforce.com/docs/platform/lwc/guide/unit-testing-using-jest-create-tests.html), [Jest 30 documentation](https://jestjs.io/docs/30.0/getting-started).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!