Org Skills

salesforce-frontend-testing

Test Salesforce UI end to end — LWC unit tests with Jest (@salesforce/sfdx-lwc-jest), mocking @wire adapters, Apex, LDS, and Lightning Message Service, accessibility checks with sa11y, Apex test classes written for @AuraEnabled controllers (positive, negative, bulk, sharing), and browser/E2E testing of Lightning pages and Experience Cloud with UTAM or Playwright.

Download .zip Raw Source
When agents use itUse this whenever the user asks how to test an LWC, mock a wire, test Apex called from LWC, set up Jest for Salesforce, reach Apex coverage for a UI controller, write Playwright/UTAM tests against a Salesforce org, or fix flaky Salesforce UI tests. Also apply it when generating tests for LWC or @AuraEnabled Apex the user just wrote, even if they don't say "test".

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill salesforce-frontend-testing -a github-copilot
# or with the org installer (adds .github/skills/salesforce-frontend-testing):
npx -y github:AGCO-Global/org-skills add skill salesforce-frontend-testing

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill salesforce-frontend-testing
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill salesforce-frontend-testing -g

Installs the salesforce-lwc-skills plugin, which bundles all Salesforce / LWC skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install salesforce-lwc-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill salesforce-frontend-testing -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill salesforce-frontend-testing -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill salesforce-frontend-testing -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill salesforce-frontend-testing -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

Salesforce Frontend Testing

Salesforce UI has three test layers with three different runtimes: Jest in Node for LWC, Apex tests in the org for controllers, and a real browser against an org for the pieces only the platform can render. Know which layer a given bug lives in before writing a test.

1. LWC unit tests with Jest

Setup (once per project):

npm i -D @salesforce/sfdx-lwc-jest @sa11y/jest

jest.config.js:

const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
module.exports = {
  ...jestConfig,
  modulePathIgnorePatterns: ['<rootDir>/.localdevserver'],
  setupFilesAfterEach: ['<rootDir>/jest-sa11y-setup.js'],
  moduleNameMapper: {
    '^lightning/(.*)$': '<rootDir>/force-app/test/jest-mocks/lightning/$1',
  },
};

Tests live in lwc/<component>/__tests__/<component>.test.js. Mocks for lightning/* modules that sfdx-lwc-jest doesn't ship live in force-app/test/jest-mocks/.

Core pattern:

import { createElement } from 'lwc';
import OrderList from 'c/orderList';
import getOrders from '@salesforce/apex/OrderController.getOrders';

jest.mock('@salesforce/apex/OrderController.getOrders',
  () => ({ default: jest.fn() }), { virtual: true });

const flushPromises = () => new Promise(process.nextTick);

describe('c-order-list', () => {
  afterEach(() => {
    while (document.body.firstChild) document.body.removeChild(document.body.firstChild);
    jest.clearAllMocks();
  });

  it('renders rows returned by Apex', async () => {
    getOrders.emit([{ Id: '1', Name: 'ORD-1' }]);          // wired Apex
    const el = createElement('c-order-list', { is: OrderList });
    document.body.appendChild(el);
    await flushPromises();
    const rows = el.shadowRoot.querySelectorAll('lightning-datatable');
    expect(rows[0].data).toHaveLength(1);
  });
});

Mocking rules:

  • Wired Apexjest.mock('@salesforce/apex/...', () => ({ default: jest.fn() }), { virtual: true }), then getOrders.emit(data) / getOrders.error(err).
  • Imperative Apex → same mock; getOrders.mockResolvedValue(data) / mockRejectedValue(error).
  • LDS wires (getRecord, getObjectInfo) → the adapters from lightning/uiRecordApi are auto-mocked by sfdx-lwc-jest; getRecord.emit(mockRecord) with a JSON fixture in __tests__/data/.
  • @salesforce/schema, @salesforce/label, @salesforce/user/Id → auto-mocked; override with jest.mock when a value matters.
  • Lightning Message Servicepublish and subscribe from lightning/messageService are mocked; assert publish was called with the channel and payload; simulate incoming messages by calling the callback captured from subscribe.mock.calls.
  • lightning/navigation → mock NavigationMixin; assert Navigate was dispatched with the right pageReference.
  • Toasts → listen for ShowToastEvent on the element: el.addEventListener('lightning__showtoast', handler).
  • Third-party libs via loadScript → mock lightning/platformResourceLoader to resolve immediately and put a stub on window.

What to assert:

  • Rendered output in shadowRoot — text, attributes, data passed to base components, lwc:if branches.
  • Events dispatched (el.addEventListener('select', handler) then trigger the child event with child.dispatchEvent(new CustomEvent('click'))).
  • Apex mock called with the exact parameters.
  • Error branch: emit/mockRejectedValue, then the error UI or toast.
  • disconnectedCallback cleanup: remove element, assert unsubscribe called.

Accessibility in every component test:

import { setup } from '@sa11y/jest'; setup();          // in setup file
await expect(el).toBeAccessible();                    // in tests

Coverage: enforce ≥ 75 % on lwc/ in jest.config.js coverageThreshold; run sfdx-lwc-jest --coverage in CI.

2. Apex tests for UI controllers

Every @AuraEnabled method gets:

Case What it proves
Positive Returns the DTO shape the LWC expects; assert fields, not just non-null
Bulk 200+ records in one call — no SOQL/DML in loops
Negative / access System.runAs(userWithoutAccess)AuraHandledException or empty result, never a raw QueryException
Sharing runAs a user who shouldn't see records → not returned (proves with sharing + USER_MODE)
Error mapping Forced failure (e.g. missing required field) → user-safe AuraHandledException message
Cacheable purity Cacheable method performs no DML (a DML inside throws in cacheable context — test asserts the method is read-only)

Patterns:

  • @TestSetup for shared data; TestDataFactory class; never SeeAllData=true.
  • Test.startTest()/stopTest() around the call to reset limits and force async.
  • Callouts → HttpCalloutMock; Platform Events → Test.getEventBus().deliver().
  • Assert with messages: Assert.areEqual(expected, actual, 'row count for owner'); (Assert class over System.assert).
  • Aim for ≥ 85 % on controller/service classes, not the 75 % org minimum; coverage is a floor, assertions are the point.

3. Browser / E2E tests

Use when the behaviour depends on the platform runtime: LDS caching, App Builder configuration, Flow navigation, Experience Cloud pages, permissions.

  • UTAM (Salesforce's page-object framework) when the team already uses WebdriverIO and wants Salesforce-maintained page objects for standard UI.
  • Playwright otherwise — faster to adopt, better tooling.

Playwright against an org:

  • Authenticate once via sf org open --url-only (frontdoor URL) or username/password on a scratch org; save storageState.
  • Locate through Shadow DOM: Playwright's getByRole/getByLabel pierce shadow roots automatically; avoid CSS chains through lightning-* internals — they change every release.
  • Navigate with direct URLs (/lightning/r/Order/{id}/view) rather than clicking through menus.
  • Seed data through the REST API or sf data create record in beforeAll; delete in afterAll. Scratch org per CI run is the cleanest isolation.
  • Wait for Lightning to settle: await expect(page.getByRole('heading', { name })).toBeVisible(); never fixed sleeps.
  • Keep the E2E suite to 5–15 critical journeys (record page renders, key quick action, Flow completes, Experience site login). Everything else belongs in Jest/Apex.
  • Run the suite against pre-release sandboxes before each Salesforce major release.

4. Test strategy per component type

Component Jest Apex Browser
Presentational LWC (@api in, events out) ✔ all states + a11y
Container LWC with wires/imperative Apex ✔ mocked adapters ✔ controller cases smoke only
LWC in Flow screen ✔ FlowAttributeChangeEvent dispatched ✔ one Flow run
Experience Cloud page ✔ components ✔ guest-user sharing ✔ guest + authed journey
App Builder config (targetConfigs) ✔ page renders with defaults
Apex Service/Selector ✔ bulk + sharing + negative

5. CI pipeline

lint (eslint + prettier + pmd) → jest --coverage (threshold) → sf project deploy validate (RunLocalTests, coverage ≥ 85 % changed) → playwright smoke on scratch org → package version create

Fail on: lint errors, Jest threshold, any Apex test failure, sa11y violations, smoke failures. Nightly: full Playwright suite against a persistent QA sandbox.

6. When asked to "write tests for this LWC / controller"

  1. List behaviours: initial render, each wire state (data/error/undefined), each user action and resulting event/Apex call, each lwc:if branch, cleanup.
  2. Check existing jest-mocks/ and TestDataFactory — reuse.
  3. Write Jest tests one behaviour per it, flushPromises after async; add toBeAccessible().
  4. For the Apex controller, write positive, bulk, negative-access, and sharing tests with runAs.
  5. Run sfdx-lwc-jest and sf apex run test; paste results.

Flaky-test triage

Symptom Cause Fix
Jest: element not found after emit Missing await flushPromises() or Promise.resolve() micro-task Add flush; for renderedCallback chains, flush twice
Jest: leakage between tests Element left in document.body, module-level state in component Remove children in afterEach; jest.resetModules() if needed
Apex: passes alone, fails in run Order-dependent data, SeeAllData, hardcoded ids @TestSetup, factory, query ids
Apex: UNABLE_TO_LOCK_ROW Parallel tests touching the same parent record Separate parents per test or run class serially
Playwright: intermittent timeouts Lightning async render, LDS refresh Role-based expect(...).toBeVisible(), increase expect timeout for record pages, avoid networkidle
Playwright: selector broke after release Selecting into lightning-* internals Use roles/labels; rely on UTAM page objects for standard UI