4

I am trying to prove that the price is being set on the component.

I have the following HTML:

<template>
    <h1>Current price of a bitcoin in Euro</h1>
    <lightning-formatted-number 
        currency-code="EUR"
        format-style="currency"
        value={euroPrice}>
    </lightning-formatted-number>
</template>

And the following JavaScript:

import {LightningElement, api, track} from 'lwc';

const bitcoinPriceProvider = 'https://api.coinbase.com/v2/prices/spot?currency=EUR';
export default class BitcoinEuroPrice extends LightningElement {
    @api
    refreshRateInSeconds;

    @track
    euroPrice;

    connectedCallback() {
        this.fetchPrice();
        setInterval(this.fetchPrice, this.refreshRateInSeconds * 1000);
    }

    fetchPrice() {
        fetch(bitcoinPriceProvider)
            .then(response => response.json())
            .then(json => {
                this.euroPrice = json.data.amount;
                console.log('######## this.euroPrice: ', this.euroPrice);
            });
    }
}

This is my test:

import {createElement} from 'lwc';
import BitcoinEuroPrice from 'c/bitcoinEuroPrice';

describe('c-bitcoinEuroPrice', () => {
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('fetches price', () => {
        // Arrange
        const testAmount = '8448.947391885';
        const mockApiResponse = {
            'data': {
                'base': 'BTC',
                'currency': 'EUR',
                'amount': testAmount
            }
        };
        fetch = global.fetch = mockFetch(mockApiResponse);

        // Act
        const priceUnderTest = createElement('c-bitcoinEuroPrice', {
            is: BitcoinEuroPrice
        });
        document.body.appendChild(priceUnderTest);

        // Assert
        expect(global.fetch).toHaveBeenCalledTimes(1);
        expect(global.fetch).toHaveBeenCalledWith('https://api.coinbase.com/v2/prices/spot?currency=EUR');

        const displayPrice = priceUnderTest.shadowRoot.querySelector('lightning-formatted-number');
        console.log('##### priceUnderTest', priceUnderTest);
        console.log('##### displayPrice', displayPrice);
        expect(displayPrice.value).toEqual(testAmount);
    });

    function mockFetch(mockApiResponse) {
        return jest
            .fn()
            .mockImplementationOnce(() =>
                Promise.resolve({
                    ok: true,
                    json: () => Promise.resolve(mockApiResponse),
                    jsonResponse: mockApiResponse
                })
            );
    }
});

This fails with:

 expect(received).toEqual(expected) // deep equality
 
     Expected: "8448.947391885"
     Received: undefined
 
       34 |         console.log('##### priceUnderTest', priceUnderTest);
       35 |         console.log('##### displayPrice', displayPrice);
     > 36 |         expect(displayPrice.value).toEqual(testAmount);
          |         ^
       37 |     });
       38 |

but further down the log I see:

   console.log
 force-app/main/default/lwc/bitcoinEuroPrice/bitcoinEuroPrice.js:21
     ######## this.euroPrice:  8448.947391885

My hypothesis is that the test is not waiting for the asynchronous chain to complete before checking the value.

How can I make it wait? Or is there some other way I should check the value?

2 Answers 2

9

Figured out a way to flush the promises by applying answer from https://stackoverflow.com/questions/44741102/how-to-make-jest-wait-for-all-asynchronous-code-to-finish-execution-before-expec

essentially just needed to:

  1. Add this function/constant:
const flushPromises = () => new Promise(setImmediate);
  1. Make the test async:
it('fetches price', async () => {
  1. Wait for the flush:
        document.body.appendChild(priceUnderTest);
        await flushPromises();
5
  • 1
    setImmediate no longer works with the version of jsdom lwc uses Commented Aug 18, 2021 at 19:39
  • Just hit the same issue after updating lwc-jest. @TemporaryFix have you found alternative?
    – dzh
    Commented Aug 25, 2021 at 10:49
  • 1
    import 'setImmediate' seemingly solved it
    – dzh
    Commented Sep 6, 2021 at 23:58
  • 3
    Or better yet put "import { setImmediate } from 'timers';" at the top of your test.js program. If that errors out then install the node timer module. Commented Sep 7, 2021 at 9:50
  • 1
    Just wanted to come back and say there's an updated way to do this with process.nextTick. See more here and here Commented Jan 24, 2022 at 0:15
1

I ran into an issue with testing async functionality. I attempted to use the flushPromises utility method mentioned in the other answer, and while that works for Promises that have resolved and their callbacks were added to the task queue it does not work for Promises that have not resolved.

Instead of waiting for an unsure amount of time with setTimeout to make sure your async functionality has finished before running your assertions, I built a simple utility called waitFor

This is a concept taken from reactjs's testing library. Their implementation is a lot more complex than my simple utility function, but it gets the job done.

In case the gist link ever becomes broken this is the utility, add it to your jest.setup.js file.

global.waitFor = (expectationFunction) => {
    const MAX_WAIT = 4000;
    const POLL_INTERVAL = 100;
    return new Promise((resolve, reject) => {
        const startTime = Date.now();
        let error;
        const interval = setInterval(() => {
            try {
                if (Date.now() - startTime < MAX_WAIT) {
                    expectationFunction();
                    clearInterval(interval);
                    resolve();
                } else {
                    clearInterval(interval);
                    reject(error);
                }
            } catch (e) {
                error = e.matcherResult;
            }
        }, POLL_INTERVAL);
    });
};

// in your test method call waitFor like this
const button = element.shadowRoot.querySelector('button');
button.dispatchEvent(new CustomEvent('click')); // calls an async handler

await waitFor(() => { expect(createUserKey.mock.calls.length).toBe(1) });

Make sure your linter is aware this is a global function by adding this to your .eslintrc.json file:

"globals": {
  "waitFor": true,
},

You must log in to answer this question.

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