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

测试提交按钮文本切换时出现TypeError: Cannot read properties of null (reading 'uid')

问题描述

我正在测试点击提交按钮后,按钮文本是否从“Upload Image”切换为“Busy...”。但点击按钮触发图片上传流程时,出现了相关错误。

测试代码如下:

it("text change within button on load", async () => {
    const user = userEvent.setup();
    const mockFn = jest.fn();
    render(<CreateProfilePhoto handleAddData={mockFn} handleSubmit={mockFn} />);
    const file = new File(["hello"], "hello.png", { type: "image/png" });
    const input = screen.getByTestId("upload-prompt");
    await user.upload(input, file);
    const uploadImage = screen.getByRole("button", { name: /upload image/i });
    await user.click(uploadImage);
    expect(
      screen.getByRole("button", { name: /busy.../i })
    ).toBeInTheDocument();
  });

错误信息:

90 |     await user.upload(input, file);
      91 |     const uploadImage = screen.getByRole("button", { name: /upload image/i });
    > 92 |     await user.click(uploadImage);
         |     ^
      93 |     expect(
      94 |       screen.getByRole("button", { name: /busy.../i })
      95 |     ).toBeInTheDocument();

以及:

Profile Photo Create › text change within button on load

    TypeError: Cannot read properties of null (reading 'uid')

      57 |       const imageRef = ref(
      58 |         storage,
    > 59 |         "profileImage/" + user.uid + "/profilePhoto"
         |                                ^
      60 |       );
      61 |    
      62 |       uploadBytes(imageRef, image)

我尝试通过props传入handleSubmit,但组件未接收该props,因此没有效果。请问该错误的原因是什么?如何在单元测试范围内解决这个问题?

补充:我实际要测试的是按钮文本切换逻辑:

{isLoading ? "Busy..." : "Upload Image"}

组件完整代码:

export default function CreateProfilePhoto({ handleAddData }) {
  const user = auth.currentUser;

  const [image, setImage] = useState();
  const [url, setUrl] = useState("");
  const [imageID, setImageID] = useState();
  const [isLoading, setIsLoading] = useState(false);

  const [originUrl, setOriginUrl] = useState();

  //To validate image size is less than 3MB
  const [largeImage, setLargeImage] = useState(false);

  //To validate image is there before upload
  const [imageMissing, setImageMissing] = useState();

  useEffect(() => {
    const work = handleAddData(imageID, url);
    return work;
  }, [imageID, url, handleAddData]);

  const handleImageChange = (e) => {
    console.log(e.target.files[0]);
    if (e.target.files[0]) {
      if (e.target.files[0].size > 3000000) {
        setLargeImage(true);
        return;
      } else {
        setLargeImage(false);
        setImage(e.target.files[0]);
      }
    }
    setImageID(e.target.id);
  };

  const handleSubmit = () => {
    if (image) {
      setIsLoading(true);
      setImageMissing(false);
      const imageRef = ref(
        storage,
        "profileImage/" + user.uid + "/profilePhoto"
      );
      uploadBytes(imageRef, image)
        .then(() => {
          getDownloadURL(imageRef)
            .then((url) => {
              setOriginUrl(url);
              setUrl(url.replace("https://firebasestorage.googleapis.com", ""));
            })
            .catch((error) => {
              console.log(error.message, "download URL catch");
              toast.error(error.message);
            });
          setImage(null);
          setIsLoading(false);
        })
        .catch((error) => {
          console.log(error.message, "upload bytes URL");
          toast.error(error.message);
        });
    } else {
      setImageMissing(true);
    }
    console.log(url);
  };

  return (
    <div className="m-5">
      <h3>Profile Picture</h3>
      {largeImage && (
        <p className="text-red-600 text-xs">Image must be 3MB or less</p>
      )}
      {imageMissing && (
        <p className="text-red-600 text-xs">
          Please attach image before upload
        </p>
      )}
      <img
        className="w-20 h-20 rounded-full ring-2 ring-orange-400 p-1 m-2"
        src={`appropriate_URL`}
        alt="Profile"
      />
      <input
        type="file"
        onChange={handleImageChange}
        id="profileImageURL"
        accept="image/png, image/jpeg"
        data-testid="upload-prompt"
      />
      <button
        type="button"
        className="border-2 p-2 hover:border-orange-400"
        onClick={handleSubmit}
      >
        {isLoading ? "Busy..." : "Upload Image"}
      </button>
    </div>
  );
}
错误原因

核心问题是单元测试环境中auth.currentUser为null。组件的handleSubmit函数直接访问user.uid,但测试时没有模拟Firebase Auth的当前用户状态,导致读取null的uid属性抛出TypeError,中断了后续流程,自然无法触发isLoading状态切换,也就看不到按钮文本变化。

另外你尝试传入handleSubmit props无效,是因为组件内部并没有接收这个props,而是直接使用了自身定义的handleSubmit函数。

单元测试解决方案

要在测试范围内解决问题,需要模拟Firebase Auth的currentUser,并mock掉Firebase Storage相关API(避免真实调用),确保流程能走到setIsLoading(true)这一步:

1. 模拟Firebase Auth的currentUser

在测试文件顶部添加mock代码,让auth.currentUser返回一个包含uid的模拟对象:

jest.mock('firebase/auth', () => ({
  ...jest.requireActual('firebase/auth'),
  getAuth: () => ({
    currentUser: { uid: 'test-user-123' }
  })
}));

2. Mock Firebase Storage相关API

避免真实调用上传逻辑,mockref、uploadBytes、getDownloadURL:

jest.mock('firebase/storage', () => ({
  ref: jest.fn(() => ({})),
  uploadBytes: jest.fn(() => Promise.resolve()),
  getDownloadURL: jest.fn(() => Promise.resolve('test-url'))
}));

3. 调整测试逻辑,等待状态更新

点击按钮后,状态更新是异步的,使用findByRole自动等待元素出现:

it("text change within button on load", async () => {
    const user = userEvent.setup();
    const mockFn = jest.fn();
    render(<CreateProfilePhoto handleAddData={mockFn} />);
    
    // 上传测试文件
    const file = new File(["hello"], "hello.png", { type: "image/png" });
    const input = screen.getByTestId("upload-prompt");
    await user.upload(input, file);
    
    // 点击上传按钮
    const uploadButton = screen.getByRole("button", { name: /upload image/i });
    await user.click(uploadButton);
    
    // 等待按钮文本变为"Busy..."
    expect(await screen.findByRole("button", { name: /busy.../i })).toBeInTheDocument();
  });

可选:重构组件(优化测试体验)

如果后续要更灵活地控制逻辑,可以将user通过props注入,这样测试时无需mock整个Firebase模块:

// 修改组件,接收user作为props
export default function CreateProfilePhoto({ handleAddData, user }) {
  // 替换原有的const user = auth.currentUser;
  // ... 其余代码不变
}

测试时直接传入模拟user:

render(<CreateProfilePhoto handleAddData={mockFn} user={{ uid: 'test-user-123' }} />);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 06:17:03