Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I test `image.onload` using jest in the context of redux actions (or other callbacks assigned in the action)

My problem was that I am trying to make a unit test for a function but can't figure out how to test a part of it.

This is a react / redux action that does the following:

1) retrieves json data with an image url

2) loads the image into an Image instance and dispatches its size to the reducer (asynchronously when image is loaded using Image.onload)

3) dispatches that the fetch was completed to the reducer

The image onload happens asynchronously, so when I try to unit test it it wouldn't be called. Moreover, I can't just mock things out because the image instance is created within the function...

Here's the code I wanted to test (removing some checks, branching logic, and stuff):

export function fetchInsuranceCardPhoto() {
    return dispatch => {
        dispatch(requestingInsuranceCardPhoto());
        return fetch(`${api}`,
            {
                headers: {},
                credentials: 'same-origin',
                method: 'GET',
            })
            .then(response => {
                switch (response.status) {
                    case 200:
                        return response.json()
                            .then(json => {
                                dispatch(receivedInsuranceCardPhoto(json));
                            })
                }
            });
    };
}

function receivedInsuranceCardPhoto(json) {
    return dispatch => {
        const insuranceCardFrontImg = json.insuranceCardData.url_front;
        const insuranceCardBackImg = json.insuranceCardData.url_back;

        if (insuranceCardFrontImg) {
            dispatch(storeImageSize(insuranceCardFrontImg, 'insuranceCardFront'));
        }

        return dispatch(receivedInsuranceCardPhotoSuccess(json));
    };
}

function receivedInsuranceCardPhotoSuccess(json) {
    const insuranceCardFrontImg = json.insuranceCardData.url_front;
    const insuranceCardBackImg = json.insuranceCardData.url_back;
    const insuranceCardId = json.insuranceCardData.id;

    return {
        type: RECEIVED_INSURANCE_CARD_PHOTO,
        insuranceCardFrontImg,
        insuranceCardBackImg,
        insuranceCardId,
    };
}

function storeImageSize(imgSrc, side) {
    return dispatch => {
        const img = new Image();
        img.src = imgSrc;
        img.onload = () => {
            return dispatch({
                type: STORE_CARD_IMAGE_SIZE,
                side,
                width: img.naturalWidth,
                height: img.naturalHeight,
            });
        };
    };
}

Notice in that last storeImageSize private function how there's an instance of Image created and an image.onload that is assigned to a function.

Now here's my test:

it('triggers RECEIVED_INSURANCE_CARD_PHOTO when 200 returned without data', async () => {
        givenAPICallSucceedsWithData();

        await store.dispatch(fetchInsuranceCardPhoto());

        expectActionsToHaveBeenTriggered(
            REQUESTING_INSURANCE_CARD_PHOTO,
            RECEIVED_INSURANCE_CARD_PHOTO,
            STORE_CARD_IMAGE_SIZE,
        );
    });

This test though will fail because the test finishes before the image.onload callback is called.

How can I force the image.onload callback to be called so that I can test that the `STORE_CARD_IMAGE_SIZE action gets broadcasted?

like image 582
ThinkBonobo Avatar asked Dec 24 '22 12:12

ThinkBonobo


1 Answers

After some investigation, I found a very interesting javascript function that would solve my issue.

It is this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Here's how I used Object.defineProperty(...) to solve my issue:

describe('fetchInsuranceCardPhoto', () => {
    let imageOnload = null;


    /** Override Image global to save onload setting here so that I can trigger it manually in my test */
    function trackImageOnload() {
        Object.defineProperty(Image.prototype, 'onload', {
            get: function () {
                return this._onload;
            },
            set: function (fn) {
                imageOnload = fn;
                this._onload = fn;
            },
        });
    }

    it('triggers RECEIVED_INSURANCE_CARD_PHOTO when 200 returned with data', async () => {
        trackImageOnload();
        givenAPICallSucceedsWithData();

        await store.dispatch(fetchInsuranceCardPhoto());
        imageOnload();

        expectActionsToHaveBeenTriggered(
            REQUESTING_INSURANCE_CARD_PHOTO,
            RECEIVED_INSURANCE_CARD_PHOTO,
            STORE_CARD_IMAGE_SIZE,
        );
    });

What I did here was use define property to override the setter of any instance of Image. the setter would continue to get or set like normal but would also save the value (in this case a function) that was set to a variable in the scope of the unit test. After which, you can just run that function you captured before the verification step of your the test.

Gotchas - configurable needs to be set - note that defineProperty is a different function than defineProperties - This is bad practice in real code. - remember to use the prototype

Hope this post can help a dev in need!

like image 73
ThinkBonobo Avatar answered Dec 31 '22 03:12

ThinkBonobo