You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jest测试Twitter克隆应用推文编辑功能失败求助

问题:Jest测试推文编辑功能失败,user.keyboard输入未生效

我用TDD模式开发Twitter克隆应用,当前测试推文编辑功能:用户点击已发布推文,会显示输入框编辑内容,点击推文按钮完成修改。该功能在Storybook手动测试正常,但Jest测试无法通过,推测是时序问题,试过act和waitFor均无效。

测试失败时,Jest打印的DOM显示'My First Tweet'仍在页面中,点击推文后输入框已出现,但调用user.keyboard时输入未生效,输入框值未更新为编辑后的内容。


组件渲染代码

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

推文编辑测试代码

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"));
    });
  });

目标组件代码

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}
            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;

测试错误信息

● 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

补充测试输出

<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=""
/>

点击事件已触发,但user.keyboard未将内容传入输入框,且已按建议添加输入框聚焦操作。


解决方案

1. 修复输入框value绑定错误

组件中输入框的value错误绑定了tweet状态,编辑模式下tweet被设为undefined,导致输入框显示空值,且用户输入的内容无法在输入框中体现。修改输入框的value绑定:

<input
  type="text"
  placeholder={placeholder}
  value={input} // 从tweet改为input状态
  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}`}
/>

2. 编辑模式初始化输入框内容

点击推文进入编辑模式时,需要将当前推文内容赋值给input状态,确保输入框默认显示原推文内容,用户可以直接编辑:

const handleTwxxtClick = () => {
  setInput(tweet); // 新增:将原推文内容赋值给input
  setPlaceholder(tweet);
  setTweet(undefined);
};

3. 优化测试代码

  • 去掉不必要的act包裹,userEvent方法本身已在act中执行
  • 编辑模式下重新获取输入框元素,避免引用旧DOM节点
  • 输入前先清空原有内容,确保新输入完全替换原内容

修改后的测试代码:

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

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

    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);
    
    // 重新获取编辑状态的输入框
    const editInput = screen.getByPlaceholderText("My First Tweet");
    await user.click(editInput);
    
    // 清空原有内容并输入新内容
    await user.keyboard("{selectall}{backspace}My edited Tweet");
    
    await user.click(screen.getByRole("button", { name: "Tweet" }));
    
    // 验证推文已更新
    await waitFor(() => {
      expect(screen.getByText("My edited Tweet")).toBeInTheDocument();
      expect(screen.queryByText("My First Tweet")).not.toBeInTheDocument();
    });
  });
});

4. 优化提交逻辑(可选)

当前handleTweet会在输入为空时仍更新tweet,可以添加判断避免空内容提交:

const handleTweet = () => {
  if (!input?.trim()) {
    setPlaceholder("Enter a twxxt");
    setPlaceholderColor("placeholder:text-red-500");
    return;
  }
  setTweet(input);
  // 提交后清空输入框,恢复默认占位符
  setInput(undefined);
  setPlaceholder("What's happening?");
  setPlaceholderColor("placeholder:text-slate-400");
};

内容的提问来源于stack exchange,提问作者nland

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 06:55:24