0

My LWC application includes a modal window and is written for use in Salesforce and uses Apex. I had a hard time writing tests for some functions, but I couldn't test the application until the end. I can't test the functions that should be triggered AFTER opening the modal window and triggering some buttons. This is switching to the recording editing mode, editing the recording fields and canceling the transition to editing mode. And (to be honest) I was able to test the very fact of closing the modal window only because I set an absolutely unnecessary event.

When I try to write a test, for example, on the method of changing the account name, I get an error due to the fact that the value of the field has not changed (i.e. it is still filled in with the data "TestAccountForWebService" and not "newValue").

The rest of the tests given here pass (I will delete them if they are not needed for the question).

In practice, the application works fine, I will specify the untested lines that I determined using command (in terminal): npm run test:unit:coverage

Some details: AccountSource2 is a custom field, not relevant to my question.

UPDATE: Initially, I received the tested element (lightning-input - in my case) via querySelector and called the dispatchEvent(changeValue) method for it inside the "Promise.resolve().the(() =>" block. Then I got an error due to the fact that the value has not changed and is equal to the originally filled ("TestAccountForWebService" and not "newValue"). After following the solution from Kris Goncalves (see below) I changed the test and took out these lines, and put them before the Promise block, the error changed to " TypeError: Cannot read properties of null (reading 'dispatchEvent') " when trying to call the dispatchEvent method.

js-meta.xml code:

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__Tab</target>
    </targets>
</LightningComponentBundle>

html code:

<template>
    
    <div>
        <lightning-button variant="brand-outline" label="Create new record" title="CreateRecord" onclick={CreateRecord} class="slds-m-left_x-small"  data-id="CreateRecordButton"></lightning-button>
        <lightning-datatable data-id='dataTable' data={wiredAccounts} columns={columnsForManyAccs} onrowaction={handlerRowAction} key-field="Id" hide-checkbox-column="true">
        </lightning-datatable>
    </div>

    <template if:true={bShowModal} data-id="modalWindowTemplate">
        <section role="dialog" 
        tabindex="-1" 
        aria-labelledby="modal-heading-01" 
        aria-modal="true" 
        aria-describedby="modal-content-id-1" 
        class="slds-modal slds-fade-in-open"
        data-id="modalWindow">
            <div class="slds-modal__container" data-id="modalWindowContainer">
                <!-- modal header start -->
                <header class="slds-modal__header">
                    <button class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse" title="Close" onclick={closeModal}>
                        <lightning-icon icon-name="utility:close" alternative-text="close" variant="inverse" size="small" ></lightning-icon>
                    </button>
                    <h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate">Account</h2>
                </header>

                <!-- modal body start -->
                <div class="slds-form-element__control backgroundWhite" data-id="modalWindowBody">
                    <lightning-input required variant="label-inline" label="Name" readonly={isEditingProhibited} type="text" id="text-input-id-1" value={account.Name} onchange={changeName} data-id="modalInputName"></lightning-input>

                    <lightning-input variant="label-inline" label="Website" readonly={isEditingProhibited} type="url" id="text-input-id-2" value={account.Website} onchange={changeWebsite} placeholder="SomeTestURL.com"></lightning-input>

                    <lightning-input variant="label-inline" label="Phone number" readonly={isEditingProhibited} type="tel" id="text-input-id-3" value={account.Phone} onchange={changePhone} placeholder="000-000-0000" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"></lightning-input>

                    <lightning-input variant="label-inline" label="AccountSource" readonly={isEditingProhibitedForAccountSource} type="text" id="text-input-id-4" value={account.AccountSource} onchange={changeAccountSource}></lightning-input>

                    <lightning-input variant="label-inline" label="Account Source 2" readonly={isEditingProhibited} type="text" id="text-input-id-5" value={account.AccountSource2} onchange={changeAccountSource2}></lightning-input>
                </div>
                <!-- modal footer start-->

                <footer class="slds-modal__footer">
                    <template if:true={isEditingProhibited}>
                        <lightning-button variant="brand" label="Edit record" title="EditRecord" onclick={EditRecord} class="slds-m-left_x-small" data-id="EditRecordButton"></lightning-button>
                        <lightning-button variant="destructive" label="Delete" title="DeleteRecord" onclick={DeleteRecord} class="slds-m-left_x-small" data-id="DeleteRecordButton"></lightning-button>
                    </template>
                    <template if:false={isEditingProhibited}>
                        <lightning-button variant="brand" label="Save record" id="SaveAccButton" title="SaveRecord" onclick={SaveRecord} class="slds-m-left_x-small" data-id="SaveRecordButton"></lightning-button>
                        <lightning-button variant="destructive" label="Cancel" title="CancelEditRecord" onclick={CancelEditRecord} disabled={isCancelNeed} class="slds-m-left_x-small" data-id="CancelEditRecordButton"></lightning-button>
                    </template>
                    
                    
                    <lightning-button variant="brand" label="Close" title="Close" onclick={closeModal} class="slds-m-left_x-small" data-id="closeModalWindow"></lightning-button>
                </footer>
            </div>
        </section>
        <div class="slds-backdrop slds-backdrop_open"></div>
    </template>
    
</template>

js code:

import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { LightningElement, api, wire,track } from 'lwc';
import getAllAccounts from '@salesforce/apex/AccountServiceLWCHandler.GetAllAccounts';
import saveAccount from '@salesforce/apex/AccountServiceLWCHandler.SaveAccount';
import deleteAccount from '@salesforce/apex/AccountServiceLWCHandler.DeleteAccount';

import { refreshApex} from '@salesforce/apex';

const columnsForManyAccs = [
    {
        label: 'View',
        type: 'button-icon',
        initialWidth: 75,
        typeAttributes: {
            iconName: 'action:preview',
            title: 'Detail',
            variant: 'border-filled',
            alternativeText: 'View'
        }
    },
    { label: 'Name', fieldName: 'Name' ,wrapText: true, hideDefaultActions: true },
    { label: 'Website', fieldName: 'Website', type: 'url',wrapText: true, hideDefaultActions: true },
    { label: 'Phone', fieldName: 'Phone', type: 'phone',wrapText: true, hideDefaultActions: true },
    { label: 'AccountSource', fieldName: 'AccountSource',wrapText: true, hideDefaultActions: true },
    { label: 'AccountSourc2oo', fieldName: 'AccountSource2',wrapText: true, hideDefaultActions: true }
];
const columnsForOneAcc = [
    { label: 'Name', fieldName: 'Name' ,wrapText: true, hideDefaultActions: true },
    { label: 'Website', fieldName: 'Website', type: 'url',wrapText: true, hideDefaultActions: true },
    { label: 'Phone', fieldName: 'Phone', type: 'phone',wrapText: true, hideDefaultActions: true },
    { label: 'AccountSource', fieldName: 'AccountSource',wrapText: true, hideDefaultActions: true },
    { label: 'AccountSourc2', fieldName: 'AccountSource2',wrapText: true, hideDefaultActions: true }
];
export default class AccountWebService extends LightningElement {
    columnsForManyAccs = columnsForManyAccs;
    columnsForOneAcc=columnsForOneAcc;
    wiredAccounts=[];
    @track wiredData;
    @api realFormData='some';
    bShowModal=false;
    @track account;
    isEditingProhibited=true;
    isEditingProhibitedForAccountSource=true;
    isCancelNeed=true;
    @track editedAccount=null;

    @wire(getAllAccounts)wireBlastData(response)
    {
        this.wiredData=response;
        this.wiredAccounts=this.wiredData.data;
        console.log(this.wiredAccounts);
    }
    handlerRowAction(event){
        var tmp=event.detail.row;
        console.log('handlerRowAction1',event.detail.row);
        console.log('handlerRowAction2',tmp.Name);
        console.log('handlerRowAction2',tmp.Id);
        console.log('handlerRowAction3',tmp);
        this.account={Id:'',Name:'',Phone:'',Website:'',AccountSource:'',AccountSource2:''};
        if(tmp.AccountSource){
            this.account.AccountSource=tmp.AccountSource;
        }
        if(tmp.AccountSource2){
            this.account.AccountSource2=tmp.AccountSource2;
        }
        if(tmp.Phone){
            this.account.Phone=tmp.Phone;
        }
        if(tmp.Website){
            this.account.Website=tmp.Website;
        }
        this.account.Id=tmp.Id;
        this.account.Name=tmp.Name;
        this.editedAccount={...this.account};
        console.log('editedAcc:'+this.editedAccount.Name);
        this.bShowModal = true;
    }
    closeModal() {
        this.bShowModal = false;
        //absolutely unnecessary event, I inserted it here only for test
        const event = new CustomEvent('close_modal_event');
        this.dispatchEvent(event);
    }
    CreateRecord(){
        this.account={Id:'',Name:'TestAccountForWebService0',Phone:'000-000-0000',Website:'SomeTestURL.com',AccountSource:'Other',AccountSource2:'NotOther'};
        this.bShowModal=true;
        this.isEditingProhibited=false;
        this.isEditingProhibitedForAccountSource=false;
    }
    showToastMessage(messageToSet,variantToSet){
        const evt = new ShowToastEvent({
            title: 'Info',
            message:messageToSet,
            variant: variantToSet,
            mode: 'sticky'
        });
        this.dispatchEvent(evt);
    }
    DeleteRecord(){
        deleteAccount({strId:this.account.Id}).then(response=>{
            if(response)
            {
                this.bShowModal=false;
                refreshApex(this.wiredData);
                this.showToastMessage('Ok: id='+response,'success');
            }
            else
            {
                //!!!!Untested lines begin below and to the end of the file
                this.showToastMessage('Not ok','error');
            }
        });
    }
    SaveRecord(){
        this.isEditingProhibited=true;
        this.isEditingProhibitedForAccountSource=true;
        saveAccount({acc:this.account}).then(response=>{
            if(response)
            {
                refreshApex(this.wiredData);
                this.showToastMessage('Ok: id='+response,'success');
                this.editedAccount=null;
                this.bShowModal=false;
            }
            else
            {
                this.showToastMessage('Not ok','error');
            }
        });
    }
    changeName(event){
        console.log(event.detail.value);
        this.account.Name=event.detail.value;
    }
    changeWebsite(event){
        this.account.Website=event.detail.value;
    }
    changePhone(event){
        this.account.Phone=event.detail.value;
    }
    changeAccountSource(event){
        this.account.AccountSource=event.detail.value;
    }
    changeAccountSource2(event){
        this.account.AccountSource2=event.detail.value;
    }
    EditRecord(){
        this.isEditingProhibited=false;
        this.isCancelNeed=false;
    }
    CancelEditRecord(){
        this.isEditingProhibited=true;
        console.log('editedAcc2:'+this.editedAccount.Name);
        this.account= {...this.editedAccount};
        console.log('editedAcc3:'+this.editedAccount.Name);
        console.log('account:'+this.account.Name);
        this.isCancelNeed=true;
    }
}

tests:

import { createElement } from 'lwc';
import AccountWebService from 'c/accountWebService';
import getAllAccounts from '@salesforce/apex/AccountServiceLWCHandler.GetAllAccounts';
import { registerApexTestWireAdapter } from '@salesforce/sfdx-lwc-jest';
import deleteAccount from '@salesforce/apex/AccountServiceLWCHandler.DeleteAccount';

const mockRecords = require('./data/getAllAccounts.json');
const getDataAdapter = registerApexTestWireAdapter(getAllAccounts);

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


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

    //THIS TEST FAIL
    it('ChangeName method testing', () => {
        const rowActionEvent = new CustomEvent("rowaction", {
            detail: 
            {
                row: 
                { 
                    Id:mockRecords[0].Id,
                    Name:mockRecords[0].Name,
                    Phone:mockRecords[0].Phone,
                    Website:mockRecords[0].Website,
                    AccountSource:mockRecords[0].AccountSource,
                    AccountSource2:mockRecords[0].AccountSource2
                }
            }
        });
        const changeValue = new CustomEvent("change", { detail:  { value:"newValue" } });
        const element = createElement('c-AccountWebService', {
            is: AccountWebService
        });
        document.body.appendChild(element);
        getDataAdapter.emit(mockRecords);
        const table = element.shadowRoot.querySelector('lightning-datatable');
        table.dispatchEvent(rowActionEvent);

        const testedElement = element.shadowRoot.querySelector('lightning-input[data-id=modalInputName]');
        //dispatch input name change event on modalInputName
        //a new error occurs on the line below : TypeError: Cannot read properties of null (reading 'dispatchEvent')
        testedElement.dispatchEvent(changeValue);
        //let the re-render happen based on value change
        
        return Promise.resolve().then(() => {
            expect(testedElement.value).toBe('newValue');
        });
    });
    it('CreateRecord button testing', () => {
        const element = createElement('c-AccountWebService', {
            is: AccountWebService
        });
        document.body.appendChild(element);
        getDataAdapter.emit(mockRecords);
        const button = element.shadowRoot.querySelector('lightning-button[data-id=CreateRecordButton]');
        button.dispatchEvent(new CustomEvent("click"));

        return Promise.resolve().then(() => {
            const lInput = element.shadowRoot.querySelector('lightning-input[data-id=modalInputName]');
            const section = element.shadowRoot.querySelector('section[data-id=modalWindow]');
            expect(section).not.toBeNull();
            expect(lInput).not.toBeNull();
            expect(lInput.label).toBe('Name');
            expect(lInput.value).toBe('TestAccountForWebService0');
        }); 
    });
    it('DeleteRecord button testing', () => {
        const MOCK_APEX_RESPONSE = mockRecords[0].Id;
        const rowActionEvent = new CustomEvent("rowaction", {
            detail: 
            {
                row: 
                { 
                    Id:mockRecords[0].Id,
                    Name:mockRecords[0].Name,
                    Phone:mockRecords[0].Phone,
                    Website:mockRecords[0].Website,
                    AccountSource:mockRecords[0].AccountSource,
                    AccountSource2:mockRecords[0].AccountSource2
                }
            }
        });
        const EXPECTED_APEX_PARAMETERS  =
            {
                "strId" : mockRecords[0].Id
            };
        deleteAccount.mockResolvedValue(MOCK_APEX_RESPONSE);
        
        const element = createElement('c-AccountWebService', {
            is: AccountWebService
        });
        document.body.appendChild(element);
        getDataAdapter.emit(mockRecords);
        const table = element.shadowRoot.querySelector('lightning-datatable');
        table.dispatchEvent(rowActionEvent);

        return Promise.resolve().then(() => {
            const button = element.shadowRoot.querySelector('lightning-button[data-id=DeleteRecordButton]');
            button.dispatchEvent(new CustomEvent("click"));
            expect(deleteAccount.mock.calls[0][0]).toEqual(EXPECTED_APEX_PARAMETERS);
        }); 
    });
    it('The modal window is NOT rendered and NOT filled', () => {
        const element = createElement('c-AccountWebService', {
            is: AccountWebService
        });
        document.body.appendChild(element);
        getDataAdapter.emit(mockRecords);
        return Promise.resolve().then(() => {
            const lInput = element.shadowRoot.querySelector('lightning-input[data-id=modalInputName]');
            const section = element.shadowRoot.querySelector('section[data-id=modalWindow]');
            expect(lInput).toBeNull();
            expect(section).toBeNull();
            const button = element.shadowRoot.querySelector('lightning-button[data-id=closeModalWindow]');
            expect(button).toBeNull();
        }); 
    });
    it('The modal window is rendered and filled AND CLOSED', () => {
        const rowActionEvent = new CustomEvent("rowaction", {
            detail: 
            {
                row: 
                { 
                    Id:mockRecords[0].Id,
                    Name:mockRecords[0].Name,
                    Phone:mockRecords[0].Phone,
                    Website:mockRecords[0].Website,
                    AccountSource:mockRecords[0].AccountSource,
                    AccountSource2:mockRecords[0].AccountSource2
                }
            }
        });
        const element = createElement('c-AccountWebService', {
            is: AccountWebService
        });
        document.body.appendChild(element);
        getDataAdapter.emit(mockRecords);
        const table = element.shadowRoot.querySelector('lightning-datatable');
        table.dispatchEvent(rowActionEvent);

        const handler = jest.fn();
        element.addEventListener('close_modal_event', handler);

        return Promise.resolve().then(() => {
            //const section = element.shadowRoot.querySelector('section[data-id=modalWindow]');
            //expect(section).not.toBeNull();
            const button = element.shadowRoot.querySelector('lightning-button[data-id=closeModalWindow]');
            button.click();
            expect(handler).toHaveBeenCalled();
        }); 
    });
    it('The modal window is rendered and filled', () => {
        const rowActionEvent = new CustomEvent("rowaction", {
            detail: 
            {
                row: 
                { 
                    Id:mockRecords[0].Id,
                    Name:mockRecords[0].Name,
                    Phone:mockRecords[0].Phone,
                    Website:mockRecords[0].Website,
                    AccountSource:mockRecords[0].AccountSource,
                    AccountSource2:mockRecords[0].AccountSource2
                }
            }
        });
        const element = createElement('c-AccountWebService', {
            is: AccountWebService
        });
        document.body.appendChild(element);
        getDataAdapter.emit(mockRecords);
        const table = element.shadowRoot.querySelector('lightning-datatable');
        table.dispatchEvent(rowActionEvent);
        return Promise.resolve().then(() => {
            const testedElement = element.shadowRoot.querySelector('lightning-input[data-id=modalInputName]');
            //expect(testedElement.label).toBe('Name');
            expect(testedElement.value).toBe('TestAccountForWebService');
        }); 
    });
    it('Renders my component', () => {
        // Create element
        const element = createElement('c-AccountWebService', {
            is: AccountWebService
        });
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            const content = document.body.querySelector('c-AccountWebService');
            expect(content).not.toBeNull();
        });
    });
    it('Renders my data', () => {
        // Create element
        const element = createElement('c-AccountWebService', {
            is: AccountWebService
        });
        document.body.appendChild(element);
        getDataAdapter.emit(mockRecords);

        return Promise.resolve().then(() => {
            const table = element.shadowRoot.querySelector('lightning-datatable[data-id=dataTable]');
            expect(table).not.toBeNull();
            expect(table.data.length).toBe(mockRecords.length);
            expect(table.data[0].Id).toBe(mockRecords[0].Id);
        }); 
    });
});

file for tests getAllAccounts.json:

[
    {
        "Id":"testId1",
        "Name":"TestAccountForWebService",
        "Phone":"000-000-0000",
        "Website":"SomeTestURL.com",
        "AccountSource":"Other",
        "AccountSource2":"NotOther"
    },
    {
        "Id":"testId2",
        "Name":"TestAccountForWebService2",
        "Phone":"000-000-0000",
        "Website":"SomeTestURL.com",
        "AccountSource":"Other",
        "AccountSource2":"NotOther"
    }
]
2
  • if you're trying to test the changeName handler, then you'd need to throw a change event from that specific input element so that handler can run. Is there a reason you're throwing a "success" event on that input element instead? Mar 7 at 22:46
  • This is my mistake, and I fixed it, but the test still fails for the same reason as before. Mar 8 at 7:28

1 Answer 1

2

When you emit the change event - you need to do it on the specific input element you're looking to change. You're currently doing it on the component itself

const element = createElement('c-AccountWebService', {
    is: AccountWebService
});
...
element.dispatchEvent(changeValue);

Whereas, you need to do it on the input you selected

return Promise.resolve().then(() => {
    const testedElement = element.shadowRoot.querySelector('lightning-input[data-id=modalInputName]');
    //dispatch input name change event on modalInputName
    testedElement.dispatchEvent(changeValue);
    ...
    

Likewise, you need to wait for the DOM update/re-rendering when the value changes.

const testedElement = element.shadowRoot.querySelector('lightning-input[data-id=modalInputName]');
//dispatch input name change event on modalInputName
testedElement.dispatchEvent(changeValue);
//let the re-render happen based on value change
return Promise.resolve().then(() => {
    expect(testedElement.value).toBe('newValue');
})

Edit:

You want to wait for DOM changes any time you dispatch an event or do an interaction that would cause one. In your test, you'd need to wait after the row action as well as after you change the input. You can accomplish this more cleanly with chaining your promises.

return Promise.resolve()
    .then(() => {
        //handlerRowAction changes (bShowModal=true) has happened so input is visible
        const testedElement = element.shadowRoot.querySelector('lightning-input[data-id=modalInputName]');
        testedElement.dispatchEvent(changeValue);
    })
    .then(() => {
        //re-render after onchange event on input has occurred
        const inputNameAfterChange = element.shadowRoot.querySelector('lightning-input[data-id=modalInputName]');
        expect(inputNameAfterChange.value).toBe('newValue');
    });
7
  • I corrected the code, it's really more correct, but it didn't help, the result is the same Mar 8 at 17:32
  • You have to let the dom re-render. Edited answer to include a resolved promise to allow that to happen before asserting on its value Mar 9 at 19:52
  • Then a new error occurs, on the line with dispatchEvent : Cannot read properties of null (reading 'dispatchEvent') Mar 11 at 6:53
  • could you edit your question with what you have after now? That error message indicates you're not finding your input with your query selector, but you clearly were before when you got different errors on the assert. Mar 11 at 12:17
  • 1
    The test passed!!!! But we need to make a small addition: we need to repeat the call of the element under test using the querrySelector before "expect()", inside the second "then" block. Otherwise, the compiler reports the error " 'testedElement' is not defined. ". Edit the answer and I'll mark it as correct. And thank you very much! Mar 14 at 14:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.