Post 6 – Building Your First Lightning Web Component for Salesforce Series

This will be the sixth of a series of posts I will be doing over the next few weeks. They will all lead up to the introduction of my new course in January 2020 titled, “Building Your First Lightning Web Component for Salesforce” from Pluralsight. These posts will contain final code snippets of the code used in each course module, but will not include all the tips and best practices about using Visual Studio Code and SFDX, along with the way I personally approach teaching in my video courses.

Handling Testing and Wrapping Up

If you are already a Salesforce developer then you know that server-side Apex Unit tests are required, but client-side JavaScript tests are not and if you are like most developers, you probably think that creating tests should be optional, kind of like documenting, right?

You might not believe this, but writing tests (even for non-required JavaScript) can actually save you a lot of time, such as:

  • Time from chasing down bugs
  • Fixing those bugs
  • Releasing your fix using whatever release process your company uses

Introducing Jest

Jest is an easy to use tool that was originally created by the engineers at Facebook. It is now supported as open-source by the community. Salesforce DX provides a utility that lets you setup and configure Jest for your LWC project.

There are a few steps involved in getting this setup, which include:

  • Install prerequisites (which includes the latest versions of node.js and npm). Even if you already have node installed, it is recommended that you install the Long Term Support Version of Node.js, which will also include npm. This will guarantee that critical bugs will be fixed for a total of 30 months. You can install the LTS version from here.
  • Setup LWC VS Code project with npm – Go to the Terminal window in your VS Code project and from the root folder, run the following command:

npm init

This will create a package.json file. Just hit enter through all the prompts

  • Install sfdx-lwc-jest and dependencies – Also from Terminal window:

npm install

This will create a package-lock.json file

npm install @salesforce/sfdx-lwc-jest --save-dev

  • Identify test script – Open the package.json file and add this to the scripts section:
,
    "test:unit": "sfdx-lwc-jest --coverage",
    "test:unit:watch": "sfdx-lwc-jest --watch",
    "test:unit:debug": "sfdx-lwc-jest --debug"

Writing Jest Tests

Unlike Apex unit tests, Jest tests are run locally and are completely independent of Salesforce. As a best practice, you should create all your tests in a special folder at the root of your project that is named __tests__. If you look at the .forceignore file in your project, you will see that an entry for this folder already exists, thus excluding any tests from being pushed to your scratch org.

In the course, I walk you through creating a test of the leadList component. That test is just a JavaScript file that is created in the __tests__folder and is named: leadList.test.js (which is also a recommended best practice). In that folder, there is also a data subfolder that contains two JSON files named: getLeads.json and getNoLeads.json. Below is the code for the test:

import { createElement } from 'lwc';
import leadList from 'c/leadList';
import { registerApexTestWireAdapter } from '@salesforce/sfdx-lwc-jest';
import searchLeads from '@salesforce/apex/LeadSearchController.searchLeads';

// Represent a list of valid records
const mockGetLeads = require('./data/getLeads.json');
// Represent a list of no records
const mockGetNoLeads = require('./data/getNoLeads.json');
// Register the Apex wire adapter
const searchLeadsAdapter = registerApexTestWireAdapter(searchLeads);

describe('c-lead-list', () => {
    beforeAll(() => {
        // Use fake timers as setTimeout
        jest.useFakeTimers();
    });

    // Best practice to cleanup after each test
    afterEach(() => {
        // The jsdom instance is shared across test cases in a single file so reset the DOM
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    describe('searchLeads @wire returns records with search parameter', () => {
        it('called with data from input', () => {
            const USER_INPUT = 'Bertha';
            const SEARCH_TERM = { searchTerm: USER_INPUT };

            // Create the leadList element
            const element = createElement('c-lead-list', {
                is: leadList
            });
            document.body.appendChild(element);

            // Simulate user input
            const inputValue = element.shadowRoot.querySelector('lightning-input');
            inputValue.value = USER_INPUT;
            inputValue.dispatchEvent(new CustomEvent('change'));

            // Run the fake timers
            jest.runAllTimers();

            // Return a promise to wait for asynchronous results
            // and fail if promise is rejected
            return Promise.resolve().then(() => {
                expect(searchLeadsAdapter.getLastConfig()).toEqual(
                    SEARCH_TERM
                );
            });
        });

        it('renders data of one record', () => {
            const USER_INPUT = 'Bertha Boxer';

            // Create the leadList element
            const element = createElement('c-lead-list', {
                is: leadList
            });
            document.body.appendChild(element);
            
            // Simulate user input
            const inputValue = element.shadowRoot.querySelector('lightning-input');
            inputValue.value = USER_INPUT;
            inputValue.dispatchEvent(new CustomEvent('change'));

            // Run the fake timers
            jest.runAllTimers();

            // Use the searchLeadsAdapter to emit data
            searchLeadsAdapter.emit(mockGetLeads);

            // Return a promise to wait for asynchronous results
            // and fail if promise is rejected
            return Promise.resolve().then(() => {
                // select some elements that would be rendered if it succeeded
                const detailElement = element.shadowRoot.querySelector('lightning-datatable');
                const rows = detailElement.data;
                expect(detailElement.length).toBe(searchLeadsAdapter.length);
                expect(rows[0].Name).toBe(mockGetLeads[0].Name);
            });

        });

        it('renders data when no record is available', () => {
            const USER_INPUT = '';

            // Create the leadList element
            const element = createElement('c-lead-list', {
                is: leadList
            });
            document.body.appendChild(element);

            // Simulate user input
            const inputValue = element.shadowRoot.querySelector('lightning-input');
            inputValue.value = USER_INPUT;
            inputValue.dispatchEvent(new CustomEvent('change'));

            // Run the fake timers
            jest.runAllTimers();

            // Use the searchLeadsAdapter to emit data
            searchLeadsAdapter.emit(mockGetNoLeads);

            // Return a promise to wait for asynchronous results
            // and fail if promise is rejected
            return Promise.resolve().then(() => {
                // select some elements that would be rendered if it succeeded
                const detailElement = element.shadowRoot.querySelector('lightning-datatable');
                expect(detailElement.length).toBe(searchLeadsAdapter.length);
            });
        });

    });

    describe('searchLeads @wire returns error', () => {
        it('shows error panel', () => {
            // Create the leadList element
            const element = createElement('c-lead-list', {
                is: leadList
            });
            document.body.appendChild(element);

            // Emit error from @wire
            searchLeadsAdapter.error();

            // Return a promise to wait for any asynchronous DOM updates. Jest
            // will automatically wait for the Promise chain to complete before
            // ending the test and fail the test if the promise rejects.
            return Promise.resolve().then(() => {
                const errorPanelEl = element.shadowRoot.querySelector(
                    'c-error-panel'
                );
                expect(errorPanelEl).not.toBeNull();
            });
        });
    });

})

And here are the two data files:

getLeads.json:

[
    {
        "Name": "Bertha Boxer",
        "Phone": "(850) 644-4200",
        "Company": "Farmers Coop. of Florida",
        "Title": "Director of Vendor Relations",
        "Street": "321 Westcott Building",
        "City": "Tallahassee",
        "State": "FL",
        "PostalCode": "32306"
    }
]

and getNoLeads.json:

[]

Running Jest Tests

To run the test seen above, you will need to return to the Terminal window and run this command:

npm run test:unit

Wrapping Up

So, this is the last post in the series and I should be announcing the release of this course soon. Sorry it has taken so long, but I hope you go away with these thoughts:

  •  Less frameworks; more web standards  
  • Embrace VS Code and Salesforce DX  
  • Use CustomEvents when possible  
  • LWC programming model is very different  
  • Never be embarrassed about mistakes  
  • Keep learning and share it