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
  });
});
在 beforeEach 和 afterEach 中,進行一些初始化和清理工作。特別是對 navigator.clipboard 進行了 mock,因此在測試過程中,原本的 navigator.clipboard 不會被調用,就跟我們當初sessionStorage測試方法一樣。
console.warn 和 console.error 的 mocking(模擬),主要是為了檢查這些函數是否被正確地調用,並且是否用正確的參數被調用。這樣的模擬可以在代碼在出錯或需要警告時能夠正確反應。同時,這種模擬還防止了這些警告和錯誤信息實際被打印到測試運行時的控制台,使輸出保持清晰和乾淨。
以下是對這兩個函數模擬的具體步驟的解釋:
jest.spyOn 進行模擬這個函數會“攔截”對特定對象方法的調用,可以檢查方法是否被調用,以及它被調用時使用的參數。
const consoleWarnSpy = jest.spyOn(console, "warn");
consoleWarnSpy.mockImplementation(() => {});
const consoleErrorSpy = jest.spyOn(console, "error");
consoleErrorSpy.mockImplementation(() => {});
一旦方法被模擬,就可以檢查它是否被調用,以及它的調用方式:
expect(consoleWarnSpy).toHaveBeenCalledWith(
  "Clipboard API not supported, falling back to execCommand"
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
  "Failed to copy text: ",
  error
);
在測試結束後,模擬的方法應該被還原,以防止對其他測試或代碼的干擾。這是一個良好的實踐,可以避免出現意外的副作用。
consoleWarnSpy.mockRestore();
consoleErrorSpy.mockRestore();
通過這種方式,就能夠驗證 console.warn 和 console.error 是否被正確調用,還能保持測試輸出的整潔,避免由於實際調用這些方法而產生的不必要的控制台消息。