---
name: salesforce-frontend-testing
description: 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. Use 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".
metadata:
  technology: Salesforce
  type: testing
---

# 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`:
```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:
```js
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 Apex** → `jest.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 Service** → `publish` 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:
```js
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 |
