skip to Main Content

I’m building a twitter clone app and I’m utilizing TDD. I’ve gotten to the point where I’m trying to test a user being able to edit a tweet. The idea is, if a user clicks on the current tweet, it will display the input again so that you can edit the current tweet as you like. You then click the tweet button again, and it sets the tweet.

I’ve tested manually in Storybook, and it works as I expect it to, however, I’m unable to get my jest test to pass. Jest prints the DOM at test failure, and it does still show that ‘My First Tweet’ is in the DOM. My guess is that this is a timng issue, but I’m not sure how to fix it. I’ve tried act & waitFor but neither of those have worked. What am I doing wrong?

Rendering my component

  beforeEach(() => {
    renderResult = render(
      <Tweet profilePic={imageUrl} username="ndland" title="Tweet Title" />,
    );
  });

Testing editing a tweet

describe("editing a tweet", () => {
    const user = userEvent.setup();
    let input: Element;

    beforeEach(async () => {
      input = screen.getByPlaceholderText("What's happening?");
      const button = screen.getByRole("button", { name: "Tweet" });

      await act(async () => {
        await user.click(input);
        await user.keyboard("My First Tweet");

        await user.click(button);
      });
    });

    it("should allow a user to edit their tweet", async () => {
      const twxxt = screen.getByText("My First Tweet");
      expect(twxxt).toBeInTheDocument();
      expect(screen.queryByText("My edited Tweet")).not.toBeInTheDocument();

      await user.click(twxxt);
      expect(input).toHaveValue("My First Tweet");

      await user.keyboard("My edited Tweet");
      await user.click(screen.getByRole("button", { name: "Tweet" }));
      await waitFor(() => expect(input).toHaveValue("My edited Tweet"));
    });
  });

Component in question

const Tweet: React.FC<TweetProps> = ({ profilePic, username, title }) => {
  const [input, setInput] = React.useState<string | undefined>(undefined);
  const [tweet, setTweet] = React.useState<string | undefined>(undefined);
  const [placeholder, setPlaceholder] = React.useState<string | undefined>(
    "What's happening?",
  );
  const [placeholderColor, setPlaceholderColor] = React.useState<string>(
    "placeholder:text-slate-400",
  );
  const [likes, setLikes] = React.useState<number>(0);
  const [hasLiked, setHasLiked] = React.useState<boolean>(false);

  const handleTweet = () => {
    if (!input) {
      setPlaceholder("Enter a twxxt");
      setPlaceholderColor("placeholder:text-red-500");
    }
    setTweet(input);
  };

  const handleLike = () => {
    if (hasLiked) {
      setLikes(likes - 1);
      setHasLiked(false);
      return;
    }
    if (input && !hasLiked) {
      setLikes(likes + 1);
      setHasLiked(true);
    }
  };

  const handleTwxxtClick = () => {
    setPlaceholder(tweet);
    setTweet(undefined);
  };

  return (
    <div className="dark:bg-slate-800 mx-auto m-4 max-w-md overflow-hidden rounded-xl bg-white shadow-md md:max-w-2xl p-4 flex flex-row space-x-8">
      <div className="relative w-16 h-16 flex-shrink-0">
        <div>
          <Image
            className="rounded-full h-full w-full object-cover absolute"
            src={profilePic}
            alt="Profile Picture"
            width={100} // note: see https://nextjs.org/docs/app/building-your-application/optimizing/images#remote-images for more info
            height={100}
          />
        </div>
      </div>
      <div className="flex-grow">
        <div
          aria-label="username"
          className="text-indigo-500 font-semibold md:text-lg py-2"
        >
          @{username}
        </div>
        <div aria-label="title" className="dark:text-white font-semibold">
          {title}
        </div>
        {tweet ? (
          <>
            <div
              onClick={handleTwxxtClick}
              className="dark:text-white py-4 hover:cursor-pointer"
            >
              {tweet}
            </div>
            <div className="flex justify-normal space-x-2 items-center">
              {likes === 0 ? (
                <FontAwesomeIcon
                  aria-label="like"
                  onClick={handleLike}
                  className="text-white"
                  size="xl"
                  icon={outlineThumb}
                />
              ) : (
                <FontAwesomeIcon
                  aria-label="like"
                  onClick={handleLike}
                  className="text-white"
                  size="xl"
                  icon={solidThumb}
                />
              )}

              {likes !== 0 && (
                <div aria-label="likes" className="text-gray-400">
                  Likes: {likes}
                </div>
              )}
            </div>
          </>
        ) : (
          <>
            <input
              type="text"
              placeholder={placeholder}
              value={tweet}
              onChange={(e) => setInput(e.target.value)}
              className={`w-full my-2 p-2 rounded-md dark:bg-slate-800 dark:text-white dark:border-2 dark:border-indigo-800 ${placeholderColor}`}
            />
            <div className="flex justify-end items-center">
              <button
                onClick={handleTweet}
                className="bg-indigo-500 text-white rounded-full px-4 py-2"
              >
                Tweet
              </button>
            </div>
          </>
        )}
      </div>
    </div>
  );
};

export default Tweet;

edit forgot to add the test failure.

● Tweet › editing a tweet › should allow a user to edit their tweet

expect(element).toHaveValue(My edited Tweet)

Expected the element to have value:
  My edited Tweet
Received:
  My First Tweet

edit 2: I see the following output when running my tests:

          <input
            aria-label="tweet input"
            class="w-full my-2 p-2 rounded-md dark:bg-slate-800 dark:text-white dark:border-2 dark:border-indigo-800 placeholder:text-slate-400"
            placeholder="My First Tweet"
            type="text"
            value=""
          />

Which tells me that it’s hitting the handler, but when I call user.keyboard it’s not sending those keys to the input. I have also added the line @Willow suggested, so input should now have the focus.

2

Answers


  1. Chosen as BEST ANSWER

    Ok, it seems that the issue was, I was referencing the wrong object for my assertions. The input object I was referencing, was still holding the original value 'My First Tweet'. Below is the modified test, which is passing as expected:

      describe("editing a tweet", () => {
        const MY_FIRST_TWEET = "My First Tweet";
        const MY_EDITED_TWEET = "My Edited Tweet";
        const user = userEvent.setup();
    
        beforeEach(async () => {
          await user.click(tweetInput());
    
          await user.keyboard(MY_FIRST_TWEET);
    
          await user.click(tweetButton());
        });
    
        it("should allow a user to edit their tweet", async () => {
          const twxxt = screen.getByText(MY_FIRST_TWEET);
          expect(twxxt).toBeInTheDocument();
    
          await user.click(twxxt);
    
          await user.click(tweetInput());
    
          expect(tweetButton()).toBeInTheDocument();
    
          await user.keyboard(MY_EDITED_TWEET);
    
          await user.click(tweetButton());
    
          expect(screen.getByText(MY_EDITED_TWEET)).toHaveTextContent(
            MY_EDITED_TWEET,
          );
        });
      });
    });
    
    const tweetInput = (): HTMLElement => {
      return screen.getByLabelText("tweet input");
    };
    
    const tweetButton = (): HTMLElement => {
      return screen.getByRole("button", { name: /tweet/i });
    };
    

    In this example, I've just pulled the screen.getBy... into a helper function, so I can reuse the code similarly, but now, when I need to update the object, I can do so easily. A rather simple solution to a simple problem, just an oversight by me. Hope this helps.


  2. As alluded to in my comment, try this:

        it("should allow a user to edit their tweet", async () => {
          const twxxt = screen.getByText("My First Tweet");
          expect(twxxt).toBeInTheDocument();
          expect(screen.queryByText("My edited Tweet")).not.toBeInTheDocument();
    
          await user.click(twxxt);
          expect(input).toHaveValue("My First Tweet");
    
          await user.click(input); // just added this line
          await user.keyboard("My edited Tweet");
          await user.click(screen.getByRole("button", { name: "Tweet" }));
          await waitFor(() => expect(input).toHaveValue("My edited Tweet"));
        });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search