4

The component itself, when loaded, queries some data from the database. Then, depending on what it finds, loads the specific template. One of those has a button, which will call the server again, and when it does it will display either a success or error toast on the interface.

I can mock the Apex calls with jest's mockRejectedValue or mockResolvedvalue alright, and I can return a Promise at the end of the test to wait for the first DOM change (that is, when the component is loaded, and it calls the server to fetch the records and decide what to render). But in this case, I need to force another DOM change.

  1. Component is set up
  2. Component is loaded (Promise used)
  3. Button is shown on the component after loading the database mocked result
  4. Click on the button is simulated
  5. The click fires a method on the component that calls another Apex method.
  6. Should I return another promise here? How?

I have the following test for this situation:

it('Should display a toast when the user checks in.', () => {

    // mock the two calls to the database that returns
    // a boolean each indicading wether the check in or check out
    // is available - it is used to display the buttons
    isCheckInAvailable.mockResolvedValue(CHECK_INOUT_AVAILABLE)
    isCheckOutAvailable.mockResolvedValue(CHECK_INOUT_AVAILABLE)

    // mock the database check-in
    doCheckIn.mockRejectedValue(CHECK_IN_ERROR)

    // create the component
    const recordId = '00Q3F000002StmkUAC'
    const comp = createElement('c-checkin', {
        is: CheckIn
    })
    // set some of the attributes
    comp.recordId = recordId
    comp.markers = []

    // create the event handler and assign it to the component
    const eventHandler = jest.fn()
    comp.addEventListener(ShowToastEventName, eventHandler)

    // put the component on the "screen"
    document.body.appendChild(comp)

    // first promise to resolve the first Apex calls
    return Promise.resolve(() => {
        // markers shouldn't exist, since we specified an empty list,
        // but it should have a length (it shouldn't be undefined)
        expect(comp.markers).toHaveLength(0)
        expect(comp.buttons.checkInEnabled).toBe(true)
        // get the check-in button, which should be displayed on the
        // screen now, after the component loads
        comp.shadowRoot.querySelector('lightning-button[label="Check-IN"]').click()
    }).then(() => {
        // TEST FAILS HERE!
        // "Expected mock function to have been called, but it was not called."
        expect(eventHandler).toHaveBeenCalled()
        expect(eventHandler.mock.calls[0][0].detail.variant).toBe('success')
        expect(eventHandler.mock.calls[0][0].detail.message).toBe('Check-In realizado!')
        expect(eventHandler.mock.calls[0][0].detail.title).toBe('OK')
    })
})

Edit 6/3/2019

I expected the "Check-in" button to be displayed, and it seems that it is displayed (otherwise the "click" part in the end of the test would fail). The click button is tied to a lightning-button on the HTML:

<lightning-button
    variant="brand"
    label="Check-IN"
    onclick={handleCheckIn}
    class="slds-m-around_small"
></lightning-button>

That click event calls the JS method:

handleCheckIn () {
    var args = {}
    if (this.coordinates.lat && this.coordinates.lon) {
        args = {
            recordId: this.recordId,
            latitude: this.coordinates.lat,
            longitude: this.coordinates.lon
        }
    } else {
        args = { recordId: this.recordId }
    }
    doCheckIn(args)
        .then(result => { // eslint-disable-line no-unused-vars
            this.dispatchEvent(new ShowToastEvent({
                title: 'OK',
                message: 'You have checked in!',
                variant: 'success'
            }))
            this.buttons.checkInEnabled = false
        })
        .catch(error => { // eslint-disable-line no-unused-vars
            this.dispatchEvent(new ShowToastEvent({
                title: 'Erro',
                message: 'Something went wrong while doing the check-in.',
                variant: 'error'
            }))
        })
        .finally(() => {
            this.showCloseButton = true
        })
}

And the doCheckIn is tied to the Apex code, which is what I'm trying to mock/test on the second part.

The idea here is that the component is loaded, then it calls Apex. It then presents information to the user. Then if the user clicks a button, it calls Apex again, and it displays a toast depending on the call result.

it.only('should show the check-in toast.', () => {

    // mocks the result that enables the check-in button
    isCheckInAvailable.mockResolvedValue(true)
    // mocks the result that disables the check-out button
    isCheckOutAvailable.mockResolvedValue(false)
    // mocks the result of the check-in action
    doCheckIn.mockResolvedValue({})

    // set a random record Id
    const recordId = '00Q3F000002StmkUAC'

    // create the component
    const comp = createElement('c-checkin', {
        is: CheckIn
    })
    comp.recordId = recordId

    // set up a mock for the toast event
    const toastEventHandler = jest.fn()
    // add the event listener to the component
    comp.addEventListener(ShowToastEventName, toastEventHandler)

    // spawn the component
    document.body.appendChild(comp)

    return Promise.resolve(() => {
        // checkInEnabled is a property that is used on the `disabled` attribute
        // on the lightning-button, it should be true for the button to be enabled
        expect(comp.checkInEnabled).toBe(true)

        // query the button, and click it
        comp.shadowRoot.querySelector('lightning-button[label="Check-IN"]').click()
    }).then(() => {
        // when the button is clicked by the user, it should fire the doCheckIn method
        // and call Apex, so I expect it to have been called.
        expect(doCheckIn).toHaveBeenCalled()
        // BUT IT DOES NOT! And the test fails here
    })
})
7
  • 1
    Are you running this test with @saleforce/lwc-jest? If so I recommend running with the "--debug" flag passed and then set a debugger in the click handler that the lightning-button invokes. Once you are sure the click is functioning properly you can see if it's a timing/promise issue or not. May 15, 2019 at 20:03
  • did you get this figured out? If you did, it'd be great to post what you ended up doing as the accepted answer to help future visitors. If this is still an issue could you please post the relevant html and javascript code of your component under test. Jun 3, 2019 at 20:41
  • I didn't, but I did get this response from René Winkelmeyer (who contributes to the lwc-recipes repo): github.com/trailheadapps/lwc-recipes/issues/100. Jun 3, 2019 at 22:04
  • Yes, I agree it's most likely a timing issue. Editing the question to include the html and javascript would help for us to get to the bottom of it. Jun 3, 2019 at 23:01
  • I've added more pieces of the code to the question. I also revisited the thread on GitHub, and tried to play with the .then chaining, but with no success. Jun 4, 2019 at 0:00

1 Answer 1

1

Most likely the doCheckIn is not being mocked properly. At the top of your test file, add something similar to the following (swapping in the name of your Apex controller):

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

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

// use the doCheckIn mock as normal in your test cases

By default if your test is run with sfdx-lwc-jest, the Apex import will return a function that resolves a promise. This is to help make testing easier by providing a reasonable default when the server and true Apex calls are not available. To share a mock for this Apex import that is used in your component under test but inspected in your test case, you need the jest.mock call so the same mock object is returned in both files.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .