0

Goal I'm developing an React app (created with create-react-app). Currently I'm trying to test my component using react-testing library and Jest to ensure it shows the two elements below on visiting the dashboard of my app: a) loading spinner and then, once the data is fetched and stored in Redux b) fetched list of posts

Problem When I run my test, the elements above just never appear regardless of timeout value. I would like to get advice on my implementation to find out the culprit.

Error Message

● PostList › should render a post list data after loading

    Timed out in waitFor.

      157 |   it('should render a post list data after loading', async () => {
      158 |     renderWithProviders(<PostList />);
    > 159 |     const loadingEl = await waitFor(() => screen.findByTestId('main loader'));
          |                                    ^
      160 |     expect(loadingEl).toBeInTheDocument();
      161 |     const postEl = await waitFor(() => screen.findByText('test'));
      162 |     expect(postEl).toBeInTheDocument();

      at waitForWrapper (node_modules/@testing-library/dom/dist/wait-for.js:166:27)
      at Object.<anonymous> (src/container/post-list.test.tsx:159:36)

    Ignored nodes: comments, script, style
    <body>
      <div>
        <div
          class="h-full mx-auto flex flex-row"
        >
          <aside
            class="fixed w-full bottom-0 left-0 right-0 md:top-0 md:w-16 lg:w-64 bg-gray-800 text-white z-10"
          >
            <nav
              class="flex py-3 lg:py-10 px-3 flex-row md:flex-col justify-between w-full h-full"
            >
              <div>
                <div
                  class="h-[92px] text-center hidden md:block"
                >
                  <img
                    alt="growcally logo"
                    class="hidden lg:inline-block"
                    src="img/text-logo-white.png"
                  />
                </div>
                <ul
                  class="flex flex-row md:flex-col gap-8 mx-auto grow"
                >
                  <li>
                    <a
                      class="pl-0 lg:pl-3 py-2 rounded-3xl hover:bg-gray-300 ease-in duration-300 flex gap-4"
                      href="/dashboard"
                    >
                      <button
                        class="w-full flex gap-4"
                        tabindex="0"
                        type="button"
                      >
                        <svg
                          aria-hidden="true"
                          class="h-6 w-6 mx-auto lg:!mx-0"
                          fill="none"
                          stroke="currentColor"
                          stroke-width="1.5"
                          viewBox="0 0 24 24"
                          xmlns="http://www.w3.org/2000/svg"
                        >
                          <path
                            d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
                            stroke-linecap="round"
                            stroke-linejoin="round"
                          />
                        </svg>
                        <p
                          class="hidden lg:block"
                        >
                          Home
                        </p>
                      </button>
                    </a>
                  </li>
                  <li>
                    <button
                      class="py-2 lg:pl-3 rounded-3xl hover:bg-gray-300 ease-in duration-300 flex gap-4 w-full  flex gap-4"
                      tabindex="0"
                      type="button"
                    >
                      <svg
                        aria-hidden="true"
                        class="h-6 w-6 mx-auto lg:!mx-0"
                        fill="none"
                        stroke="currentColor"
                        stroke-width="1.5"
                        viewBox="0 0 24 24"
                        xmlns="http://www.w3.org/2000/svg"
                      >
                        <path
                          d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
                          stroke-linecap="round"
                          stroke-linejoin="round"
                        />
                      </svg>
                      <p
                        class="hidden lg:block"
                      >
                        Create
                      </p>
                    </button>
                  </li>
                  <li>
                    <a
                      class="pl-0 lg:pl-3 py-2 rounded-3xl hover:bg-gray-300 ease-in duration-300 flex gap-4"
                      href="/likes"
                    >
                      <button
                        class="w-full flex gap-4"
                        tabindex="0"
                        type="button"
                      >
                        <svg
                          class="h-6 w-6 mx-auto lg:!mx-0"
                          fill="white"
                          height="30"
                          viewBox="0 0 30 30"
                          width="26"
                          xmlns="http://www.w3.org/2000/svg"
                        >
                          <g
                            fill="white"
                            stroke="none"
                            stroke-width="1"
                            transform="translate(0.000000,32.000000) scale(0.100000,-0.100000)"
                          >
                            <path
                              d="M2 273 c2 -5 16 -33 30 -63 29 -62 51 -85 91 -95 23 -6 27 -12 27
    -41 0 -19 5 -34 10 -34 6 0 10 9 10 20 0 15 8 22 29 26 32 7 53 27 94 91 26
    41 26 42 7 48 -28 9 -74 -3 -104 -26 l-25 -20 -16 31 c-21 41 -71 70 -118 70
    -21 0 -37 -3 -35 -7z m114 -47 c36 -36 49 -96 20 -96 -33 0 -96 79 -96 121 0
    21 45 6 76 -25z m148 -59 c-31 -57 -94 -89 -94 -48 0 38 56 89 102 90 14 1 13
    -5 -8 -42z"
                            />
                          </g>
                        </svg>
                        <p
                          class="hidden lg:block"
                        >
                          Likes
                        </p>
                      </button>
                    </a>
                  </li>
                </ul>
              </div>
              <div>
                <button
                  class="py-2 lg:pl-3 rounded-3xl hover:bg-gray-300 ease-in duration-300 flex gap-4 w-full  flex gap-4"
                  tabindex="0"
                  type="button"
                >
                  <svg
                    aria-hidden="true"
                    class="h-6 w-6 mx-auto lg:!mx-0"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="1.5"
                    viewBox="0 0 24 24"
                    xmlns="http://www.w3.org/2000/svg"
                  >
                    <path
                      d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
                      stroke-linecap="round"
                      stroke-linejoin="round"
                    />
                  </svg>
                  <p
                    class="hidden lg:block"
                  >
                    More
                  </p>
                </button>
              </div>
            </nav>
          </aside>
          <div
            class="sm:ml-1 md:!ml-16 lg:!ml-64 pt-10 px-2 md:px-12 lg:px-36 w-full"
          >
            <div
              class="flex justify-center items-center h-screen"
            >
              <div
                class="text-center"
              >
                <h3
                  class="text-3xl font-bold mb-4"
                >
                  No posts yet
                </h3>
                <p
                  class="text-lg mb-6"
                >
                  Create a new post to get started.
                </p>
                <button
                  class="w-full bg-primary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75"
                  tabindex="0"
                  type="button"
                >
                  Create Post
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </body>

Test code

import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { renderWithProviders } from '../util/test';
import { PostItem, PostList } from './post-list';
import { screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { DashboardPage } from '../pages/dashboard-page';
import config from '../config';
import { User } from '../interfaces/user';
import { Post } from '../interfaces/post';
import { formatDistanceToNow } from 'date-fns';

const mockUser: User = {
  id: '123',
  status: 'active',
  givenName: 'Carrot',
  familyName: 'Legend',
  email: 'carrotlegend@example.com',
  bio: 'Lorem ipsum dolor sit amet.',
  profileImage: {
    id: '12345',
    fileName: 'profile.jpg',
    size: 1024,
    mimetype: 'image/jpeg',
    fileKey: 'abcde12345',
    fileUrl: 'https://example.com/profile.jpg'
  },
  sub: 'xyz123',
  posts: ['post1', 'post2'],
  likedPosts: ['post3', 'post4'],
  account: {
    id: '456',
    ownerId: '123'
  },
  comments: []
};

const mockPostItem1: Post = {
  id: '123',
  author: mockUser,
  caption: 'test',
  createdAt: '2023-06-19T15:30:00Z',
  files: [
    {
      id: '123',
      fileName: 'Vegetable garden',
      size: '60b170c0-7673-46c4-a71d-861845ae9097',
      mimetype: 'image/jpeg',
      alt: 'Vegetable garden',
      portraitFileKey:
        'b269db9743e39238bb1babce7e70fb0db134f45dc222efac56e324016b764xxx',
      squareFileKey:
        'adked8886bd2f14f824914e5e87387136471a4aaf630f1073bcdg7cba',
      portraitFileUrl: '/image/mock-post-image.png?'
    }
  ],
  totalLikes: 5,
  comments: [],
  totalComments: 7,
  updatedAt: '2023-06-19T15:30:00Z'
};

const mockPostItem2: Post = {
  id: '456',
  author: mockUser,
  caption: 'test2',
  createdAt: '2023-06-20T15:30:00Z',
  files: [
    {
      id: '234',
      fileName: 'Muddy day',
      size: '60b170c0-7673-46c4-a71d-861845ae9097',
      mimetype: 'image/jpeg',
      alt: 'Children in mud',
      portraitFileKey:
        'b269db9743e39238bb1babce7e70fb0db134f45dc222efac56e324016b764zzz',
      squareFileKey: 'adked8886bd2f14f824914e5e87387136471a4aaf630f1073bcdgopd',
      portraitFileUrl: '/image/mock-post-image.png?'
    }
  ],
  totalLikes: 5,
  comments: [
    {
      id: '456',
      content: 'You did a great job!',
      updatedAt: '2023-06-19T15:30:00Z',
      author: mockUser
    }
  ],
  totalComments: 7,
  updatedAt: '2023-06-20T15:30:00Z'
};

const baseURL = config.apiUrl;

const server = setupServer(
  rest.get(`${baseURL}/post/get-posts`, (req, res, ctx) => {
    return res(ctx.json([mockPostItem1, mockPostItem2]));
  })
);

beforeAll(() => server.listen());

afterEach(() => {
  server.resetHandlers();
});

afterAll(() => server.close());

describe('PostList', () => {
  it('should render a post list data after loading', async () => {
    renderWithProviders(<PostList />);
    const loadingEl = await waitFor(() => screen.findByTestId('main loader'));
    expect(loadingEl).toBeInTheDocument();
    const postEl = await waitFor(() => screen.findByText('test'));
    expect(postEl).toBeInTheDocument();
  }, 10000);
});

PostList component

import {
  ChatBubbleOvalLeftIcon,
  PaperAirplaneIcon
} from '@heroicons/react/24/outline';
import React from 'react';
import { Button } from '../components/button';
import { TextArea } from '../components/textarea';
import { Comment, MediaFile } from '../interfaces/post';
import { pluralize } from '../util/string';
import { useAppDispatch, useAppSelector } from '../hooks/hooks';
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import { useNavigate } from 'react-router-dom';
import { LOG_IN_PATH } from '../routes/routes';
import {
  createComment,
  getAllPosts,
  likePost,
  unlikePost
} from '../api/media.service';
import { setCurrentPost, updatePosts } from '../slices/posts-slice';
import { ModalType } from '../interfaces/modal-type';
import { resetModal, showModal } from '../slices/modals-slice';
import { User } from '../interfaces/user';
import { LeafFill } from '../icons/leaf-fill';
import { LeafNoFillBlack } from '../icons/leaf-no-fill-black';
import { MainLoader } from '../components/main-loader';
import { Thumbnail } from '../components/thumbnail';

export const PostList: React.FC = () => {
  const dispatch = useAppDispatch();
  const { posts } = useAppSelector((state) => state.posts);
  const navigate = useNavigate();
  const [isLoading, setIsLoading] = React.useState<boolean>(false);
  const { user } = useAppSelector((state) => state.auth);

  React.useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      try {
        setIsLoading(true);
        if (user) {
          const { data, alertMessage, isSuccess } = await getAllPosts(user.id);
          if (!isSuccess) {
            if (alertMessage === 'Unauthorised') {
              navigate(LOG_IN_PATH);
            }
            alert(alertMessage);
            setIsLoading(false);
            return;
          }
          if (isMounted) {
            dispatch(updatePosts(data));
          }
        } else {
          navigate(LOG_IN_PATH);
        }
        setIsLoading(false);
      } catch (error) {
        console.log(error);
        alert('We failed to get information of posts.');
      }
    };
    fetchData();
    return () => {
      isMounted = false;
    };
  }, []);

  if (isLoading) {
    return <MainLoader />;
  }
  if (!posts.length) {
    return <NoPostsMessage />;
  }
  const renderPostItems = posts?.map((i) => {
    return (
      <PostItem
        key={i.id}
        id={i.id}
        files={i.files}
        author={i.author}
        caption={i.caption}
        createdAt={i.createdAt}
        totalLikes={i.totalLikes}
        comments={i.comments}
      />
    );
  });

  return (
    <>
      <div data-testid="posts-list">{renderPostItems}</div>
    </>
  );
};

export const PostItem: React.FC<PostItemProps> = ({
  id,
  author,
  caption,
  createdAt,
  files,
  totalLikes,
  comments
}) => {

  return (
    <article className="mb-3 mx-auto pb-5 border-b border-solid border-[#262626] flex flex-col w-full gap-[6px] md:w-[430px]">
      <div className="flex gap-4 items-center">
        <Thumbnail
          src={ThumbnailSrc}
          onClick={handleClickAuthor}
          className="cursor-pointer hover:opacity-70"
          alt={`Profile of ${authorName}`}
        />
        <div className="flex">
          <p>{authorName}</p>
          {'・'}
          <p>{formattedDate} ago</p>
        </div>
      </div>

      {mediaUrl && (
        <img
          className="rounded w-[400px] md:w-[430px] h-[768px] cursor-pointer mx-auto"
          alt={primaryFile.alt || primaryFile.fileName}
          src={mediaUrl}
          onClick={handleOpenPostDetail}
        />
      )}
      <div className="flex flex-col gap-2">
        <div className="flex gap-2 items-center">
          {isAuthor ? (
            <></>
          ) : (
            <Button
              type="button"
              onClick={handleClickLike}
              isLoading={isLoading}
              className="cursor-pointer hover:opacity-70"
              aria-label={hasLiked ? 'liked' : 'like'}
            >
              {hasLiked ? <LeafFill /> : <LeafNoFillBlack />}
            </Button>
          )}
          <Button
            type="button"
            className="cursor-pointer hover:opacity-70"
            onClick={handleOpenPostDetail}
          >
            <ChatBubbleOvalLeftIcon className="h-6 w-6" />
          </Button>
        </div>
        {totalLikes ? (
          <p>
            {totalLikes} {pluralize(totalLikes, 'like', 'likes')}
          </p>
        ) : (
          <></>
        )}
        <p>{caption}</p>
        {comments?.length ? (
          <Button
            type="button"
            testId="view comments"
            onClick={handleOpenPostDetail}
          >
            View {comments.length} {pluralize(comments.length, 'comment')}
          </Button>
        ) : (
          <></>
        )}

        <form className="flex gap-4" onSubmit={handleSubmitComment}>
          <TextArea
            name="comment"
            placeholder="Add a comment"
            aria-label="Add a comment"
            className="max-h-10 p-1 text-sm"
            value={comment}
            onChange={(e) => setComment(e.target.value)}
          />
          <Button
            type="submit"
            testId="submit button"
            isLoading={isLoading}
            disabled={!comment}
          >
            <PaperAirplaneIcon className="h-6 w-6 text-black -rotate-45 hover:opacity-70" />
          </Button>
        </form>
      </div>
    </article>
  );
};

const NoPostsMessage: React.FC = () => {
  const dispatch = useAppDispatch();
  return (
    <div className="flex justify-center items-center h-screen">
      <div className="text-center">
        <h3 className="text-3xl font-bold mb-4">No posts yet</h3>
        <p className="text-lg mb-6">Create a new post to get started.</p>
        <Button
          type="button"
          isPrimary
          onClick={() =>
            dispatch(showModal({ modalType: ModalType.CreatePost }))
          }
        >
          Create Post
        </Button>
      </div>
    </div>
  );
};

MainLoader component

export const MainLoader = () => {
  return (
    <div className="w-full h-full flex items-center" data-testid="main loader">
      <PropagateLoader
        size={30}
        cssOverride={override}
        aria-label="Loading Spinner"
      />
    </div>
  );
};

What I tried:

  1. Added timeout to asynchronously retrieved elements and each test ==> still see the same error
  2. Stored env variable used in api call locally ==> I'm storing env variable in Doppler and don't use env file. I thought it could be the problem but it seems not.
  3. Change package versions ==> Changed the versions of axios and msw as interception error due to the compatibility has been reported. But I still see the same error.
  4. Investigate renderWithProvider ==> This is a custom render function that wraps passed components with provider. There seems no problem with the function meaning most probably this problem has to do with interception of api request.
  5. Check other API call ==> Other tests that involves API call are also having the same issue.

Side notes

  • App is running properly.
  • renderWithProvider() is a function that wraps component passed as an argument with redux provider. It's working properly in other tests.
mihomiho
  • 1
  • 2
  • I found solutions by myself. It turned out that User state needs to be updated before rendering the component and I omitted this in my test code. Also, I found the mock fetched data (= json object) that I prepared in msw handler had a slightly different structure than what's expected in frontend. After changing these two points, I could see my test pass without error! I'm explaining this in [my article](https://dev.to/mihomihouk/timed-out-error-in-reactredux-app-test-this-is-how-i-found-my-solution-1j9j) if someone wants to see the detail. – mihomiho Jun 23 '23 at 16:39

0 Answers0