14

I thought this would be straight-forward, but it turns out to be a complete nightmare. Found no help on google, in the lwc-recipes or documentation.

Here's my issue: I have a LWC that makes multiple asynchronous server calls, two of them use the same getRecord method from lightning/uiRecordApi.

    @wire(getRecord, {recordId : '$opportunityId', fields: [
        OPPORTUNITY_STAGE_FIELD,
        OPPORTUNITY_SYNCED_QUOTE_STATUS_FIELD,
        OPPORTUNITY_CONTACT_ID,
        OPPORTUNITY_SYNCED_QUOTE_LINE_ITEM_COUNT,
        OPPORTUNITY_ACCOUNT_SHIPPING_ADDRESS_COMPLETE,
        OPPORTUNITY_ACCOUNT_BILLING_ADDRESS_COMPLETE
    ]})
    getOpportunity(result) {
        this.opportunity = result;
        this.contactId = getFieldValue(this.opportunity.data, OPPORTUNITY_CONTACT_ID);
    }

    @wire(getRecord, { recordId : '$contactId', fields: [CONTACT_EMAIL_FIELD]})
    contact;

    @wire(checkPermission)
    permission;

My code works: The data provisioned from getRecord is first written to the opportunity and then to the contact variable.

However, it turns out to be untestable. If I write it like this, the contact-data is written to both the opportunity and the contact variable. If I only emit OPPORTUNITY_VALID, both variables have the opportunity data.

test('valid opportunity and valid contact => submit button enabled', async () => {

    const element = createElement('c-create-order-from-opportunity', {
        is: createOrder 
    });

    element.opportunityId = OPPORTUNITY_VALID.id;
    getRecordWireAdapter.emit(OPPORTUNITY_VALID);
    getRecordWireAdapter.emit(CONTACT_VALID);    
    checkPermissionWireAdapter.emit(true);

    document.body.appendChild(element);

    await Promise.resolve();

    let submitButton = element.shadowRoot.querySelector('[data-id="submitButton"]');
    expect(submitButton.disabled).toBe(false);

    let msgBoxes = element.shadowRoot.querySelectorAll('c-message-box');
    expect(msgBoxes.length).toBe(0);

});

I also tried to use Promise.resolve() between .emit() calls, but this did not work either.

Can anybody help me writing solid unit tests for a component that uses multiple independent @wired properties/functions with getRecord calls? My last resort would be to stop using getRecord in both instances and write my own apex method for getting the contact data so I can then mock the apex method.

1
  • How you have fix this issue, have you used any alternative without using the apex call for one method? Jan 4, 2021 at 16:54

4 Answers 4

10

This is a known gap in the wire service test utility. It's been around for a while, but we haven't forgotten about it. There's currently some non-trivial changes being made to the internals of how the wire service and wire adapters interact. As soon as those internal settle we will solve this testing gap.

The best place to follow along for a fix would be in the @salesforce/wire-service-jest-util github repo. The specific issue filed for this is here: https://github.com/salesforce/wire-service-jest-util/issues/15

1
  • I have check the issue but didn't see any resolution about the issue. Do you have any alternative way to fix that or any suggestion for the same regarding fix Jan 4, 2021 at 16:48
4

Further to @TrevorBliss's answer - this has now been patched in the latest version (3.x or greater) of the @salesforce/wire-service-jest-util.

Paraphrasing from the changelog; to enable mocking data to multiple instances of the same wire adapter, a second argument should be provided to the emit method. See the (borrowed) example below:

// Your component under test code.
@wire(getRecord, { recordId: '1' }) recordOne;

@wire(getRecord, { recordId: '2' }) recordTwo;
// Your test code
getRecord.emit({ data: 'for record one' }, (config) => config.recordId === '1');

getRecord.emit({ data: 'for record two' }, (config) => config.recordId === '2');

The docs provide a bit more detail; the second argument should be a function which evaluates to a bool:

interface TestWireAdapter {
    /**
     * Emits any value of any shape.
     * @param value The value to emit to the component
     * @param filterFn When provided, it will be invoked for every adapter instance on the
     *                 component with its associated config; if it returns true, the value will be
     *                 emitted to that particular instance.
     */
    emit(value: object, filterFn?: (config) => boolean): void;

In my experience leaving a space in the config of the @wire method stopped this approach from working, which isn't documented behaviour, but ymmv. i.e.

// Doesn't work
@wire(getRecord, { recordId: '1' }) recordOne;

// Does work
@wire(getRecord, {recordId: '1' }) recordOne;
3

I had the same issue with getPicklistValues which I use two times to first get the country picklist values and second the state picklist values. I found this post and the answers from @TrevorBliss and @grbeazley which where quite helpful, but I missed a complete example.

I could solved it for getPicklistValues (which works mostly the same way - I checked not for a record id but for an object and because of the difficulties which come around with comparing objects I struggled a lot, so take care of that) and I tried it out as well for getRecord and it worked. I attach my code below.

The wire-service-jest-util is integrated in sfdx-lwc-jest, so there is no need to add wire-service-jest-util manually. But it should be checked that it is up-to-date.

Hope it helps.

testLwcComp.js

import { LightningElement, api, wire } from "lwc";
import { getRecord } from "lightning/uiRecordApi";
import ID from "@salesforce/schema/Account.Id";

export default class TestLwcComp extends LightningElement {
    @wire(getRecord, { recordId: "1234", fields: [ID] })
    firstRecord;

    @wire(getRecord, { recordId: "5678", fields: [ID] })
    secondRecord;

    @api getFirstRecord() {
        return this.firstRecord?.data?.id;
    }

    @api getSecondRecord() {
        return this.secondRecord?.data?.id;
    }
}

testLwcComp.test.js

import { createElement } from "lwc";
import testLwcComp from "c/testLwcComp";
import { getRecord } from "lightning/uiRecordApi";

describe("c-test-lwc-comp", () => {
    const mockGetRecordCloud = require("./data/cloud.json");
    const mockGetRecordStarship = require("./data/starship.json");

    // reset DOM after each test
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
        jest.clearAllMocks();
    });

    test("input => result", () => {
        // ARRANGE
        const element = createElement("c-test-lwc-comp", {
            is: testLwcComp
        });

        // ACT
        document.body.appendChild(element);
        getRecord.emit(mockGetRecordCloud, (config) => config.recordId === "1234"); 
        getRecord.emit(mockGetRecordStarship, (config) => config.recordId === "5678"); 

        // ASSERT
        expect(element.getFirstRecord()).toBe("1234");
        expect(element.getSecondRecord()).toBe("5678");
    });
});

cloud.json

{
  "id": "1234"
}

starship.json

{
  "id": "5678"
}
0

Try to use:

  function flushPromises() {
        return new Promise(resolve => setImmediate(resolve));
    }

  test('valid opportunity and valid contact => submit button enabled', async () => {

        const element = createElement('c-create-order-from-opportunity', {
            is: createOrder 
        });

        element.opportunityId = OPPORTUNITY_VALID.id;
        getRecordWireAdapter.emit(OPPORTUNITY_VALID);
        getRecordWireAdapter.emit(CONTACT_VALID);    
        checkPermissionWireAdapter.emit(true);

        document.body.appendChild(element);

        return flushPromises().then(() => {
             let submitButton = element.shadowRoot.querySelector('[data-id="submitButton"]');
             expect(submitButton.disabled).toBe(false);

             let msgBoxes = element.shadowRoot.querySelectorAll('c-message-box');
             expect(msgBoxes.length).toBe(0); 

        });
   });

You must log in to answer this question.

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