sfdx-lwc-jest icon indicating copy to clipboard operation
sfdx-lwc-jest copied to clipboard

Documentation on "testing that a component dispatch an event"

Open edmondop opened this issue 3 years ago • 19 comments

Description

The example shows how to test events dispatching, but events are dispatched explicitly. https://github.com/trailheadapps/lwc-recipes/blob/master/force-app/main/default/lwc/miscNotification/tests/miscNotification.test.js

One additional pattern is instead verifying that for example, upon invoking a wire service that returns an error, the component dispatch the event, i.e. we do not want to dispatch the event explicitly.

Steps to Reproduce

import MyComponent from "c/myComponent";
import { registerLdsTestWireAdapter } from "@salesforce/sfdx-lwc-jest";
import myMethod from "@salesforce/apex/MyController.MyMethod";
import { ShowToastEventName } from "lightning/platformShowToastEvent";
import { createElement } from "lwc";

const mockMyMethod = registerLdsTestWireAdapter(
  myMethod
);

const mockErrorResponse = require("./data/errorResponse.json");

describe("My Component", () => {
  afterEach(() => {
    while (document.body.firstChild) {
      document.body.removeChild(document.body.firstChild);
    }
  });
  
  it("should correctly show a toast when the API returns an error", () => {
    const element = createElement("c-my-component", {
      is: MyComponent,
    });
    const handler = jest.fn();
    document.body.appendChild(element);
    element.addEventListener(ShowToastEventName, handler);
    myMethod.error(
      mockErrorResponse.body,
      mockErrorResponse.status
    );
    return Promise.resolve().then(() => {
      expect(handler).toHaveBeenCalled();     
    });
  });
});

<!-- HTML for component under test -->
<template>
    ...
</template>
import { api, LightningElement, wire, track } from "lwc";
import myMethod from "@salesforce/apex/MyController.MyMethod";
import { ShowToastEvent } from "lightning/platformShowToastEvent";

export default class MyComponent extends LightningElement {

  @api recordId = "who-cares";

  @track
  data;

  @wire(myMethod, { myObjId: "$recordId" })
  wiredRecord({ data, error }) {
    if (data) {
      this.data = data;
    } else {
      if (error) {
        let errorMessage = "Unknown error";

        if (Array.isArray(error.body.message)) {
          errorMessage = error.body.message.map((e) => e.message).join(", ");
        } else if (typeof error.body.message === "string") {
          errorMessage = error.body.message;
        }
        this.dispatchEvent(
          new ShowToastEvent({
            title: "Error when invoking the Controller API",
            message: errorMessage,
            variant: "error",
          })
        );
      }
    }
  }


}

# Command to repro
sfdx-lwc-jest -- --no-cache

Expected Results

the Jest handler is invoked once

Actual Results

Expected number of calls: >= 1 Received number of calls: 0

Version

  • @salesforce/sfdx-lwc-jest: 0.10.2
  • Node: 12.12.0

edmondop avatar Nov 07 '20 12:11 edmondop

This issue here is that the platformShowToast stub does not export the event name so your handler in your test is never configured correctly.

https://github.com/salesforce/sfdx-lwc-jest/blob/master/src/lightning-stubs/platformShowToastEvent/platformShowToastEvent.js

damianpoole avatar Nov 17 '20 09:11 damianpoole

@damianpoole how am I even able to import it then?

import { ShowToastEventName } from "lightning/platformShowToastEvent";

edmondop avatar Nov 17 '20 09:11 edmondop

Did you stub it? https://github.com/trailheadapps/lwc-recipes/blob/master/force-app/test/jest-mocks/lightning/platformShowToastEvent.js

muenzpraeger avatar Nov 17 '20 09:11 muenzpraeger

@muenzpraeger what needs to be done to stub specifically?

edmondop avatar Nov 17 '20 09:11 edmondop

ShowToastEventName is not exported, as @damianpoole already pointed out. That's one of the changes in the custom mock that I shared.

You would have to hardcode lightning__showtoast in your test, and not import, if you want to use the standard mock. Also note that the standard mock does not include the event details. So if you want to test any of the details, like variant, message etc, you'd have also to use the custom mock.

muenzpraeger avatar Nov 17 '20 09:11 muenzpraeger

Also see here the Jest config to us the mock.

Using this custom mock for platformShowToastEvent is also described in the official LWC Jest documentation here (search for Module Imports).

muenzpraeger avatar Nov 17 '20 09:11 muenzpraeger

Just to be sure, there are two ways I can act:

  • create a custom mock and change my jest configuration
  • hardcode the name

When the jest config mention these folders, should I place my custom mocks there? or are these available as a consequence of importing some additional npm modules? i.e. who places something in '<rootDir>/force-app/test/jest-mocks/lightning/navigation' ?

const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
module.exports = {
    ...jestConfig,
    moduleNameMapper: {
        '^@salesforce/apex$': '<rootDir>/force-app/test/jest-mocks/apex',
        '^lightning/navigation$':
            '<rootDir>/force-app/test/jest-mocks/lightning/navigation',
        '^lightning/platformShowToastEvent$':
            '<rootDir>/force-app/test/jest-mocks/lightning/platformShowToastEvent',
        '^lightning/uiRecordApi$':
            '<rootDir>/force-app/test/jest-mocks/lightning/uiRecordApi'
    }
};

Thanks a lot for your help

edmondop avatar Nov 17 '20 09:11 edmondop

Yes you would place your custom mock there or change the reference to a folder that better suits your project.

damianpoole avatar Nov 17 '20 10:11 damianpoole

but the question is, why doesn't the import statement fail in npm if that constant is not exported?

edmondop avatar Nov 17 '20 10:11 edmondop

AFAIK because you are creating a variable called ShowToastEventName, it is not being set to anything that is exported therefore the variable exists it is just undefined.

damianpoole avatar Nov 17 '20 10:11 damianpoole

Now the handler is getting triggered, except that the event.detail is null in the handler so if I had the following assertion, the test fails

      expect(handler.mock.calls[0][0].detail.title).toBe(
        "Error when invoking the Controller API"
      );
      expect(handler.mock.calls[0][0].detail.variant).toBe("error");

edmondop avatar Nov 17 '20 15:11 edmondop

Does your mock constructor pass the detail along like the example @muenzpraeger linked?

https://github.com/trailheadapps/lwc-recipes/blob/master/force-app/test/jest-mocks/lightning/platformShowToastEvent.js#L10

damianpoole avatar Nov 17 '20 15:11 damianpoole

I am not mocking the event, it is dispatched from within the component. I hardcoded the event name and subscribed to it in my test

edmondop avatar Nov 17 '20 16:11 edmondop

@edmondo1984 Do you use the stub that I linked?

muenzpraeger avatar Nov 17 '20 16:11 muenzpraeger

No @muenzpraeger I understood it was enough to perform this change in my unit test:

element.addEventListener("lightning__showtoast", handler);

edmondop avatar Nov 17 '20 16:11 edmondop

As mentioned earlier:

If you want to test any of the details, like variant, message etc, you'd have also to use the custom mock.

muenzpraeger avatar Nov 17 '20 16:11 muenzpraeger

Thanks, I didn't understood that. Can you please explain why?

edmondop avatar Nov 17 '20 16:11 edmondop

Because the detail property of the CustomEvent is not included in the default provided mock, that's the relevant modification in our custom version.

muenzpraeger avatar Nov 18 '20 07:11 muenzpraeger

Excellent, thank you. It was basically me getting confused, maybe I can submit a PR to the documentation saying that you need to be careful about default stubs not stubbing all the properties?

edmondop avatar Nov 18 '20 08:11 edmondop