lwc-jest-testing-with-accessibility
Use when authoring or reviewing Jest unit tests for Lightning Web Components and the test plan must include explicit accessibility assertions — covers `@salesforce/sfdx-lwc-jest` setup, `createElement` / `document.body.appendChild` render harness, wire-service mocks via `@salesforce/wire-service-jest-util`, imperative Apex mocks via `jest.fn()`, simulated user interactions (`click`, `keydown`, `focus`), ARIA attribute and accessible-name assertions, focus-management tests, keyboard-navigation tests, and optional `axe-core` integration via `jest-axe`. Triggers: 'add a11y assertions to my LWC jest tests', 'how do I test focus management in LWC', 'jest test for keyboard navigation', 'integrate axe-core into sfdx-lwc-jest', 'assert ARIA attributes after interaction', 'how do I prove the LWC is accessible in CI'. NOT for general LWC jest setup without an a11y angle (use lwc/lwc-testing — this skill is the accessibility-deep-dive sibling), NOT for accessibility-pattern authoring inside the component itself (use lwc/lwc-accessibility-patterns), NOT for end-to-end UI automation via UTAM, NOT for manual screen-reader QA workflows.
Best use case
lwc-jest-testing-with-accessibility is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Use when authoring or reviewing Jest unit tests for Lightning Web Components and the test plan must include explicit accessibility assertions — covers `@salesforce/sfdx-lwc-jest` setup, `createElement` / `document.body.appendChild` render harness, wire-service mocks via `@salesforce/wire-service-jest-util`, imperative Apex mocks via `jest.fn()`, simulated user interactions (`click`, `keydown`, `focus`), ARIA attribute and accessible-name assertions, focus-management tests, keyboard-navigation tests, and optional `axe-core` integration via `jest-axe`. Triggers: 'add a11y assertions to my LWC jest tests', 'how do I test focus management in LWC', 'jest test for keyboard navigation', 'integrate axe-core into sfdx-lwc-jest', 'assert ARIA attributes after interaction', 'how do I prove the LWC is accessible in CI'. NOT for general LWC jest setup without an a11y angle (use lwc/lwc-testing — this skill is the accessibility-deep-dive sibling), NOT for accessibility-pattern authoring inside the component itself (use lwc/lwc-accessibility-patterns), NOT for end-to-end UI automation via UTAM, NOT for manual screen-reader QA workflows.
Teams using lwc-jest-testing-with-accessibility should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/lwc-jest-testing-with-accessibility/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How lwc-jest-testing-with-accessibility Compares
| Feature / Agent | lwc-jest-testing-with-accessibility | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Use when authoring or reviewing Jest unit tests for Lightning Web Components and the test plan must include explicit accessibility assertions — covers `@salesforce/sfdx-lwc-jest` setup, `createElement` / `document.body.appendChild` render harness, wire-service mocks via `@salesforce/wire-service-jest-util`, imperative Apex mocks via `jest.fn()`, simulated user interactions (`click`, `keydown`, `focus`), ARIA attribute and accessible-name assertions, focus-management tests, keyboard-navigation tests, and optional `axe-core` integration via `jest-axe`. Triggers: 'add a11y assertions to my LWC jest tests', 'how do I test focus management in LWC', 'jest test for keyboard navigation', 'integrate axe-core into sfdx-lwc-jest', 'assert ARIA attributes after interaction', 'how do I prove the LWC is accessible in CI'. NOT for general LWC jest setup without an a11y angle (use lwc/lwc-testing — this skill is the accessibility-deep-dive sibling), NOT for accessibility-pattern authoring inside the component itself (use lwc/lwc-accessibility-patterns), NOT for end-to-end UI automation via UTAM, NOT for manual screen-reader QA workflows.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
SKILL.md Source
# LWC Jest Testing with Accessibility
Activate this skill when the component already has (or needs) Jest tests and the test plan must prove accessibility behavior — not just rendering and event wiring. The skill is the accessibility-deep-dive sibling of `lwc/lwc-testing`. The core test harness is the same; the difference is what you assert.
---
## Before Starting
Gather this context before writing the test:
- **What a11y dimensions actually live in the component.** Tests can only assert what the component is responsible for. A component that consumes `lightning-input` inherits a lot of accessibility from the base component — you don't re-test SLDS. What you DO test is the markup *your* template adds: `role`, `aria-*`, `tabindex`, dynamic accessible-name updates, focus-target wiring, keyboard handlers.
- **What the jsdom environment can and cannot do.** `@salesforce/sfdx-lwc-jest` runs in jsdom under Node. jsdom has no layout engine, no real focus visualization, no actual screen reader. You can read `aria-*` attributes and check `document.activeElement`, but you cannot assert pixel positions, computed contrast, or screen-reader announcements. Plan accordingly — jest catches structural regressions; manual or UTAM testing catches sensory ones.
- **Whether Locker Service is in play.** It is not, in jest. jest runs the component in a plain Node + jsdom context with no Locker Service or Lightning Locker. Tests that pass in jest can still fail at runtime due to Locker restrictions — keep a Locker-aware end-to-end check separately.
- **Existing setup in the project.** Check `package.json` for `@salesforce/sfdx-lwc-jest` and a `test:unit` script. Check `jest.config.js` for the official Salesforce preset. If either is missing, the first deliverable is the setup, not the assertions.
- **Whether you're going to add `axe-core`.** `axe-core` works in jsdom but with caveats — it cannot evaluate computed style or rendered layout, so a "passes axe in jest" result is a *structural* pass, not a full a11y pass. Decide upfront whether to integrate it (for fail-fast on common ARIA mistakes) or skip it (for less ceremony) and document the choice.
---
## Core Concepts
### Concept 1 — The `createElement` + `document.body.appendChild` harness
Every LWC jest test follows the same render harness:
```js
import { createElement } from 'lwc';
import MyComponent from 'c/myComponent';
describe('c-my-component', () => {
afterEach(() => {
// Clear DOM between tests so state doesn't leak.
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
// Clear any leftover async calls.
jest.clearAllMocks();
});
it('renders with the correct accessible name', async () => {
const element = createElement('c-my-component', { is: MyComponent });
element.label = 'Customer details';
document.body.appendChild(element);
await Promise.resolve(); // wait for first render
const region = element.shadowRoot.querySelector('[role="region"]');
expect(region.getAttribute('aria-label')).toBe('Customer details');
});
});
```
Two things are non-negotiable:
1. **`shadowRoot` queries.** LWC uses Shadow DOM (synthetic in older API versions, native in newer). Inside the test, query through `element.shadowRoot.querySelector(...)` — never through `document.querySelector`, which won't pierce the shadow boundary.
2. **Wait for render before assertions.** LWC re-renders asynchronously. `await Promise.resolve()` flushes the microtask queue and lets pending re-renders settle. Skip this and your assertion runs before the DOM updates, producing flaky tests. For multiple re-render cycles, chain multiple awaits or use a `flushPromises()` helper.
The same harness handles a11y assertions — there is no special "a11y mode." You use it to read `aria-*` attributes, query focusable elements, dispatch keyboard events, and check `document.activeElement`.
### Concept 2 — A11y assertions you CAN make in jest
These structural a11y dimensions are reliably testable in the jest jsdom environment:
| Dimension | Test approach |
|---|---|
| Static ARIA attributes | `el.getAttribute('aria-label')`, `getAttribute('role')`, etc. |
| Dynamic ARIA state | Toggle the component, `await Promise.resolve()`, re-read `aria-expanded` / `aria-pressed` / `aria-selected` |
| Accessible name | `getAttribute('aria-label')` OR `getAttribute('aria-labelledby')` resolved against the referenced element's text |
| Focus management | `expect(element.shadowRoot.activeElement).toBe(expectedEl)` after an action |
| Keyboard handlers | Dispatch `new KeyboardEvent('keydown', { key: 'Escape' })` and assert state change |
| `tabindex` correctness | Read `tabindex`; assert `-1` for programmatically-focused elements, `0` for tab-stops |
| Live region usage | Query `[role="status"]` / `[aria-live]` and assert text content updates after async |
| `aria-describedby` wiring | Read the attribute, then look up the referenced ID inside `shadowRoot` to confirm it resolves |
These dimensions are NOT reliably testable in jest jsdom — defer to UTAM, real-browser e2e, or manual QA:
- **Computed contrast** — jsdom has no rendering layer.
- **Visible focus indicator** — no real layout / `:focus-visible` evaluation.
- **Screen-reader announcement order** — no AT integration in jsdom.
- **`prefers-reduced-motion` and other media queries** — partial / unreliable in jsdom.
- **Layout-dependent role inferences** (e.g. table semantics, scroll containers).
Knowing the jest/jsdom boundary is the difference between a useful test and a misleading "looks accessible" badge.
### Concept 3 — Mocking `@wire` and imperative Apex without breaking a11y assertions
Wire and Apex mocks are essential because jest does not have a Salesforce server. Two official patterns:
**Wire adapters** — use the test wire adapter helpers bundled with `@salesforce/sfdx-lwc-jest`:
```js
import { createElement } from 'lwc';
import { registerApexTestWireAdapter } from '@salesforce/sfdx-lwc-jest';
import getCases from '@salesforce/apex/CaseController.getCases';
import CaseList from 'c/caseList';
const getCasesAdapter = registerApexTestWireAdapter(getCases);
it('shows accessible empty state when no cases', async () => {
const element = createElement('c-case-list', { is: CaseList });
document.body.appendChild(element);
getCasesAdapter.emit([]); // simulate wire returning empty
await Promise.resolve();
const empty = element.shadowRoot.querySelector('[role="status"]');
expect(empty).not.toBeNull();
expect(empty.textContent).toContain('No cases');
});
```
**Imperative Apex** — use `jest.mock` with `jest.fn()` returning a Promise:
```js
jest.mock(
'@salesforce/apex/CaseController.assignCase',
() => ({ default: jest.fn() }),
{ virtual: true }
);
import assignCase from '@salesforce/apex/CaseController.assignCase';
it('announces success in the live region after assignment', async () => {
assignCase.mockResolvedValue({ status: 'OK' });
// ... interact with component, then:
await Promise.resolve(); // flush click handler
await Promise.resolve(); // flush mockResolvedValue microtask
await Promise.resolve(); // flush re-render
const liveRegion = element.shadowRoot.querySelector('[aria-live="polite"]');
expect(liveRegion.textContent).toContain('assigned');
});
```
The `{ virtual: true }` option is required because `@salesforce/apex/...` is a virtual module resolved at compile time on the platform — jest needs to be told it's OK that the module doesn't exist on disk.
A11y assertions sit immediately after the mocked async settles. The dance with three `await Promise.resolve()`s exists because click → mock resolution → re-render is three microtasks, not one.
### Concept 4 — `axe-core` integration: structural pass only, with caveats
`axe-core` (https://github.com/dequelabs/axe-core) can run inside jsdom and check a defined subset of WCAG rules — those that don't depend on rendered layout or computed style. The integration via `jest-axe` looks like:
```js
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('has no axe violations in the default state', async () => {
const element = createElement('c-my-component', { is: MyComponent });
document.body.appendChild(element);
await Promise.resolve();
// axe needs the DOM subtree — pass shadowRoot or the element host.
const results = await axe(element.shadowRoot);
expect(results).toHaveNoViolations();
});
```
What axe-in-jest catches:
- Missing `alt` on `img`
- Missing accessible name on interactive elements
- Invalid ARIA attribute usage (e.g. `aria-checked` on a non-checkbox role)
- Duplicate IDs in the rendered subtree
- ARIA role / required-children mismatches
What it does NOT catch (because jsdom can't compute layout):
- Color contrast (`color-contrast` rule is unreliable / disabled by default in jsdom)
- Visible focus indicator
- Reading order vs DOM order in flexbox/grid
- Touch target size
Treat `axe(...)` in jest as an extra gate against ARIA mistakes, not as a full a11y certificate. The "passes axe in jest" claim should always be paired with a manual or UTAM-based real-browser axe pass before release.
---
## Recommended Workflow
1. **Inventory the component's a11y surface.** Open the component template; list every `role`, `aria-*`, `tabindex`, focus target, and keyboard handler. Each is a candidate assertion. If the list is empty, the component may not need a dedicated a11y test (e.g. a thin display-only wrapper around `lightning-card`).
2. **Confirm the jest setup.** `package.json` should depend on `@salesforce/sfdx-lwc-jest`. `jest.config.js` should extend the Salesforce preset (`module.exports = require('@salesforce/sfdx-lwc-jest/config');`). If missing, follow the current `sfdx-lwc-jest` README to install. Without these, none of the assertions in this skill will work.
3. **Write the render + accessible-name baseline.** Start with one test that renders the component in a default state and asserts the accessible name (`aria-label` or `aria-labelledby` chain). This is the cheapest, highest-signal a11y test — if it fails, the component has no accessible identity at all.
4. **Add interaction-driven ARIA-state tests.** For every `aria-expanded`, `aria-pressed`, `aria-selected`, or `aria-checked` your template emits, add one test that toggles the underlying state and asserts the attribute follows. This is where most real a11y regressions happen.
5. **Add focus-management and keyboard-handler tests.** If the component manages focus on open/close (modal, popover, accordion) or implements a keyboard handler (`Escape` to close, `Arrow` to navigate), add a test that dispatches the event and asserts both the state change AND `shadowRoot.activeElement`.
6. **Decide on axe-core.** If the team wants a fail-fast gate against ARIA mistakes, add `jest-axe` and a single per-state axe assertion. Document in the PR description that axe-in-jest does NOT replace browser axe checks. If the team prefers minimal ceremony, skip it — explicit assertions are usually more maintainable.
7. **Run the bundled checker against the project.** `python3 scripts/check_lwc_jest_testing_with_accessibility.py --root force-app/main/default` enumerates LWC components without test files, test files without a11y assertions, and `package.json` lacking the official preset.
---
## Related Skills
- `lwc/lwc-testing` — general LWC jest setup, wire / Apex / navigation / LMS mock recipes; this skill assumes that one as a base
- `lwc/lwc-accessibility-patterns` — how to *build* accessible LWC markup (the source of what this skill asserts against)
- `lwc/lwc-async-patterns` — `flushPromises` and microtask handling — relevant whenever the a11y assertion sits after an async resolve
- `lwc/common-lwc-runtime-errors` — Locker-Service / shadow-DOM gotchas that don't reproduce in jestRelated Skills
omnistudio-testing-patterns
Use when testing or validating OmniStudio components — OmniScript preview, Integration Procedure step debugging, DataRaptor field-mapping validation, and end-to-end UTAM-based automation. NOT for Apex unit testing or standard Flow debugging.
lwc-testing
Use when setting up or reviewing Lightning Web Component unit tests with Jest, including `@salesforce/sfdx-lwc-jest`, wire adapter mocks, imperative Apex mocks, async rerender handling, and accessibility smoke checks. Triggers: 'how do I test @wire in LWC', 'Jest test is flaky', 'mock Apex in LWC test', 'flushPromises pattern'. NOT for Apex unit tests, browser end-to-end automation, or performance testing.
lwc-accessibility
Use when designing or reviewing Lightning Web Components for keyboard access, semantic labeling, focus management, screen-reader behavior, and WCAG-aligned UX in Salesforce. Triggers: 'lwc accessibility', 'keyboard navigation in lwc', 'screen reader labels', 'focus trap in modal'. NOT for Apex or sharing security reviews, or for purely visual SLDS styling that does not affect accessibility behavior.
lwc-accessibility-patterns
Use when implementing specific ARIA attributes, keyboard navigation patterns, screen reader live regions, WCAG 2.1 compliance, focus management, or accessible data tables in Lightning Web Components. Trigger keywords: ARIA, aria-live, aria-label, aria-labelledby, role=grid, tabindex, keydown handler, WCAG AA, focus trap, accessible datatable, shadow DOM ARIA boundary. NOT for general LWC styling, visual design, SLDS theming, or CSS that does not affect assistive-technology behavior.
screen-flow-accessibility
Use when building Screen Flows that must meet accessibility standards (WCAG 2.1 AA, Salesforce accessibility guidelines). Covers keyboard navigation, focus order, labels, error messaging, color contrast, and screen reader compatibility. Does NOT cover LWC a11y (see lwc-accessibility) or general record-page a11y.
flow-testing
Use when defining or reviewing test strategy for Salesforce Flow, including Flow Tests, debug runs, path coverage, test data, and explicit validation of fault paths and custom component behavior. Triggers: 'flow test tool', 'how do i test a flow', 'flow fault path testing', 'flow debug interview'. NOT for Apex unit testing or manual QA planning that is unrelated to Flow behavior.
performance-testing-salesforce
Use when planning or executing load tests, stress tests, or performance benchmarks against a Salesforce org. Covers Salesforce Scale Test, third-party tools (JMeter, BlazeMeter, k6), API throughput testing, Experience Page Time (EPT) measurement, and sandbox sizing for performance work. Triggers: 'load test Salesforce org', 'Scale Test Full Copy sandbox', 'EPT optimization', 'API concurrency limits', 'JMeter Salesforce performance'. NOT for Apex unit test performance assertions, LWC Jest tests, query optimization (see data/soql-query-optimization), or NFR definition (see architect/nfr-definition-for-salesforce).
data-seeding-for-testing
Use when creating test data for scratch orgs, sandboxes, or CI pipelines: Apex @testSetup factories, sf data import tree plans, CumulusCI datasets, Snowfakery. NOT for production data migration or ETL pipelines.
continuous-integration-testing
Use when configuring Apex test execution in CI/CD pipelines, choosing test levels for deployments, parsing test results, or troubleshooting code coverage in automated builds. Triggers: 'RunLocalTests', 'RunSpecifiedTests', 'sf project deploy', 'code coverage CI', 'JUnit test results'. NOT for Apex test class design patterns, test data factory architecture, or LWC Jest testing.
automated-regression-testing
Use when building or maintaining automated end-to-end regression tests for Salesforce UI (Lightning, LWC, Aura, Flows), selecting a testing framework (UTAM, Provar, Selenium), handling Shadow DOM challenges, or scheduling regression suites against pre-release orgs. Triggers: 'UTAM page objects', 'Shadow DOM testing', 'Salesforce E2E regression', 'Provar test automation', 'pre-release regression window'. NOT for Apex unit testing, LWC Jest component testing, manual UAT planning, or Agentforce conversational testing.
agentforce-testing-strategy
Design Agentforce testing: topic coverage, action unit tests, deterministic golden sets, adversarial prompts, and regression harness. Trigger keywords: agentforce testing, agent eval, agent regression suite, prompt golden set, action unit test agentforce. Does NOT cover: generic LLM evaluation academia, human-labeled RLHF pipelines, or Einstein Classify accuracy.
agent-testing-and-evaluation
Use when testing, evaluating, or building regression suites for Agentforce agents: conversation testing in Agent Builder, topic coverage and utterance testing, Testing API and AiEvaluationDefinition metadata, evaluation metrics (containment rate, escalation rate, CSAT, topic activation accuracy), and post-deploy analytics via Enhanced Event Logs. Triggers: 'how do I test my Agentforce agent', 'agent routes to wrong topic', 'write utterance tests', 'regression test after topic change', 'measure agent quality', 'agent containment rate'. NOT for agent creation, topic design, or action contract design — use agentforce/agentforce-agent-creation, agentforce/agent-topic-design, or agentforce/agent-actions respectively.