salesforce / sfdx-lwc-jest

Run Jest against LWC components in SFDX workspace environment
MIT License
164 stars 81 forks source link

Add support for testing components that use Inheritance #251

Closed AllanOricil closed 2 years ago

AllanOricil commented 2 years ago

Description

I have a quick action built with LWC that has to be used in multiple Objects. Since quick actions don't support passing parameters I have to create one Quick Action for each Object. Each quick action will share the same UI, the only difference is the Data that each display. To implement this and avoid code repetition, specifically to avoid having multiple dummy .html files on each one of the lwc components that are specific to each Object, I'm using inhertance, instead of composition. The base class takes care of rendering the UI and controlling the main logic of the component, and each child class provides data, as each object has its own way of fetching the data (SOQL queries vary).`

When I tried to write tests for the Child component, I got the following error message:

image

From the error message I concluded that this library does not allow us to Test child classes. I think you are not considering that if I extend a base class that is extending "LightningElement", this child class is also a "LightningElement". So it should be accepted by the "createElement" method.

Steps to Reproduce

To exemplify this, take a look at a simple version of this component

baseComponent.html

<template>
    <lightning-quick-action-panel
        title="Title"
        header="Title"
    >
        <div>shared ui</div>
        <div slot="footer">
                <lightning-button
                    class="slds-m-right_xx-small"
                    variant="neutral"
                    label="Cancel"
                    onclick={onCloseQuickAction}
                ></lightning-button>
          </div>
    </lightning-quick-action-panel>
</template>

baseComponent.js

export default class BaseComponent extends LightningElement {
    //You might be asking why I did this, and the answer is in the link bellow
    //https://salesforce.stackexchange.com/questions/344045/recordid-is-undefined-in-lwc-quick-action-component
    _recordId;

    @api
    set recordId(value) {
        this._recordId = value;
        this.setupData();
    }

    get recordId() {
        return this._recordId;
    }

   setupData() {
        Promise.all([`
            /* this can be any method that fetches data that is required by every quick action. I left it commented so you can just understand why Im using inheritance here
               fetchDataThatIsGenericForAllQuickActions(),
           */
            /*
                resolveContacts comes from the component that extends BaseComponent. This method MUST return a promise
            */
            this.resolveContacts()
        ])
            .then((responses) => {
                //Do something
            })
            .catch((errors) => {
                //Do something
            })
            .finally(() => {
                //Do something
            });
    }

    //...

    onCloseQuickAction() {
        this.dispatchEvent(new CloseActionScreenEvent());
    }

}

baseComponent.js-meta.xml

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

childComponent.js

import BaseComponent from 'c/baseComponent';
import fetchContacts from '@salesforce/apex/MY_CONTROLLER.fetchContacts';

export default class ChildComponent extends BaseComponent {
    //BaseComponent class is responsible for rendering the component
    //this is only being used to determine how Contacts are fetched for the Opportunity object
   //As the query can vary for different objects, and we can't pass parameters for quick actions, each quick action will have its own lwc component which will only serve to Provide/Resolve data.
    resolveContacts() {
        return fetchContacts({ opportunityId: this.recordId });
    }
}

childComponent.js-meta.xml

<?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__RecordAction</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordAction">
            <actionType>ScreenAction</actionType>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

childComponent.test.js

import { createElement } from 'lwc';
import ChildComponent from 'c/childComponent';
import BaseComponent from 'c/baseComponent';

jest.mock('c/baseComponent');

describe('c-child-component', () => {

    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);
        }
    });

    it('TODO: test case generated by CLI command, please fill in test logic', () => {
        const element = createElement('c-child-component', {
            is: ChildComponent
        });

        document.body.appendChild(element);
        expect(1).toBe(2);
    });
});

I could have used Composition, but I would end up with lots of dummy .html, one for each component, that would be exactly the same, thus requiring code repetition.

Expected Results

Child/Sub classes that exnteds a Parent/Base class that extends "LightningElement" should be allowed on "createElement" method.

Actual Results

TypeError: class DspOpportunityRequestSignatureAction extends _dspRequestSignature.default {
      //DSPRequestSignature is responsible for rendering the component
      //this is only being used to determine how Contacts are fetched for the Opportunity object
      resolveContacts() {
        return fetchContacts({
          opportunityId: this.recordId
        });
      }

    } is not a valid component, or does not extends LightningElement from "lwc". You probably forgot to add the extend clause on the 
class declaration.

Version

Possible Solution

The problem is here, I think:

image

Additional context/Screenshots

AllanOricil commented 2 years ago

My real implementation works as expected. I can't just write jest test for it :/

AllanOricil commented 2 years ago

If I remove these lines

import BaseComponent from 'c/baseComponent';

jest.mock('c/baseComponent');

createElement does not throw the error. But I can't remove those lines because I have to mock the baseClass, right?

AllanOricil commented 2 years ago

Ok, no need for that. Then to avoid repeating tests across all children I will isolate the common ones in a separate module and then import on each test.