Search code examples
reactjstypescriptjestjsreact-testing-librarychakra-ui

How to test pasted content when using `useClipboard` hook in ChakraUI


I have a component that has a copy button that copies some data to the clipboard.

I would like to test that the correct data has been copied to the clipboard using jest & @testing-library/react

This is the component's implementation:

import React from "react";
import { Button, useClipboard } from "@chakra-ui/react";
import { formatData } from "@/lib/formatters";

export const MyComponent = ({ data }: Data) => {
  const { hasCopied, onCopy } = useClipboard(formatData(data));
  return <Button onClick={onCopy}>{hasCopied ? "Copied!" : "Copy Data"}</Button>;
};

And here is the unit test

it("copies data to clipboard", () => {
  render(<MyComponent data={data} />);
  screen.getByRole("button").click();
  fireEvent.click(screen.getByRole("button", { name: "Copy Data" }));
  // Expect that the clipboard data is set to the formatted data
  expect(navigator.clipboard.readText()).toEqual(formatData(data)); // Doesn't work!
});

However, when I run the unit test, I get an error TypeError: Cannot read properties of undefined (reading 'readText')

Is there a way to elegantly test the pasted content?

PS: useClipboard is using copy-to-clipboard package under the hood, which could be mocked, but that solution wouldn't be so elegant.


Solution

  • Jest is running the tests with jsdom and jsdom doesn't support navigator.clipborad, this is why clipboard is undefined and cannot read property writeText of undefined. However react testing library replaces window.navigator.clipboard with a stub when userEvent.setup() is used.

    If your implementation was using navigator.clipboard instead of copy-to-clipboard

    const MyComponent = ({ data }: Data) => {
      const [hasCopied, setHasCopied] = React.useState(false)
      const val = formatData(data)
    
      return (
        <Button onClick={() => {
          navigator.clipboard.writeText(val).then(() => {
            setHasCopied(true)
          });
        }}>{hasCopied ? "Copied!" : "Copy Data"}</Button>
      );
    };
    

    you would have been able to expect:

    test("should return text, reading the clipboard text", async () => {
    
      const user = userEvent.setup()
    
      render(<App />)
      const linkElement = screen.getByText(/copy data/i)
      user.click(linkElement)
    
      await waitFor(() => {
        expect(linkElement).toHaveTextContent('Copied!')
      })
    
      await expect(navigator.clipboard.readText()).resolves.toEqual("{\"hello\":\"world\"}")
    })
    

    but since execCommand is used instead of the navigator.clipboard API it would have to be mocked as well, as it's not supported by jsdom

    to mock it I would use the already prepared stub by react testing library like this

    
    let user: ReturnType<typeof userEvent.setup>;
    
    beforeAll(() => {
      user = userEvent.setup();
    
      Object.assign(document, {
        execCommand: (cmd: string) => {
          switch (cmd) {
            case "copy":
              user.copy();
              return true;
            case "paste":
              user.paste();
              return;
          }
        },
      });
    });
    

    and the above test should work once again