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

5

49

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;
Attendant answered 12/6, 2020 at 20:22 Comment(0)
M
85

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 () => {},
  },
});
Mesh answered 13/6, 2020 at 6:23 Comment(6)
Thanks @Teneff. This was quite an education in jest and jsdom!Attendant
I had to tweak like so Object.assign(navigator, { clipboard: { writeText: jest.fn().mockImplementation(() => Promise.resolve()), }, });Sceptre
@Sceptre you may want to post a separate answer is you think it may help someoneMesh
@Mesh 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!Sceptre
I believe it's better to use Daniel Ngozika's solution which uses react-testing-library.Jehanna
One nice thing about this solution is that you don't need @ts-ignore, and your IDE won't yell at you for writing to a read-only property.Fortyish
H
24

Using react-testing-library:

To enable testing of workflows involving the clipboard, userEvent.setup() replaces window.navigator.clipboard with a stub.

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

    // Read from the stub clipboard
    const clipboardText = await navigator.clipboard.readText();

    expect(clipboardText).toBe('bla bla bla');
})
Haematoxylon answered 11/6, 2022 at 13:29 Comment(4)
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.Lisette
I was wrong. The docs state To enable testing of workflows involving the clipboard, userEvent.setup() replaces window.navigator.clipboard with a stub..Lisette
It does not work at all.Pyrrhic
@Pyrrhic Worked great for me. Are you certain your call to navigator.clipboard.writeText is happening?Jehanna
C
12

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);
});
Chromous answered 22/5, 2021 at 2:39 Comment(0)
L
6

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)
    );
  });
Louie answered 24/1, 2021 at 11:42 Comment(2)
Thanks. This solution is working perfect!Thusly
I have a cannot assign to clipboard as readonly error on this sadly.Cruiser
P
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');
  });

});
Proximate answered 18/1, 2022 at 6:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.