How to mock navigator.clipboard.writeText() in Jest?

Viewed 15457

I have tried the following 4 options after looking at Jest issues and SO answers, but I am either getting TypeScript errors or runtime errors. I would really like to get option 1 (spyOn) working.

// ------ option 1 -----
// Gives this runtime error: "Cannot spyOn on a primitive value; undefined given"
const writeText = jest.spyOn(navigator.clipboard, 'writeText');

// ------ option 2 -----
Object.defineProperty(navigator, 'clipboard', {
    writeText: jest.fn(),
});

// ------ option 3 -----
// This is from SO answer but gives a TypeScript error
window.__defineGetter__('navigator', function() {
    return {
        clipboard: {
            writeText: jest.fn(x => x)
        }
    }
})

// ------ option 4 -----
const mockClipboard = {
    writeText: jest.fn()
};
global.navigator.clipboard = mockClipboard;
5 Answers

Jest tests are running in JSdom environment and not all of the properties are defined, but so you should define the function before spying on it.

Here is an example:

Object.assign(navigator, {
  clipboard: {
    writeText: () => {},
  },
});

describe("Clipboard", () => {
  describe("writeText", () => {
    jest.spyOn(navigator.clipboard, "writeText");
    beforeAll(() => {
      yourImplementationThatWouldInvokeClipboardWriteText();
    });
    it("should call clipboard.writeText", () => {
      expect(navigator.clipboard.writeText).toHaveBeenCalledWith("zxc");
    });
  });
});

Edit: you can also use Object.defineProperty, but it accepts descriptors object as third parameter

Object.defineProperty(navigator, "clipboard", {
  value: {
    writeText: () => {},
  },
});

I expanded on the earlier solutions and also gave the mock clipboard functionality for readText so the content of the clipboard can be tested.

Here is the full content of my test.js file

import copyStringToClipboard from 'functions/copy-string-to-clipboard.js';

// ------- Mock -------
//Solution for mocking clipboard so it can be tested credit: <link to this post>
const originalClipboard = { ...global.navigator.clipboard };

beforeEach(() => {
    let clipboardData = '' //initalizing clipboard data so it can be used in testing
    const mockClipboard = {
        writeText: jest.fn(
            (data) => {clipboardData = data}
        ),
        readText: jest.fn(
            () => {return clipboardData}  
        ),
    };
    global.navigator.clipboard = mockClipboard;

});

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


it("copies a string to the clipboard", async () => {
    
    //arrange
    const string = 'test '
  
    //act
    copyStringToClipboard(string)

    //assert
    expect(navigator.clipboard.readText()).toBe(string)
    expect(navigator.clipboard.writeText).toBeCalledTimes(1);
    expect(navigator.clipboard.writeText).toHaveBeenCalledWith(string);
});

I ran into a similar situation and used the following method to mock the clipboard in the navigator object:

  const originalClipboard = { ...global.navigator.clipboard };
  const mockData = {
     "name": "Test Name",
     "otherKey": "otherValue"
  }

  beforeEach(() => {
    const mockClipboard = {
      writeText: jest.fn(),
    };
    global.navigator.clipboard = mockClipboard;

  });

  afterEach(() => {
    jest.resetAllMocks();
    global.navigator.clipboard = originalClipboard;
  });

  test("copies data to the clipboard", () => {
    copyData(); //my method in the source code which uses the clipboard
    expect(navigator.clipboard.writeText).toBeCalledTimes(1);
    expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
      JSON.stringify(mockData)
    );
  });

In my environment, testing-library svelte and jest jsdom, I did not manage to mock the global.navigator. The solution that worked was mocking the window.navigator within my test.

describe('my-test', () => {

  it("should copy to clipboard", () => {
    const { getByRole } = render(MyComponent);

    Object.assign(window.navigator, {
      clipboard: {
        writeText: jest.fn().mockImplementation(() => Promise.resolve()),
      },
    });

    const button = getByRole("button");
    fireEvent.click(button);

    expect(window.navigator.clipboard.writeText)
      .toHaveBeenCalledWith('the text that needs to be copied');
  });

});

In case you're using react-testing-library:

First, install @testing-library/user-event

Secondly, import user event like so: import userEvent from '@testing-library/user-event';

Then, for example:

test('copies all codes to clipboard when clicked', async () => {
    const user = userEvent.setup()
    render(<Success />);
    const copyButton = screen.getByTestId('test-copy-button');
    await user.click(copyButton);
    const clipboardText = await navigator.clipboard.readText();
    expect(clipboardText).toBe('bla bla bla');
})
Related