How can I assert that a Storybook action was fired?

393 views Asked by At

I'm using Storybook to write interaction tests for my Next.js/React components. I'm using the @storybook/nextjs addon which is great because it automatically applies actions to the mocked router's methods.

One of the most significant pieces of Next.js is the router, which handles navigation and prefetching of page data. This framework will mock all of the necessary routing contexts in your Storybook, for both next/router and next/navigation.

I have a component that calls router.push() on change. I can visually confirm the nextRouter.push action is logged in the Storybook server's console.

nextRouter.push action is logged

Is there a way to programatically assert that the action was fired? This way the assertion could be part of the interaction test and I could run it in the CI. Currently I'm using Cypress end-to-end tests to express such test scenarios.

It feels like it should be possible, but I'm unable to achieve it.

export const Default: Story = {
    play: async (args) => {
        const canvas = within(args.canvasElement);
        fireEvent.change(canvas.getByTestId('my-component')}, {
            target: { value: 'AMSTERDAM' },
        });
        const routerPush = action('nextRouter.push');
        expect(routerPush).toHaveBeenCalledWith('/stays/amsterdam');
    },
};

However, this results in an error:

expect(received).toHaveBeenCalledWith(...expected)

Matcher error: received value must be a mock or spy function
2

There are 2 answers

2
Ninhache On

I Would've comment this if I was able to comment.. anyways :

Since you're using Cypress you could simply visit the storybook's url and then start your tests from there..

I don't know if that would be possible I don't have the setup to try it myself but I think it costs nothing to try !

cy.visit(`{STORYBOOK_URL}/...`);
cy.get(..) // get the action button glhf lol
cy.get('button[..]').click();
cy.get('.os-content').should('contain', 'the-my-component-event');

That's the only idea i've from now, with that, you're covering storybook !

I've based my naming over the styling of this storybook

4
James On

using the same addons as you are moking what next.js needs.
Making use of Storybook parameters, i made a mock for the push method:

export const Default = Template.bind({});
Default.parameters = {
  nextRouter: {
    pathname: '/profile/[id]',
    asPath: '/profile/lifeiscontent',
    query: {
      id: 'lifeiscontent',
    },
    push: (url, as) => {
      console.log('Navigating to ', url);
      action('router.push')(url, as);
    },
  },
};

So i take action function from a @storybook/addons to create the log

now in the interation test:

import { action } from '@storybook/addon-actions';

export const Default: Story = {
  play: async (args) => {
    const canvas = within(args.canvasElement);
    fireEvent.click(canvas.getByTestId('my-component')});
    
    const routerPush = action('router.push');
    expect(routerPush).toHaveBeenCalledWith('/expected-url');
  },
};

I'm using Jest toHaveBeenCalledWith function to assert that router.push should be called correctly.