What is the best approach for testing async XMLHttpRequest callbacks using Jest?

In my Jest test, I am trying to call a mock server using ajax with XMLHttpRequest:

import mock from "xhr-mock";

describe("ajax callbacks", function() {
  beforeEach(function() {
    mock.setup();
  });

  afterAll(function() {
    mock.teardown();
  });

  it("gets called when done", function(done) {
    mock.get("/get-url", {
      status: 200,
      body: '{ "key": "value" }'
    });

    const doneCallback = jest.fn();

    const xhr = new XMLHttpRequest();
    xhr.open("GET", "/get-url");
    xhr.onreadystatechange = function() {
      if (xhr.readyState === XMLHttpRequest.DONE) {
        doneCallback();
        done();
      }
    }
    xhr.send();

    expect(doneCallback).toHaveBeenCalled();
  });
});

It's failing because the AJAX call is asynchronous and the expectation is made before the callback is called.

Is there a way for Jest to wait until the callback is called before making the expectation?

Please keep in mind that I cannot change the request to synchronous or switch to a Promise-based API just for testing purposes. This simplified version of the test may differ from the actual code being tested, which contains different abstractions.

Answer №1

If you're looking to simulate XMLHttpRequests, Sinon is a great tool to achieve that.

import { createFakeServer } from 'sinon';

describe('Example', () => {

    let xhr;

    beforeEach(() => xhr = createFakeServer());

    afterEach(() => xhr.restore());

    test('example calls callback', () => {
        jest.spyOn(exampleObject, 'exampleCallback');

        xhr.respondWith('POST', '/expected/url', [200, {}, JSON.stringify({ foo: 'response' })]);

        exampleObject.funcToDoRequest();

        xhr.respond();
    
        expect(exampleObject.exampleCallback).toBeCalledWith({ foo: 'response' });
    });

});

To delve deeper into this topic, check out https://sinonjs.org/releases/v9.2.0/fake-xhr-and-server/

Alternatively, if you are using xhr-mock, employing setTimeout can be another approach. You can encapsulate the callback assertion within setTimeout and trigger the done() callback for the Jest test. It may feel like a workaround, but it gets the job done.

rest of your code
...

setTimeout(() => {
  expect(doneCallback).toHaveBeenCalled();
  done();
});

Answer №2

I managed to solve the problem by utilizing Jest's built-in async/await functionality. The workaround involves wrapping the asynchronous request in a Promise and resolving it when the onreadystatechange callback is triggered. Here is the code snippet:

import mock from "xhr-mock";

describe("ajax callbacks", function() {
  beforeEach(function() {
    mock.setup();
  });

  afterAll(function() {
    mock.teardown();
  });

  it("executes upon completion", async function() {
    mock.get("/get-url", {
      status: 200,
      body: '{ "key": "value" }'
    });

    const doneCallback = jest.fn();

    const xhr = new XMLHttpRequest();
    xhr.open("GET", "/get-url");

    await new Promise(function(resolve) {
      xhr.onreadystatechange = function() {
        if (xhr.readyState === XMLHttpRequest.DONE) {
          doneCallback();
          resolve();
        }
      }
      xhr.send();
    });

    expect(doneCallback).toHaveBeenCalled();
  });
});

By using await, the test will pause until the Promise is resolved. Although this approach may seem unconventional, it was the most effective solution I could find. I explored other options but none seemed as viable.

If you want to learn more about incorporating async/await with Jest, check out the documentation here.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Customize your Shopify Messenger icon using JavaScript!

Shopify recently introduced a new feature that allows customers to contact store owners through messenger. My client has requested me to customize the appearance of this icon with their branded icon instead. https://i.stack.imgur.com/uytpd.png I have not ...

The pagination feature of the material-ui data grid is experiencing issues with double clicks because of its compatibility with the react-grid-layout library for

I am currently using the react-grid-layout library to manage the resizing of both charts and a material-ui data grid table. However, I am encountering an issue where when clicking on the table pagination arrow, it does not work properly. I have to click tw ...

Unable to import necessary modules within my React TypeScript project

I am currently building a React/Express application with TypeScript. While I'm not very familiar with it, I've decided to use it to expand my knowledge. However, I've encountered an issue when trying to import one component into another comp ...

Need to specify a parameter type for the input tag in HTML

Currently, I am developing a customized focus method for my webpage using the following script: <script type="text/javascript"> function myFunction() { document.getElementById("name").focus(); } </script> I ...

The initialization of the R Shiny HTML canvas does not occur until the page is resized

I am currently facing an issue while integrating an HTML page with a canvas into my shiny R application using includeHTML(). The packages I am using are shiny, shinydashboard, shinycssloaders, dplyr, and DT. Everything is working perfectly fine except for ...

Tips for continuously running a loop function until retrieving a value from an API within a cypress project

Need help looping a function to retrieve the value from an API in my Cypress project. The goal is to call the API multiple times until we receive the desired value. let otpValue = ''; const loopFunc = () => { cy.request({ method: &ap ...

React/Redux Component fails to re-render

I've encountered an issue with a react component not re-rendering even though the state appears to be updated correctly. I am using redux and suspect that state mutation may be the root cause, although I don't believe I am doing that. The specifi ...

The data contained in the Response is an extensive script instead of the intended JSON object

Currently, I am in the process of developing a web application using Laravel and Vue.js. In order to retrieve a list of users, I have implemented an axios GET request. However, the response I am receiving is in the form of a Promise object, which I have le ...

Exploring the possibilities of combining OpenCV with web technologies like Javascript

I am currently faced with the challenge of integrating OpenCV into a web application using Express.js. While I am familiar with the Python and C++ libraries for OpenCV, I now need to work with it in conjunction with Express.js. My main requirements include ...

Is there a way to retrieve text from within a div using code behind in asp.net with c#?

I am currently developing a SQL QUERY editor where users can enter queries. The entered text is then passed to a SQL COMMAND for execution, and the results are displayed to the user in a GRIDVIEW. However, I am facing an issue in retrieving the text entere ...

Independent Dropbox Collections

Query: function addRow(tableID) { var table = document.getElementById(tableID); var rowCount = table.rows.length; var row = table.insertRow(rowCount); var colCount = table.rows[0].cells.length; for (var i = 0; i < colCount; i++) { var ...

Struggling with incorporating a print preview feature using JavaScript?

I am currently working on adding a print preview feature to a page that opens in PDF format. Unfortunately, I am facing an issue where the print function does not execute and nothing happens when the page is opened. I am using Python in conjunction with ...

Shuffle the JSON data before displaying it

Just a heads up, the code you're about to see might make you cringe, but I'm doing my best with what I know. I've got a JSON file with a bunch of questions, here's what it looks like: { "questions": [ { "id": 1 ...

Extract particular elements from a nested JSON response in order to convert them into a string

I am currently integrating Microsoft Azure Cognitive Services text recognition API into a JavaScript web application. After successfully extracting text from an image, the response looks like this: { "language": "en", "textAngle": 0, ...

Overlap one element entirely with another

Currently, I am working on a way for an element called overlayElement to completely cover another element known as originalElement. The overlayElement is an iFrame, but that detail may not be significant in this scenario. My goal is to ensure that the over ...

Make object calculations easy with the power of JavaScript

I've stored a JSON object in a variable and I'm performing calculations based on its properties. However, the current method of calculation seems lengthy, especially as the data grows larger. I'm searching for a more concise approach to per ...

Utilizing LocalStorage in an Ionic application to store data on a $scope

Having trouble saving the array named $scope.tasks to LocalStorage while building a To Do List app. Tried multiple times but can't figure it out. Here's the code, appreciate any help :^) index.html <!DOCTYPE html> <html> <head& ...

Angular Checkbox Single Select

I have a piece of HTML code with ng-repeat that includes checkboxes. <table> <tr ng-repeat="address in contactInfo.Addresses"> <td>{{address.DisplayType}}</td> <td>{{address.AddressLine1}}</td> <td>{ ...

Issue with getStaticProps in Next.js component not functioning as expected

I have a component that I imported and used on a page, but I'm encountering the error - TypeError: Cannot read property 'labels' of undefined. The issue seems to be with how I pass the data and options to ChartCard because they are underline ...

Can a webpage be sent to a particular Chromecast device without using the extension through programming?

My organization has strategically placed multiple Chromecasts across our facility, each dedicated to displaying a different webpage based on its location. Within my database, I maintain a record of the Chromecast names and their corresponding URLs. These d ...