Search code examples
reactjsjestjsmobxmobx-state-tree

Jest & Mobx: how to test execution of disposer?


Hope anyone already overcome this issue. codesandbox

I want to check, that after changing appliedIds, loadCampaign will be executed. As we say in disposer:

addDisposer( self, reaction(() => self.appliedIds, this.loadCampaign), );

So how can I do it? How to wait in jest tests for reaction execution? I see here that I can use const reaction = jest.spyOn(mobx, 'reaction'); but I want to check the result of execution function in disposer.

src/store/filters/campaignFilterStore.ts

import { reaction } from 'mobx';
import { Instance, addDisposer, flow, types } from 'mobx-state-tree';

export const CampaignFull = types.model('CampaignFull', {
  id: types.identifierNumber,
  name: types.string,
  userId: types.number,
});

export type ICampaignFull = Instance<typeof CampaignFull>;

interface ICampaignData {
  id: number;
  name: string;
  userId: number;
}

export const CampaignFilterStore = types
  .model('CampaignFilterStore', {
    appliedIds: types.array(types.number),
    campaignIds: types.array(types.number),
    campaign: types.maybe(CampaignFull),
  })
  .actions((self) => ({
    afterCreate(): void {
      console.log('afterCreate');

      addDisposer(
        self,
        reaction(() => self.appliedIds, this.loadCampaign),
      );
    },
    loadCampaign: flow(function* loadCampaign(): Generator<
      Promise<ICampaignData>,
      void,
      ICampaignData
    > {
      const campaignId = self.appliedIds[0];

      console.log('loadCampaign', campaignId);

      if (!campaignId) {
        return;
      }

      try {
        const data = yield new Promise((res) => {
          setTimeout(() => {
            res({
              id: 1,
              name: 'name',
              userId: 12,
            });
          }, 500);
        });

        self.campaign = CampaignFull.create({
          id: data.id,
          name: data.name,
          userId: data.userId,
        });
      } catch (e) {
        console.error(e);
      }
    }),
    apply(): void {
      self.appliedIds.replace([...self.campaignIds]);
    },
    toggleCampaign(campaign: { id: number; name: string }): void {
      self.campaignIds.push(campaign.id);
    },
  }));

src/store/filters/campaignFilterStore.test.ts

import { isObservable } from 'mobx';
import { CampaignFilterStore } from 'store/filters/campaignFilterStore';

const CAMPAIGN_1 = {
  id: 1,
  name: 'name',
  userId: 12,
};

describe('CampaignFilterStore', () => {
  test('should load campaign by disposer - DOESNT WORK', async () => {
    const campaigns = CampaignFilterStore.create();

    campaigns.toggleCampaign(CAMPAIGN_1);
    expect(campaigns.campaign).toBeUndefined();
    try {
      expect(isObservable(campaigns.appliedIds)).toBe(true);
      campaigns.apply();
      expect(campaigns.appliedIds).toEqual([CAMPAIGN_1.id]);
    } finally {
      // campaign SHOULD be loaded
      expect(campaigns.campaign).not.toBeUndefined();
    }
  });

  test('should load campaign', async () => {
    const campaigns = CampaignFilterStore.create({
      appliedIds: [CAMPAIGN_1.id],
    });

    expect(campaigns.campaign).toBeUndefined();
    await campaigns.loadCampaign();

    expect(campaigns.campaign).not.toBeUndefined();
    expect(campaigns.campaign?.name).toEqual(CAMPAIGN_1.name);
  });
});


Solution

  • self.loadCampaign action needs to be called explicitly inside reaction effect callback

      addDisposer(
        self,
        reaction(
          () => getSnapshot(self.appliedIds),
          () => self.loadCampaign()
        )
      );
    

    Then in your test to can create a spy on loadCampaign action and make sure you change appliedIds

    const campaigns = CampaignFilterStore.create();
    const loadCampaignSpy = jest.spyOn(campaigns, 'loadCampaign');
    campaigns.setAppliedIds([1,2,3]); // appliedIds should be changed in order to run the effect defined in reaction()
    expect(loadCampaignSpy).toHaveBeenCalledTimes(1);
    

    Unlike autorun, the side effect won't run once when initialized, but only after the data expression returns a new value for the first time.

    https://mobx.js.org/reactions.html#reaction