39

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;
skyboyer
  • 22,209
  • 7
  • 57
  • 64
Naresh
  • 23,937
  • 33
  • 132
  • 204

5 Answers5

65

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:

const writeText = jest.fn()

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

describe("Clipboard", () => {
  describe("writeText", () => {
    beforeAll(() => {
      navigator.clipboard.writeText.mockResolvedValue(undefined)
      // or if needed
      // navigator.clipboard.writeText.mockRejectedValue(new Error()) 
      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: async () => {},
  },
});
Teneff
  • 30,564
  • 13
  • 72
  • 103
  • 2
    Thanks @Teneff. This was quite an education in jest and jsdom! – Naresh Jun 13 '20 at 14:24
  • 7
    I had to tweak like so `Object.assign(navigator, { clipboard: { writeText: jest.fn().mockImplementation(() => Promise.resolve()), }, });` – gawkface Jul 28 '21 at 19:02
  • 1
    @gawkface you may want to post a separate answer is you think it may help someone – Teneff Jul 28 '21 at 19:52
  • 1
    @Teneff I did not post a separate answer as its your answer that worked for me overall, with this little tweak that might be useful in some certain scenarios on top of ur answer but thanks for the reminder! – gawkface Jul 28 '21 at 23:40
10

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);
});
Chart96
  • 430
  • 4
  • 5
8

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');
})
  • While this may work, I believe this is bad practice because you are also testing the Clipboard API. This would be the same as testing an external library. – Marten Sep 22 '22 at 12:21
  • I was wrong. The docs state `To enable testing of workflows involving the clipboard, userEvent.setup() replaces window.navigator.clipboard with a stub.`. – Marten Sep 22 '22 at 18:26
6

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');
  });

});
David Dal Busco
  • 7,975
  • 15
  • 55
  • 96
5

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)
    );
  });
Parthipan Natkunam
  • 756
  • 1
  • 11
  • 16