Salesforce Lightning Web Component Jest Test Cheat Sheet

SFDX Project Setup 🔗

One-time configuration for SFDX project using Salesforce CLI:

sfdx force:lightning:lwc:test:setup

One-time local installation for already configured SFDX project:

npm install

Create file jest.config.js in project root (required for Jest extension and debugging):

const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
module.exports = {
  ...jestConfig
};

Running Tests 🔗

Run all tests in SFDX project:

npm run test:unit

Run all tests in SFDX project including coverage report:

npm run test:unit:coverage

Create Test File 🔗

Test files must be located in subdirectory __tests__ of the component directory and must have the suffix .test.js.

Given a component mySimpleComponent the test file is force-app/main/default/lwc/mySimpleComponent/__tests__/mySimpleComponent.test.js

Create test file using Salesforce CLI:

sfdx force:lightning:lwc:test:create -f {component JS file}

Test File Structure 🔗

import { createElement } from 'lwc';
import MySimpleComponent from 'c/mySimpleComponent';

describe('c-my-simple-component', () => {
  afterEach(() => {
    while (document.body.firstChild) {
      document.body.removeChild(document.body.firstChild);
    }
  });

  it('does something useful', () => {
    // ...
  });
});

Basic Test 🔗

Given a component mySimpleComponent with the following class and template:

import { LightningElement } from 'lwc';
export default class MySimpleComponent extends LightningElement {}
<template>
  <div class="message">Hello World</div>
</template>

Basic test is:

it('displays the message', () => {
  const element = createElement('c-my-simple-component', {
    is: MySimpleComponent
  });
  document.body.appendChild(element);
  const message = element.shadowRoot.querySelector('.message');
  expect(message).not.toBeNull();
  expect(message.textContent).toBe('Hello World');
});

Asynchronous Test 🔗

Required to defer test execution until asynchronous tasks (e.g. server calls, rerendering) have completed.

One-time install Node module flush-promises:

npm install flush-promises --save-dev

Import helper function in test file:

import flushPromises from 'flush-promises';

Use async/await with helper function in test case:

it('does something useful', async () => {
  // Initialize component and trigger async task
  await flushPromises();
  // Steps to execute after async task has completed
});

Test Public Properties 🔗

Given a component myParameterizedComponent with the following class and template:

import { api, LightningElement } from 'lwc';
export default class MyParameterizedComponent extends LightningElement {
  @api name;
}
<template>
  <div class="message">Hello {name}</div>
</template>

If public property is always expected to be set before component is added to DOM the test can by synchronous:

it('displays the message', () => {
  const element = createElement('c-my-parameterized-component', {
    is: MyParameterizedComponent
  });
  element.name = 'John';  // Set property BEFORE adding LWC to DOM
  document.body.appendChild(element);
  const message = element.shadowRoot.querySelector('.message');
  expect(message).not.toBeNull();
  expect(message.textContent).toBe('Hello John');
});

If public property may change after component has been added to DOM the test must be asynchronous:

it('displays the changed message', async () => {
  const element = createElement('c-my-parameterized-component', {
    is: MyParameterizedComponent
  });
  document.body.appendChild(element);
  element.name = 'John';  // Set property AFTER adding LWC to DOM
  await flushPromises();  // Wait for LWC to rerender
  const message = element.shadowRoot.querySelector('.message');
  expect(message).not.toBeNull();
  expect(message.textContent).toBe('Hello John');
});

Test Wire Service 🔗 🔗

Given a component myWireComponent with the following class and template:

import { LightningElement, wire } from 'lwc';
import myControllerMethod from '@salesforce/apex/MyController.myControllerMethod';
export default class MyWireComponent extends LightningElement {
  name = 'John';
  @wire(myControllerMethod, { name: '$name' })
  wiredResult;
}
<template>
  <div if:true={wiredResult.data} class="message">
    {wiredResult.data.message}
  </div>
  <div if:true={wiredResult.error} class="error">
    An error has occurred.
  </div>
</template>

Import Apex method and Apex wire adapter function in test file:

import myControllerMethod from '@salesforce/apex/MyController.myControllerMethod';
import { registerApexTestWireAdapter } from '@salesforce/sfdx-lwc-jest';

Register wire adapter before declaring test suite:

const myControllerMethodWireAdapter = registerApexTestWireAdapter(myControllerMethod);              
describe('c-my-wire-component', () => { ... });

Test successful response of Apex method:

it('displays the message when it has been retrieved successfully', async () => {
  const element = createElement('c-my-wire-component', {
    is: MyWireComponent
  });
  document.body.appendChild(element);
  myControllerMethodWireAdapter.emit({ message: 'Hello John' });
  await flushPromises();
  const message = element.shadowRoot.querySelector('.message');
  expect(message).not.toBeNull();
  expect(message.textContent).toBe('Hello John');
  const error = element.shadowRoot.querySelector('.error');
  expect(error).toBeNull();
});

Test failure response of Apex method:

it('displays an error when message retrieval has failed', async () => {
  const element = createElement('c-my-wire-component', {
    is: MyWireComponent
  });
  document.body.appendChild(element);
  myControllerMethodWireAdapter.error();
  await flushPromises();
  const error = element.shadowRoot.querySelector('.error');
  expect(error).not.toBeNull();
  const message = element.shadowRoot.querySelector('.message');
  expect(message).toBeNull();
});

Test parameters sent to Apex method:

it('calls the server with given parameters', async () => {
  const element = createElement('c-my-wire-component', {
    is: MyWireComponent
  });
  document.body.appendChild(element);
  await flushPromises();
  expect(myControllerMethodWireAdapter.getLastConfig()).toEqual({ name: 'John' });
});

Test Imperative Apex Call 🔗

Given a component myApexComponent with the following class and template:

import { LightningElement } from 'lwc';
import myControllerMethod from '@salesforce/apex/MyController.myControllerMethod';
export default class MyApexComponent extends LightningElement {
  name = 'John';
  message = null;
  error = false;
  connectedCallback() {
    myControllerMethod(this.name)
      .then((data) => { this.message = data.message; })
      .catch((error) => { this.error = true; });
  }
}
<template>
  <div if:true={message} class="message">
    {message}
  </div>
  <div if:true={error} class="error">
    An error has occurred.
  </div>
</template>

Import Apex method in test file:

import myControllerMethod from '@salesforce/apex/MyController.myControllerMethod';

Mock Apex method module before declaring test suite:

jest.mock(
  '@salesforce/apex/MyController.myControllerMethod',
  () => ({ default: jest.fn() }), { virtual: true }
);
describe('c-my-apex-component', () => { ... });

Reset all Jest mocks after each test case:

afterEach(() => {
  jest.resetAllMocks();
  // ...
});

Test successful response of Apex method:

it('displays the message when it has been retrieved successfully', async () => {
  // Resolve with object typically returned by Apex method
  myControllerMethod.mockResolvedValue({ message: 'Hello John' });
  const element = createElement('c-my-apex-component', {
    is: MyApexComponent
  });
  document.body.appendChild(element);
  await flushPromises();
  const message = element.shadowRoot.querySelector('.message');
  expect(message).not.toBeNull();
  expect(message.textContent).toBe('Hello John');
  const error = element.shadowRoot.querySelector('.error');
  expect(error).toBeNull();
});

Test failure response of Apex method:

it('displays an error when message retrieval has failed', async () => {
  // Reject with Salesforce error object
  myControllerMethod.mockRejectedValue({ ok: false, status: 400 });
  const element = createElement('c-my-apex-component', {
    is: MyApexComponent
  });
  document.body.appendChild(element);
  await flushPromises();
  const error = element.shadowRoot.querySelector('.error');
  expect(error).not.toBeNull();
  const message = element.shadowRoot.querySelector('.message');
  expect(message).toBeNull();
});

Test parameters sent to Apex method:

it('calls the server with given parameters', async () => {
  myControllerMethod.mockResolvedValue({ message: 'Hello John' });
  const element = createElement('c-my-apex-component', {
    is: MyApexComponent
  });
  document.body.appendChild(element);
  await flushPromises();
  expect(myControllerMethod).toHaveBeenCalledWith('John');
  // Or verify first argument of first call explicitly
  expect(myControllerMethod.mock.calls[0][0]).toBe('John');
});

Test I18N and Guest User 🔗

Given a component myGuestAwareComponent with the following class and template:

import { LightningElement } from 'lwc';
import LANG from '@salesforce/i18n/lang';
import isGuest from '@salesforce/user/isGuest';
export default class MyGuestAwareComponent extends LightningElement {
  authenticated = !isGuest;
  language = LANG;
}
<template>
  <div class="message">
    <span if:false={authenticated}>You're a guest</span>
    <span if:true={authenticated}>You're signed in</span>
    <span> with language {language}</span>
  </div>
</template>

Mock virtual modules before declaring test suite:

jest.mock('@salesforce/i18n/lang', () => ({ default: 'de' }), { virtual: true });
jest.mock('@salesforce/user/isGuest', () => ({ default: true }), { virtual: true });
describe('c-my-guest-aware-component', () => { ... });

Use multiple test files to simulate different user and I18N states.

Test component output:

it('displays user state and language', () => {
  const element = createElement('c-my-guest-aware-component', {
    is: MyGuestAwareComponent
  });
  document.body.appendChild(element);
  const message = element.shadowRoot.querySelector('.message');
  expect(message.textContent).toBe("You're a guest with language de");
});

Test User Interaction 🔗

Given a component myInteractiveComponent with the following class and template:

import { LightningElement } from 'lwc';
export default class MyInteractiveComponent extends LightningElement {
  selectedOption;
  handleChange(event) {
    this.selectedOption = event.target.value;
  }
}
<template>
  <div class="selected">{selectedOption}</div>
  <div>
    <select class="selection" onchange={handleChange}>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
  </div>
</template>

Test component interaction using synthetic event:

it('displays the selected option', async () => {
  const element = createElement('c-my-interactive-component', {
    is: MyInteractiveComponent
  });
  document.body.appendChild(element);
  const selection = element.shadowRoot.querySelector('.selection');
  selection.value = 'B';
  selection.dispatchEvent(new CustomEvent('change', { bubbles: true }));
  await flushPromises();
  const selected = element.shadowRoot.querySelector('.selected');
  expect(selected.textContent).toBe('B');
});

Test Navigation 🔗

Given a component myNavigationComponent with the following class and template:

import { LightningElement } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
export default class MyNavigationComponent extends NavigationMixin(LightningElement) {
  navigateToHomepage() {
    this[NavigationMixin.Navigate]({
      type: 'standard__namedPage',
      attributes: {
        pageName: 'home'
      }
    });
  }
}
<template>
  <a onclick={navigateToHomepage} class="home_link">Go to homepage</a>
</template>

Create global mock file force-app/test/jest-mocks/lightning/navigation.js for navigation module in SFDX project:

export const CurrentPageReference = jest.fn();
export const NavigateMock = jest.fn();
export const GenerateUrlMock = jest.fn();
const Navigate = Symbol('Navigate');
const GenerateUrl = Symbol('GenerateUrl');
export const NavigationMixin = (Base) => {
  return class extends Base {
    [Navigate] = NavigateMock;
    [GenerateUrl] = GenerateUrlMock;
  };
};
NavigationMixin.Navigate = Navigate;
NavigationMixin.GenerateUrl = GenerateUrl;

Register navigation mock module in jest.config.js:

module.exports = {
  ...jestConfig,
  moduleNameMapper: {
    '^lightning/navigation$':
      '<rootDir>/force-app/test/jest-mocks/lightning/navigation'
  }
};

Import navigation mock in test file:

import { NavigateMock } from 'lightning/navigation';

Reset all Jest mocks after each test case:

afterEach(() => {
  jest.resetAllMocks();
  // ...
});

Test call of navigation with expected page reference:

it('navigates to homepage when link is clicked', () => {
  const element = createElement('c-my-navigation-component', {
    is: MyNavigationComponent
  });
  document.body.appendChild(element);
  const link = element.shadowRoot.querySelector('.home_link');
  link.click();
  expect(NavigateMock).toHaveBeenCalledWith({
    type: 'standard__namedPage',
    attributes: {
      pageName: 'home'
    }
  });
});

Test Current Page Reference 🔗 🔗

Given a component myPageRefComponent with the following class and template:

import { LightningElement, wire } from 'lwc';
import { CurrentPageReference } from 'lightning/navigation';
export default class MyPageRefComponent extends LightningElement {
  @wire(CurrentPageReference)
  wiredPageRef;
}
<template>
  <div if:true={wiredPageRef} class="page_type">{wiredPageRef.type}</div>
</template>

Import current page reference and test wire adapter function in test file:

import { CurrentPageReference } from 'lightning/navigation';
import { registerTestWireAdapter } from '@salesforce/sfdx-lwc-jest';

Register wire adapter before declaring test suite:

const currentPageRefWireAdapter = registerTestWireAdapter(CurrentPageReference);
describe('c-my-page-ref-component', () => { ... });

Test component with synthetic page reference:

it('displays current page type', async () => {
  const element = createElement('c-my-page-ref-component', {
    is: MyPageRefComponent
  });
  document.body.appendChild(element);
  currentPageRefWireAdapter.emit({
    type: 'standard__objectPage',
    attributes: {
      objectApiName: 'Contact',
      actionName: 'new'
    }
  });
  await flushPromises();
  const pageType = element.shadowRoot.querySelector('.page_type');
  expect(pageType).not.toBeNull();
  expect(pageType.textContent).toBe('standard__objectPage');
});

Test with Mock Child Components

Given a parent component myParentComponent with the following class and template:

import { LightningElement } from 'lwc';
export default class MyParentComponent extends LightningElement {}
<template>
  <c-my-child-component param="Hello"></c-my-child-component>
</template>

And a child component myChildComponent with the following class:

import { api, LightningElement } from 'lwc';
export default class MyChildComponent extends LightningElement {
  @api param;
  // More logic
}

Create a mock file __mocks__/myChildComponent.js in the child component directory, i.e. force-app/main/default/lwc/myChildComponent/__mocks__/myChildComponent.js , with the same public properties but no logic.

import { api, LightningElement } from 'lwc';
export default class MyChildComponentMock extends LightningElement {
  @api param;
}

Mock the child component in the parent test file before declaring test suite:

jest.mock('c/myChildComponent');
describe('c-my-parent-component', () => { ... });

Test parent component with mocked child component:

it('displays the child with given parameters', () => {
  const element = createElement('c-my-parent-component', {
    is: MyParentComponent
  });
  document.body.appendChild(element);
  const child = element.shadowRoot.querySelector('c-my-child-component');
  // Child is the mock, not the real component
  expect(child).not.toBeNull();
  expect(child.param).toBe('Hello');
});

Test with Externalized JSON Data 🔗

Create JSON file in subdirectory data inside the __tests__ directory of the component, e.g. myWireComponent/__tests__/data/successResponse.json

Import JSON file in test file:

const successResponse = require('./data/successResponse.json');

Inside test cases use imported data for wire adapter or Jest mocks:

myControllerMethodWireAdapter.emit(successResponse);

Debugging in Visual Studio Code 🔗

One-time configuration of launch task in file .vscode/launch.json in the SFDX project (Windows version):

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Jest Tests",
      "type": "node",
      "request": "launch",
      "runtimeArgs": [
        "--inspect-brk",
        "${workspaceRoot}/node_modules/jest/bin/jest.js",
        "--runInBand"
      ],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "port": 9229
    }
  ]
}

In the launch panel of VS Code select the config Debug Jest Tests.

Set a breakpoint in test case and run tests via launch config.

Tasks in Visual Studio Code

One-time configuration of tasks in file .vscode/tasks.json in the SFDX project:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Run All LWC Tests",
      "type": "shell",
      "command": "${workspaceFolder}/node_modules/.bin/lwc-jest",
      "args": ["--coverage"],
      "problemMatcher": []
    },
    {
      "label": "Run LWC Tests in Current File",
      "type": "shell",
      "command": "${workspaceFolder}/node_modules/.bin/lwc-jest",
      "args": ["--", "--runTestsByPath", "${relativeFile}"],
      "problemMatcher": []
    }
  ]
}

Press CTRL+P, type task followed by space. Then select the desired task. Alternatively, press F1 and select command Run Task.