jest-clipboard for Clipboard API Testing Success

Last updated Apr 26, 2024 Published Oct 15, 2022

The content here is under the Attribution 4.0 International (CC BY 4.0) license

TLDR

The blog post discusses the challenges of testing applications that interact with the clipboard API and presents a solution using the jest-clipboard plugin. It begins by explaining the limitations of the synchronous execCommand method and introduces the asynchronous Clipboard API. The post outlines the steps for handling text and other media types with jest-clipboard, providing code examples for setting up and testing clipboard interactions. It emphasizes the importance of improving user experience through asynchronous clipboard processing and highlights the benefits of using jest-clipboard for testing clipboard-related functionalities in Jest.

Introduction

Recently I have been playing around ReactJs + jest trying to sharpen my skills on outside-in TDD and the idea behind the setup is to give an easy way to start testing. The application though, was a simple one, it just formatted JSON string, but it has some interesting things, for example, accessing the clipboard to read and write.

Such a thing seems to be trivial when testing, so some kind of test-double is required to be in place, and it turns out that it is indeed, easy to deal with that, but the testing code becomes a bit verbose. In StackOverflow Naresh asked the question “How to mock navigator.clipboard.writeText() in Jest?” and the responses there worked but required a verbose setup.

In this post, I am going to try to depict the problems I had around mocking the clipboard API, and what solution I put in place. Even though I try as much as possible to refer to extra resources along the journey, it is recommended familiar with the javascript ecosystem.

A bit of context

Copying/pasting things in and out from the browser has been done through a specific command document.execCommand(). However, as depicted by Jason Miller and Thomas Steiner, it has an important drawback: execCommand is synchronous. Such a behavior leads to a poor experience from the user’s perspective leading to hanging the page until it is completed. For that reason, the async Clipboard API was built. The new API allows developers to create applications that do not block while operating on the clipboard for reading or writing.

The introduction of the new API besides improving the user experience of handling the clipboard without hanging the page introduced a new security measurement to protect user privacy even more. With the async clipboard API, the user has the power to allow or block the interaction with the clipboard - which was previously not possible with the execCommand. The process to access this new clipboard API is resumed in the following flow chart:

Having the general flow in mind gives an idea of the reasons behind checking if the clipboard object is available or not in the code.

The solution

Developing an application that interacts with the new clipboard API is not a problem, there are different examples on the internet and the blog post by web.dev is one of those that I recommend reading. The challenge comes when we want to test the interactions that should happen when implementing the code that will read/write in the clipboard.

Given this scenario the plugin jest-clipboard was created, it enables developers to set up the clipboard state and focus on the expected behavior instead of details of mocking the clipboard API.

Handling text

Let’s start with a scenario in which we need to read a text that the user has in the transfer area. In that case, the first thing to use jest-clipboard is to set the plugin in the beforeEach and afterEach hook functions. This will enable jest-clipboard to set correctly the scenario for each test in the suite:

import { setUpClipboard, tearDownClipboard } from 'jest-clipboard';

describe('My test', () => {
  beforeEach(() => {
    setUpClipboard();
  });

  afterEach(() => {
    tearDownClipboard();
  });
});

The next step is to write the test and start asserting with the clipboard. For example, to set a text in the clipboard transfer the test would be something like the following:

import { setUpClipboard, tearDownClipboard, writeTextToClipboard, readTextFromClipboard } from 'jest-clipboard';

describe('My test', () => {
  beforeEach(() => {
    setUpClipboard();
  });

  afterEach(() => {
    tearDownClipboard();
  });

   it('should use text from clipboard', async () => {
      await writeTextToClipboard('I want this to be in the transfer area');

      // in here the production code needs to be used

      expect(await readTextFromClipboard()).toEqual('I want this to be in the transfer area');
  });
});

Another example that we might face is sending some information we have to the clipboard, for that, the clipboard API has the writeText functionality, let’s have a look at how it would be:

import { setUpClipboard, tearDownClipboard, writeTextToClipboard, readTextFromClipboard } from 'jest-clipboard';

describe('My test', () => {
  beforeEach(() => {
    setUpClipboard();
  });

  afterEach(() => {
    tearDownClipboard();
  });

   it('should use text from clipboard', async () => {
      // no need to write something to the clipboard first, the setup/teardown make sure to give
      // a clean state for every test.

      // in here the production code needs to be used, given an example that
      // the production code writes 'I want this to be in the transfer area'
      // this text would be there.

      // Thus, the assertion would be the same as the previous example.

      expect(await readTextFromClipboard()).toEqual('I want this to be in the transfer area');
  });
});

The clipboard was designed to support multiple types of media and for that end, it was split into the text that we just saw and the other things (that could be an image for example). Handling other media types follows the same process, let’s see how it is in the next section.

Handling other things

The setup for the clipboard remains the same, no change is required, the bit of piece that requires change is the method we invoke to write and read from the clipboard. Let’s take out same example that we used to write/read text from the clipboard and transform it into the one that uses the read/write API from the clipboard.

The first thing to note is that now, we are starting to handle ClipboardItems and Blobs [1] instead of raw strings:

describe('My test', () => {
  beforeEach(() => {
    setUpClipboard();
  });

  afterEach(() => {
    tearDownClipboard();
  });

  it('write to clipboard (write)', async () => {
    // this is now a blob and not a text anymore
    // here we need to specify the type of media
    const blob = new Blob(['my text'], { type: 'text/plain' });

    const clipboardItem: ClipboardItem = {
      presentationStyle: 'inline',
      types: ['plain/text'],
      getType(type: string): Promise<Blob> {
        return new Promise((resolve) => {
          resolve(blob)
        });
      }
    };

    const clipboardItems: ClipboardItems = [clipboardItem]
    await writeItemsToClipboard(clipboardItems);

    const items = await readFromClipboard();

    const type1 = await items[0].getType(imagePng);
    expect(await type1.text()).toBe('my text');
  });
});

Let’s pretend that we want to handle an image that the user will copy from the browser and paste into our application (like Google Docs or Notion does). There are a few steps before doing that, let’s enumerate them:

  1. We need an image to use as an example
  2. We will convert the image into a blob
  3. Assert the file contents or other information

Reading another media type from the clipboard would be like the following:

describe('My test', () => {
  beforeEach(() => {
    setUpClipboard();
  });

  afterEach(() => {
    tearDownClipboard();
  });

  it('write to clipboard (write)', async () => {
    // this is the first items, we are creating an image from a base64 string
    // and we are also creating a blob from that
    const imagePng = 'image/png';
    const base64Image ='iVBORw0KGgoAAAANSUhEUgAAAEYAAABACAIAAAAoFZbOAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAaElEQVRoge3PQQ3AIADAQMAQD4J/azOxZOlyp6Cd+9zxL+vrgPdZKrBUYKnAUoGlAksFlgosFVgqsFRgqcBSgaUCSwWWCiwVWCqwVGCpwFKBpQJLBZYKLBVYKrBUYKnAUoGlAksFlgoeg2ABFxfCv1QAAAAASUVORK5CYII='
    const buffer = Buffer.from(base64Image, 'base64');
    const blob = new Blob([buffer]);

    // refers to https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/web_tests/external/wpt/clipboard-apis/async-promise-write-blobs-read-blobs.https.html
    const clipboardItem: ClipboardItem = {
      presentationStyle: 'inline',
      types: [imagePng],
      getType(type: string): Promise<Blob> {
        return new Promise((resolve) => {
          resolve(blob)
        });
      }
    };

    // finally we can grab what the clipboard has
    const clipboardItems: ClipboardItems = [clipboardItem]
    await writeItemsToClipboard(clipboardItems);

    const items = await readFromClipboard();

    const type1 = await items[0].getType(imagePng);

    // the assertion here is with the size as the text would give a bibary encoded
    expect(type1.size).toBe(182);
  });
});

The clipboard API handles different media types based on the blob interface and the clipboard items, it gives developers the power to decide which thing to take from the user when pasting. For example, developers can trigger resizing images when an image is detected in the clipboard, likewise with video content.

Takeaways

The main idea behind the clipboard API is to enable developers to improve the user experience through asynchronous clipboard processing. The clipboard API provides a read, write and permissions interface to be used. First, the user needs to allow its usage and then developers can fine-tune the clipboard handling between raw text and other media types such as images. With this powerful API, the testing aspect was left behind without any support to help developers test their applications when writing test-first.

As jest is one of the most popular test runners in the javascript ecosystem, jest-clipboard provides an AI for developers to focus on the application behavior instead of mocking the clipboard API. jest-clipboard follows the conversions used in the clipboard API and adds a readable API that shows intent. For example, writeText will write text to the clipboard and readText will read text from the clipboard. It also provides utilities to set up and clean the test state to avoid behaviors that make testing hard.

References

  1. [1]en.javascript.info, “Blob,” 2023 [Online]. Available at: https://javascript.info/blob. [Accessed: 22-May-2022]

Changelog

  • Apr 26, 2024 - Added LinkedIn post
  • Apr 05, 2024 - Fixes in the text