Check to see if the event handler is triggered and the promises are executed in sequence (syncronously)

I have a Vue button click handler that, depending on the arguments it receives, can do the following:

  • execute request A only
  • execute request B only
  • execute request A and then request B sequentially (request B is only called if request A completes successfully. It's important to note that the implementation cannot use Promise.all()).

My issue is figuring out how to write unit tests in Jest for the "execute A and then B sequentially" behavior.

Code Implementation

Below is the event handler that runs after clicking a button:

const loadA = this.$store.dispatch['loadA']; //these are FUNCTIONS THAT RETURN PROMISES
const loadB = this.$store.dispatch['loadB'];
async makeRequests(shouldMakeRequestA, shouldMakeRequestB) {
  const requests = [];
  if(shouldMakeRequestA) requests.push(loadA);
  if(shouldMakeRequestB) requests.push(loadB);

  for(const request of requests) {
    await request(); //waits for request to complete before proceeding to the next one
  }
}

Testing Approach

The ideal test case should:

  • FAIL❌ when:

    • the implementation calls both requests concurrently like:

      • () => { Promise.all([loadA(), loadB()]) }
      • () => { loadA(); loadB() }
  • PASS✔️ when:

    • the implementation executes loadA, waits for its promise to resolve, then executes loadB, e.g.:
      • () => {await loadA(); await loadB();}

Below is my attempt at writing the test case, but I acknowledge it might be vulnerable to race conditions and not easily understood by colleagues.

//component.spec.js
import MyCustomButton from '@/components/MyCustomButton.vue'
import TheComponentWeAreTesting from '@/components/TheComponentWeAreTesting'
describe('foo', () => {
const resolveAfterOneSecond = () => new Promise(resolve => setTimeout(resolve, 1000));

let wrapper;
const loadA = jest.fn(resolveAfterOneSecond);
const loadB = jest.fn(resolveAfterOneSecond);

beforeEach(() => {
  wrapper = shallowMount(TheComponentWeAreTesting, store: new Vuex.Store({actions: {loadA, loadB}});
})

it('executes A and B sequentially', async () => {
  wrapper.find(MyCustomButton).vm.$emit('click');
  
  await wrapper.vm.$nextTick();

  await new Promise(resolve => setTimeout(resolve, 500));
  const callCount = loadA.mock.calls.length + loadB.mock.calls.length;
  expect(callCount).toBe(1); 
}
}

Is there an alternative approach to testing this behavior? I am aware of methods like jest.advanceTimersByTime, but it affects all timers, not just the current one.

Answer №1

I suggest replacing

await new Promise(r => setTimeout(r, 500));
with a custom handler like this:

async makeRequests(shouldMakeRequestA, shouldMakeRequestB) {
  const requests = [];
  if(shouldMakeRequestA) requests.push(loadA);
  if(shouldMakeRequestB) requests.push(loadB);

  for(const request in requests) {
    await request(); //waits for request to return before calling for the second one
  }
}

This function returns a promise.

this.handler = (async() => {
        const requests = [];
        if (shouldMakeRequestA) requests.push(loadA);
        if (shouldMakeRequestB) requests.push(loadB);

        for (const request of requests) {
          await request();
        }
      })()

**Here's an example snippet **

Vue.config.devtools = false;
Vue.config.productionTip = false;


const {
  shallowMount
} = VueTestUtils;

const {
  core: {
    beforeEach,
    describe,
    it,
    expect,
    run,
    jest
  },
} = window.jestLite;

const resolveAfterOneSecond = () => new Promise(r => setTimeout(r, 1000));
let loadA = resolveAfterOneSecond;
let loadB = resolveAfterOneSecond;

const combineAndSendRequests = async function*(shouldMakeRequestA, shouldMakeRequestB) {

  if (shouldMakeRequestA) {
    await loadA();
    yield 1;
  }

  if (shouldMakeRequestB) {
    await loadB();
    yield 2;
  }
}

const TestComponent = Vue.component('test-component', {

  template: `<button @click="sendRequests()">Send</button>`,
  data() {
    return {
      handler: null
    }
  },
  methods: {
    sendRequests() {
      const shouldMakeRequestA = true;
      const shouldMakeRequestB = true;

      this.handler = (async() => {
        for await (let promise of combineAndSendRequests(shouldMakeRequestA, shouldMakeRequestB)) {

        }

      })();
    }
  }
})

var app = new Vue({
  el: '#app'
})

document.querySelector("#tests").addEventListener("click", (event) => {
  const element = event.target;
  element.dataset.running = true;
  element.textContent = "Running..."

  loadA = jest.fn(resolveAfterOneSecond);
  loadB = jest.fn(resolveAfterOneSecond);

  describe("combineAndSendRequests", () => {

    it('runs A and B one after the other', async() => {

      const shouldMakeRequestA = true;
      const shouldMakeRequestB = true;
      const iterator = combineAndSendRequests(shouldMakeRequestA, shouldMakeRequestB);

      await iterator.next();
      let loadACallsCount = loadA.mock.calls.length;
      let loadBCallsCount = loadB.mock.calls.length;
      expect(loadACallsCount).toBe(1);
      expect(loadBCallsCount).toBe(0);

      await iterator.next();
      loadBCallsCount = loadB.mock.calls.length;
      expect(loadBCallsCount).toBe(1);

      const callsCount = loadA.mock.calls.length + loadB.mock.calls.length;
      expect(callsCount).toBe(2);
    });

  });

  describe("test-component", () => {
    let wrapper = null;

    beforeEach(() => {
      wrapper = shallowMount(TestComponent);

    })

    it('runs request after click', async() => {
      wrapper.find("button").trigger('click');

      await wrapper.vm.$nextTick();
      const handler = wrapper.vm.$data.handler;
      expect(handler).not.toBe(null);

    });
  });

  run().then(result => {

    console.log(result);
    delete element.dataset.running;

    if (!result.some(pr => pr.status.includes("fail"))) {
      element.textContent = "Passed!"
      element.dataset.pass = true;
    } else {
      element.textContent = "Fail!"
      element.dataset.fail = true;
    }
  })


})
#tests {
  margin-top: 1rem;
  padding: 0.5rem;
  border: 1px solid black;
  cursor: pointer;
}

button[data-pass] {
  background: green;
  color: white;
}

button[data-running] {
  background: orange;
  color: white;
}

button[data-fail] {
  background: red;
  color: white;
}
<script src="https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="0b616e787f2667627f6e4b3a253b253b266a677b636a253f">[email protected]</a>/dist/core.js"></script>
<script src="https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d8aeadbd98eaf6eef6e9e9">[email protected]</a>/dist/vue.js"></script>
<script src="https://www.unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="186e6d7d356c7d756874796c7d357b77756871747d6a582a362e362929">[email protected]</a>/browser.js"></script>
<script src="https://unpkg.com/@vue/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="56223325227b23223f3a25166778667865">[email protected]</a>/dist/vue-test-utils.umd.js"></script>


<div id="app">
  <test-component></test-component>
</div>

<button id="tests">Run Tests</button>

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

Updating a secondary state array is possible by modifying a JavaScript array with setState()

In my React application, there is a grid where names can be selected. When a name is chosen, the app retrieves corresponding data from a database and displays rows of information related to that particular name. Each row is represented as an object stored ...

Is there a way to set the starting position of the overflow scroll to the middle?

Is there a way to have the overflow-x scrollbar scroll position start in the middle rather than on the left side? Currently, it always begins at the left side. Here is what it looks like now: https://i.stack.imgur.com/NN5Ty.png If anyone knows of a soluti ...

Leveraging geoPosition.js in conjunction with colobox

How can I create a colorbox link that prompts the user for permission to access their location, and if granted, displays a map with directions from their current location? I've managed to get it partially working, but there's an issue when the us ...

What is the best way to add Vue dependency using CDN?

For my project which is built using Kendo, Vue, .Net, Angular and jQuery, I need to incorporate https://www.npmjs.com/package/vue2-daterange-picker. <script src="https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-c ...

Obtain the shared value between two data entries within a Vue array

In my Vue.js data, I have two records with the same question id. I want to select both record options and only one of the questions. [ { "id":7, "question_id":102, "option":"true", "is_corr ...

Experimenting with a hover rule in the CSS of a React button

Currently, I am working on testing the visibility of an outline on a button at the unit test level. The technologies that I have been using for this task are React, React Testing Library, Jest, and Styled Components. React React Testing Library Jest Style ...

Tips for modifying the background color of an individual page in Ionic 3 and above

https://i.stack.imgur.com/t2mDw.pngI am just starting with Ionic and I'm attempting to modify the CSS of a single page by changing the background color to something different, like green, for example. I know that I can make global changes, but in this ...

ERROR: An issue occurred while attempting to resolve key-value pairs

Within my function, I am attempting to return a set of key-value pairs upon promise completion, but encountering difficulties. const getCartSummary = async(order) => { return new Promise(async(request, resolve) => { try { cons ...

It requires two clicks on the button for the Vue and Pinia store to update

I've been experimenting with Vue and trying to understand it better. When I click the button in LoginForm.vue for the first time, both token and user_data are null. It's only on the second click that they finally update. How can I ensure these va ...

"Modifying a variable within an object while iterating through a loop

I am attempting to update a variable within an object while looping through the elements. Specifically, I am targeting all parent elements with the class "parent" to use with the ScrollMagic plugin. Here is my code: var childElement = $('.child' ...

Error in ReactJS: Trying to access property 'items' of an undefined object

I've been diving into ReactJS, but I've hit a roadblock with this perplexing error. My goal is to update the parent's items array state from its child component. To achieve this, I attempted to pass the addItem function as a prop to the chi ...

I want to know how to move data (variables) between different HTML pages. I am currently implementing this using HTML and the Django framework

I am currently working on a code where I am fetching elements from a database and displaying them using a loop. When the user clicks on the buy button, I need to pass the specific product ID to another page. How can I retrieve the product ID and successful ...

Providing Node-server with Vue.js Server Side Rendering

I've been attempting to deploy the Hackernews 2.0 demo on my Digital Ocean droplet but have hit a roadblock. npm run start starts the server on port :8080. npm run build is used for production builds. The specific build tasks can be found below: ...

Ways to modify the final sum exclusively for a single table

I am currently struggling to figure out how to calculate only the grand total of the first table using just one jQuery/JavaScript script. The code I am referencing is from: Below is the code snippet: <!DOCTYPE html> <html xmlns="http://www.w3 ...

Adjust the size of the mat-expansion indicator to your desired height and width

Trying to modify the width and height of the mat indicator has been a bit challenging. Despite following suggestions from other similar questions, such as adjusting the border width and padding, I am still unable to see the changes reflect in my CSS file ...

What is the best way to conceal elements that do not have any subsequent elements with a specific class?

Here is the HTML code I have, and I am looking to use jQuery to hide all lsHeader elements that do not have any subsequent elements with the class 'contact'. <div id="B" class="lsHeader">B</div> <div id="contact_1" class="contac ...

ReactJS Chatkit has not been initialized

I made some progress on a tutorial for creating an Instant Messenger application using React and Chatkit. The tutorial can be found in the link below: https://www.youtube.com/watch?v=6vcIW0CO07k However, I hit a roadblock around the 19-minute mark. In t ...

An issue has occurred: The necessary parameter (Slug) was not included as a string in the getStaticPaths function for the /post/[Slug] route

Hello, I've recently embarked on a tutorial journey to create the ultimate Modern Blog App using React, GraphQL, NextJS, and Tailwind CSS. However, I encountered an error that's giving me some trouble specifically when trying to access a post. He ...

What is the process for retrieving an excel file from the backend to the frontend?

I am currently facing an issue where I want to initiate a browser download of an excel file that is generated in my backend. However, I am struggling to figure out how to pass this type of response. app.get('/all', function (req, res) { db.q ...

What is the best way to manage returning to the original page that was loaded when utilizing the History API?

I'm in a bit of a pickle here. I've been using History.js with the History API and everything was going smoothly until I encountered an issue. Let's start with a simple page setup like this: <div ="header"> Header </div> <d ...