iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0

我們會測試五種情況

a. 測試初始狀態

test("should use clipboard", () => {
  const { result } = renderHook(() => useCopyToClipboard());
  expect(result.current[0]).toBe(false);
  expect(typeof result.current[1]).toBe("function");
});

這個用例檢查初始狀態時,isCopied 應該是 false,並且返回的第二個值應該是一個函數。

b. 測試 execCommand

test("should copy to clipboard using execCommand for unsupported browsers", () => {...});

這個用例是在 navigator.clipboard 不可用的情況下進行的,檢查是否會使用 document.execCommand 方法作為備用,並且發出一個警告。

c. 測試 navigator.clipboard 的成功情況

test("should use navigator.clipboard copy to the clipboard and copy state become true", async () => {...});

這個用例檢查在成功使用 navigator.clipboard 來復制文本時,isCopied 應該被設置為 true

d. 測試狀態回退

test("should set isCopied back to false after 3 seconds", async () => {...});

這個用例檢查 isCopied 是否會在 3 秒後被重置為 false

e. 測試出錯情況

test("should set isCopied to false on error", async () => {...});

這個用例檢查當 navigator.clipboard.writeText 拋出錯誤時,isCopied 是否會被設置為 false,並且是否會有正確的錯誤信息輸出到控制台。

完整測試

import { act, renderHook } from "@testing-library/react";

import { useCopyToClipboard } from "../src";

describe("useClipboard()", () => {
  const originalClipboard = { ...global.navigator.clipboard };
  const mockData = "Test value";

  beforeEach(() => {
    const mockClipboard = {
      writeText: jest.fn(),
    };
    // @ts-ignore mock clipboard
    global.navigator.clipboard = mockClipboard;
  });

  afterEach(() => {
    jest.resetAllMocks();
    // @ts-ignore mock clipboard
    global.navigator.clipboard = originalClipboard;
  });

  test("should use clipboard", () => {
    const { result } = renderHook(() => useCopyToClipboard());

    expect(result.current[0]).toBe(false);
    expect(typeof result.current[1]).toBe("function");
  });

  test("should copy to clipboard using execCommand for unsupported browsers", () => {
    // Mocking execCommand
    document.execCommand = jest.fn();
    // Mocking navigator.clipboard to be undefined
    // @ts-ignore
    global.navigator.clipboard = undefined;

    // Mock console.warn
    const consoleWarnSpy = jest.spyOn(console, "warn");
    consoleWarnSpy.mockImplementation(() => {});

    const { result } = renderHook(() => useCopyToClipboard());
    act(() => {
      result.current[1](mockData);
    });

    expect(document.execCommand).toHaveBeenCalledWith("copy"); // 應該調用了 document.execCommand
    expect(result.current[0]).toBe(true); // isCopied 應該是 true

    // 確保警告被打印出來
    expect(consoleWarnSpy).toHaveBeenCalledWith(
      "Clipboard API not supported, falling back to execCommand"
    );

    // 還原 console.warn
    consoleWarnSpy.mockRestore();
  });

  
  test("should use navigator.clipboard copy to the clipboard and copy state become true", async () => {
    const { result } = renderHook(() => useCopyToClipboard());

    await act(async () => {
      await result.current[1](mockData);
    });

    expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
    expect(navigator.clipboard.writeText).toHaveBeenCalledWith(mockData);
    expect(result.current[0]).toBe(true);
  });

  test("should set isCopied back to false after 3 seconds", async () => {
    jest.useFakeTimers(); // 使用 Jest 的 fake timers
    const { result } = renderHook(() => useCopyToClipboard());

    // 初始化為 true
    await act(async () => {
      await result.current[1](mockData);
    });

    expect(result.current[0]).toBe(true); // 確保 state 被設置為 true

    act(() => {
      jest.advanceTimersByTime(3000); // 跳過 3 秒
    });

    expect(result.current[0]).toBe(false); // 檢查 state 是否被重置為 false

    jest.useRealTimers(); // 清理 timers
  });
  test("should set isCopied to false on error", async () => {
    // Mock console.error
    const consoleErrorSpy = jest.spyOn(console, "error");
    consoleErrorSpy.mockImplementation(() => {});

    const error = new Error("Failed to copy text");
    // Mocking writeText to throw an error
    global.navigator.clipboard.writeText = jest.fn(() => Promise.reject(error));

    const { result } = renderHook(() => useCopyToClipboard());
    await act(async () => {
      await result.current[1](mockData);
    });

    expect(result.current[0]).toBe(false); // isCopied 應該是 false
    expect(consoleErrorSpy).toHaveBeenCalledWith(
      "Failed to copy text: ",
      error
    ); // 確保正確的錯誤被輸出

    consoleErrorSpy.mockRestore(); // 還原 console.error
  });
});

beforeEachafterEach 中,進行一些初始化和清理工作。特別是對 navigator.clipboard 進行了 mock,因此在測試過程中,原本的 navigator.clipboard 不會被調用,就跟我們當初sessionStorage測試方法一樣。

特別說一下console.warn 與 console.error吧,之前好像都沒寫過

console.warnconsole.error 的 mocking(模擬),主要是為了檢查這些函數是否被正確地調用,並且是否用正確的參數被調用。這樣的模擬可以在代碼在出錯或需要警告時能夠正確反應。同時,這種模擬還防止了這些警告和錯誤信息實際被打印到測試運行時的控制台,使輸出保持清晰和乾淨。

以下是對這兩個函數模擬的具體步驟的解釋:

1. 使用 jest.spyOn 進行模擬

這個函數會“攔截”對特定對象方法的調用,可以檢查方法是否被調用,以及它被調用時使用的參數。

const consoleWarnSpy = jest.spyOn(console, "warn");
consoleWarnSpy.mockImplementation(() => {});

const consoleErrorSpy = jest.spyOn(console, "error");
consoleErrorSpy.mockImplementation(() => {});

2. 檢查模擬的方法

一旦方法被模擬,就可以檢查它是否被調用,以及它的調用方式:

expect(consoleWarnSpy).toHaveBeenCalledWith(
  "Clipboard API not supported, falling back to execCommand"
);

expect(consoleErrorSpy).toHaveBeenCalledWith(
  "Failed to copy text: ",
  error
);

3. 還原模擬

在測試結束後,模擬的方法應該被還原,以防止對其他測試或代碼的干擾。這是一個良好的實踐,可以避免出現意外的副作用。

consoleWarnSpy.mockRestore();
consoleErrorSpy.mockRestore();

通過這種方式,就能夠驗證 console.warnconsole.error 是否被正確調用,還能保持測試輸出的整潔,避免由於實際調用這些方法而產生的不必要的控制台消息。


上一篇
[Day 16] 使用複製文字功能吧useCopyToClipboard
下一篇
[Day 18] useThrottle 製作
系列文
React進階班,用typescript與jest製作自己的custom hooks庫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言