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

Utilizing v-data-iterator in Vuetify to display data fetched from an API in a table

After a long day of searching and experimenting, I've finally arrived here. Initially, I attempted to create a simple table with 2 columns using Row and iterate over the object to populate the left column with its keys. However, I hit a roadblock when ...

The expected React component's generic type was 0 arguments, however, it received 1 argument

type TCommonField = { label?: string, dataKey?: string, required?: boolean, loading?: boolean, placeholder?: string, getListOptionsPromissoryCallback?: unknown, listingPromissoryOptions?: unknown, renderOption?: unknown, getOptionLabelFor ...

I must align a bootstrap modal(dialog) based on the opener's position

Using a bootstrap modal for the settings dialog in an angularJS app, but facing an issue where it opens in the center of the page by default instead of relative to the opener. Looking for a solution to keep it positioned correctly when scrolling. Code: M ...

JQuery encountered a HTTP 404 error, signaling that the server was unable to locate anything that matched the requested URI

I am currently learning front-end development on my own and encountered an issue with implementing jQuery. You can find all the files for my site here: The problem I am facing is that there should be blog posts displayed between the header and navigation ...

Utilize the power of jQuery accordion to organize and display your table

Is there a way to integrate jQuery UI Accordion with an HTML table so that columns can be collapsible? I have tried various methods but haven't been successful. Currently, this is what I have implemented: $(".col1, .col2").addClass("hidden"); $(".s ...

The default image field value in Django Rest Framework triggers a Validation error

Our development team utilizes REST and includes a site management feature on our app. This feature retrieves information such as name, description, title, and icon from the API. Additionally, we have an admin interface on a separate front-end app that can ...

Navigating routes with regular expressions in Express

Recently, I've implemented the regex pattern /^\/(\d{5})$/ in my express route. However, an error has surfaced. The error message reads: SyntaxError: Invalid regular expression: /^\/^\/(?(?:([^\/]+?)){5})$\/?$/: Inval ...

Can a Vuetify datatable have a header that spans multiple lines?

Is it possible to create a table with a multiline header, similar to the one shown in this example? I came across this post, but the solution provided didn't work for me. I also checked the Vuetify documentation and Github issues, but couldn't f ...

Is it possible for an independent perl script to execute a function from a website's javascript?

Looking at this complex setup, I find myself in a situation where I must find a way to trigger the $.ajax function on a webpage using a separate Perl script. The scenario involves a webpage making $.ajax calls to a Perl file, which retrieves data and send ...

Retrieve a remote text file using vanilla JavaScript instead of relying on jQuery

Currently, I am aiming to retrieve the content of a text file using JavaScript for parsing purposes. Although I previously relied on jQuery and JSONP for this task, I now prefer to achieve it without any framework. Despite numerous attempts, none have bee ...

Creating a mandatory and meaningful text input in Angular 2 is essential for a

I am trying to ensure that a text material input in my app is mandatory, with a message like "Please enter issue description." However, I have noticed that users can bypass this by entering spaces or meaningless characters like "xxx." Is there an npm pac ...

Switch between individual highcharts by selecting or deselecting checkboxes

One of the challenges I am facing involves manipulating multiple scatter plots created with highcharts. I have a list of checkboxes, each labeled to correspond with legend identifiers in the highcharts. My goal is to create a dynamic functionality so tha ...

When clicking on the file input field in Angular.js, the image name mysteriously disappears

I am currently utilizing ng-file-upload to upload images with Angular.js. The issue I'm encountering is that when a user selects a file for the second time in the same field, the name of the previously chosen image doesn't display. Below is my c ...

Scrollable content with sticky positioning using CSS3 and JavaScript

I successfully implemented a sidebar using the position: sticky property and it is functioning perfectly. To identify colors in the following text, refer to the script below. When scrolling, the black area remains fixed while the green area sticks to its ...

Exploring date comparison in AngularJS

I've encountered an issue while using ng-show in a page that I'm currently designing: <td ng-show="week.EndDate > controller.currentDate"> The week object has a property called EndDate, and the value of currentDate is being set in my c ...

Sending the factory's response back to the controller in AngularJS

I operate a factory that uses an api call to request user data: angular.module('MyApp') .factory('UserApi', function($auth,Account){ return { getProfile: function() { Account.get ...

Can a for loop be implemented within a mongoose schema method?

Is there a way to modify this for loop so that it runs through the entire array instead of adding one by one? Any suggestions? EndorsedSkillSchema.methods = { async userEndorsedSkill(arr) { for (var i = 0; i < arr.length; i++) { const skil ...

Updating the selected value in TomSelect based on an ajax response

I am facing an issue setting a value on TomSelect using ajax response. Normally, I would use the following code on a general dropdown: $('#homebasepeg').val(data.hmb_id).change(); However, when I try to apply this on TomSelect, it doesn't s ...

Tips for invoking a function from a JavaScript file within an Angular component

This particular query remains unanswered and pertains to AngularJS. I am seeking a solution specifically for Angular, as none of the existing answers online seem to be effective in my case. Here is an outline of my code: Columns.js export class Columns { ...

Issue with box shadow appearing incorrectly as element content increases in size while the body has an image background

After applying a box shadow to the header div, I noticed that the box shadow doesn't display properly when the hidden elements within the header are revealed. <div id="header"> <div id="logo"> <a href="#"><img src="logo.png" ...