Asserting function calls in Storybook interaction tests

Asserting how many times a function has been called, and with what arguments, is a common task in unit tests. Despiite this, when I started writing Storybook interaction tests it wasn't immediately obvious how to actually do it.

After trawling the Storybook docs and still not finding an answer, I stumbled upon this solution through trial and error. It appears that the action argType is using a mock function behind the scenes — which makes sense when you think about it — and you can use Jest's toHaveBeenCalled matcher to assert on it.

import type { Meta, StoryObj } from '@storybook/react';
import {
    within,
    userEvent,
} from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { Button } from './button';

const meta: Meta<typeof Button> = {
    component: Button,
    title: 'Button',
    argTypes: {
        // Use the `action` argType to mock a function
        onClick: { action: 'clicked' },
    },
};
export default meta;

type Story = StoryObj<typeof Button>;

export const Click: Story = {
    play: async ({ canvasElement, args }) => {
        const canvas = within(canvasElement);

        // Jest will see the action as a normal mock
        expect(args.onClick).not.toHaveBeenCalled();

        await userEvent.click(canvas.getByRole('button'));

        expect(args.onClick).toHaveBeenCalledTimes(1);
    },
};