menu

Questions & Answers

How to automate testing function with click handler that have async await inside the function using karma-jasmine?

So i'm trying to testing on my button that run the function asynchronously. this is my button logic looks like.

    // Function below will run when user click the button
    this._pageModule.favoriteButtonCallback = async () => {
      try {
        // I want to expect run after this await below is done
        await this._favoriteRestaurants.PutRestaurant(this._restaurant);
        console.log(
          'console log in button',
          await this._favoriteRestaurants.GetAllRestaurant(),
        );
        this._renderButton();
        return Promise.resolve(
          `Success add ${this._restaurant.name} to favorite!`,
        );
      } catch (err) {
        this._renderButton();
        return Promise.reject(
          new Error(
            `Failed add ${this._restaurant.name} to favorite! Error: ${err}`,
          ).message,
        );
      }
    };

and this is my test

fit('should be able to add the restaurant to favorite', async () => {
    expect((await RestaurantIdb.GetAllRestaurant()).length).toEqual(0);

    // spyOn(RestaurantIdb, 'PutRestaurant');

    document.body.innerHTML = `<detail-module></detail-module>
    <modal-element></modal-element>`;

    const pageModule = document.querySelector('detail-module');

    await FavoriteButtonInitiator.init({
      pageModule,
      restaurant,
      favoriteRestaurants: RestaurantIdb,
    });

    pageModule.restaurantDetail = restaurant;

    await pageModule.updateComplete;

    const favoriteButton = pageModule.shadowRoot
      .querySelector('[aria-label="favorite this restaurant"]')
      .shadowRoot.querySelector('button');

    // 1. Simulate user click the button
    favoriteButton.dispatchEvent(new Event('click'));

    // expect(RestaurantIdb.PutRestaurant).toHaveBeenCalled();

    const restaurants = await RestaurantIdb.GetAllRestaurant();
    console.log('console log from test', restaurants);
    expect(restaurants).toEqual([restaurant]);
  });

i'm using lit-element, simply it similar with react, i have custom element <define-module> with button inside. then i give the required properties to it, then it will render.

This is my test log Test log

as you can see the console log from the test ran before the console log that i put in the button. and it is empty array.

what i want is when click event dispatched. the next line in the test wait until the asynchronous function in the button done, how do i make it possible?

What have i done: i have tried to console log them. i have tried to using done in jasmine, but it doesn't work since i using async/await in the test. I have tried use spyOn, but i don't really understand how to spy indexedDb

UPDATE

So i have found what caused problem, here i have simplified my code.

/* eslint-disable */
import { openDB } from 'idb';
import { CONFIG } from '../src/scripts/globals';

const { DATABASE_NAME, DATABASE_VERSION, OBJECT_STORE_NAME } = CONFIG;

const dbPromise = openDB(DATABASE_NAME, DATABASE_VERSION, {
  upgrade(database) {
    database.createObjectStore(OBJECT_STORE_NAME, { keyPath: 'id' });
  },
});

const RestaurantIdb = {
  async GetRestaurant(id) {
    return (await dbPromise).get(OBJECT_STORE_NAME, id);
  },
  async GetAllRestaurant() {
    return (await dbPromise).getAll(OBJECT_STORE_NAME);
  },
  async PutRestaurant(restaurant) {
    if (await this.GetRestaurant(restaurant.id)) {
      return Promise.reject(
        new Error('This restauant is already in your favorite!').message,
      );
    }
    return (await dbPromise).put(OBJECT_STORE_NAME, restaurant);
  },
  async DeleteRestaurant(id) {
    if (await this.GetRestaurant(id)) {
      return (await dbPromise).delete(OBJECT_STORE_NAME, id);
    }
    return Promise.reject(
      new Error('This restauant is not in favorite!').message,
    );
  },
};

describe('Testing RestaurantIdb', () => {
  const removeAllRestaurant = async () => {
    const restaurants = await RestaurantIdb.GetAllRestaurant();

    for (const { id } of restaurants) {
      await RestaurantIdb.DeleteRestaurant(id);
    }
  };

  beforeEach(async () => {
    await removeAllRestaurant();
  });

  afterEach(async () => {
    await removeAllRestaurant();
  });

  it('should add restaurant', async () => {
    document.body.innerHTML = `<button></button>`;

    const button = document.querySelector('button');
    button.addEventListener('click', async () => {
      await RestaurantIdb.PutRestaurant({ id: 1 });
    });

    button.dispatchEvent(new Event('click'));

    setTimeout(async () => {
      const restaurants = await RestaurantIdb.GetAllRestaurant();
      console.log('console log in test', restaurants);

      expect(restaurants).toEqual([{ id: 1 }]);
    }, 0);
  });
});

And this is the result Test Result

I assume that IndexedDb takes times to put my restaurant data. and i still can't figure out how to fix it.

Answers(1) :

If you were using Angular, you would have access to fixture.whenStable(), and fakeAsync and tick() which wait until promises are resolved before carrying forward with the test.

In this scenario, I would try wrapping what you have in the test in a setTimeout

fit('should be able to add the restaurant to favorite', async () => {
    expect((await RestaurantIdb.GetAllRestaurant()).length).toEqual(0);

    // spyOn(RestaurantIdb, 'PutRestaurant');

    document.body.innerHTML = `<detail-module></detail-module>
    <modal-element></modal-element>`;

    const pageModule = document.querySelector('detail-module');

    await FavoriteButtonInitiator.init({
      pageModule,
      restaurant,
      favoriteRestaurants: RestaurantIdb,
    });

    pageModule.restaurantDetail = restaurant;

    await pageModule.updateComplete;

    const favoriteButton = pageModule.shadowRoot
      .querySelector('[aria-label="favorite this restaurant"]')
      .shadowRoot.querySelector('button');

    // 1. Simulate user click the button
    favoriteButton.dispatchEvent(new Event('click'));

    // expect(RestaurantIdb.PutRestaurant).toHaveBeenCalled();
    setTimeout(() => {
     const restaurants = await RestaurantIdb.GetAllRestaurant();
     console.log('console log from test', restaurants);
     expect(restaurants).toEqual([restaurant]);
    }, 0);
  });

The things in the setTimeout should hopefully happen after the asynchronous task of the button click since promises are microtasks and setTimeout is a macrotask and microtasks have higher priority than macrotasks.

Comments:
2023-01-17 23:10:06
Thanks for your answer! My project doesn't using any framework. And i have tried using setTimeout but it seems not working. But i have found what is causing the problem, i have updated my question