agentleFS
jiten-singh-shahijiten-singh-shahi/salesforce-claude-code/.cursor/skills/sf-lwc-testing/SKILL.md

sf-lwc-testing

Skill15 starsChanged 7 months agoMITView on GitHub

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.

  • Installs packages

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 5 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When writing Jest unit tests for Lightning Web Components

    line 13
  2. Distinctive:

    When mocking Wire adapters, Apex imperative calls, or navigation in LWC tests

    line 14
  3. Distinctive:

    When debugging flaky or async LWC tests

    line 15
  4. Distinctive:

    When setting up LWC test infrastructure for a new project

    line 16
  5. Very specific:

    Use jest.mock() with the wire adapter directly.

    line 100
  6. Distinctive:

    Test LWC components in isolation without deploying to a scratch org.

    line 294

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:each renders items)
  • Base component rendering (that lightning-button shows 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-jest to 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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.