Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monitor React state changes from Storybook side to improve test suite stability #30211

Open
dpizzolongo opened this issue Jan 7, 2025 · 0 comments

Comments

@dpizzolongo
Copy link

Discussed in #30210

Originally posted by dpizzolongo January 7, 2025

Summary

Is it possible to monitor React states from Storybook side for stability purposes? The goal would be to wait for the component to be ready (after state or prop changes) before making test assertions. I know that the @storybook/test package provides the waitFor function to evaluate test assertions until a timeout expires, but I would like to have a more sophisticated and stable solution.

I was able to implement this successfully with React class components, but I would like to be able to do the same with React functional components. This solution uses spyOn which spies on componentDidUpdate lifecycle function to return { fetchIsComplete: true, componentIsReady: true } before asserting the final state of the component.

TestComponent.tsx

import React, { Component } from 'react';

interface TestComponentState {
  data: string;
  fetchIsComplete: null | boolean;
}

/**
 * Sample component that fetches data and displays it, using React
 * class-based syntax and lifecycle methods.
 */
class TestComponent extends Component<Readonly<{}>, TestComponentState> {
  constructor(props) {
    super(props);
    this.state = {
      data: 'Hello',
      fetchIsComplete: null
    };
    this.fetchData = this.fetchData.bind(this);
  }

  // Checks the current state of the component after an update was performed,
  // and returns true if it is in a ready state.
  componentDidUpdate(
    prevProps: Readonly<{}>,
    prevState: TestComponentState,
    snapshot?: any
  ): Object {
    return this.state.fetchIsComplete
      ? { fetchIsComplete: this.state.fetchIsComplete, componentIsReady: true }
      : { fetchIsComplete: this.state.fetchIsComplete, componentIsReady: false };
  }

  async fetchData() {
    this.setState({ data: 'Fetching data...', fetchIsComplete: false });
    // simulate fetch, then change state
    await new Promise(res => setTimeout(res, 5000));
    this.setState({ data: 'Sample data', fetchIsComplete: true });
  }

  render(): JSX.Element {
    return (
      <div>
        <h1 style={{ color: 'white' }} data-auto="data">
          {this.state.data}
        </h1>
        <button data-auto="fetch-data-btn" onClick={this.fetchData}>
          Fetch data
        </button>
      </div>
    );
  }
}

export default TestComponent;

TestComponent.stories.tsx

import type { Meta, StoryObj } from '@storybook/react';
import { expect, spyOn, userEvent, waitFor, within } from '@storybook/test';
import React from 'react';
import TestComponent from './TestComponent';

const meta: Meta<typeof TestComponent> = {
  component: TestComponent
};

export default meta;

type Story = StoryObj<typeof meta>;

export const FetchDataExample: Story = {
  render: () => {
    return <TestComponent />;
  },
  play: async ({ canvasElement }) => {
    const dataElement = await within(canvasElement).findByTestId('data');
    const fetchDataElement = await within(canvasElement).findByTestId('fetch-data-btn');
    const spyComponentUpdate = spyOn(TestComponent.prototype, 'componentDidUpdate');

    // initial state
    expect(dataElement.textContent).toBe('Hello');

    // user interaction
    await userEvent.click(fetchDataElement);

    // wait for the componentDidUpdate to be called and component to be ready for assertions
    await waitFor(
      () => {
        expect(spyComponentUpdate).toBeCalled();
        expect(spyComponentUpdate).toReturnWith({ fetchIsComplete: true, componentIsReady: true });
      },
      { timeout: 10000, interval: 500 } // check every 500 ms (times out after 10000 ms)
    );

    // verify new state
    expect(dataElement.textContent).toBe('Sample data');
  }
};

Additional information

No response

Create a reproduction

No response

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant