In a new Ionic React application that uses the jose package to decode JWT’s, I ran into trouble with the Jest-driven testing environment.

I started to see the following error:

    ReferenceError: TextEncoder is not defined

      2 | import { IonContent, IonPage, IonGrid, IonCol, IonRow } from '@ionic/react';
      3 | import { useLazyQuery } from '@apollo/client';
    > 4 | import { decodeJwt } from 'jose';
        | ^

      at Object.<anonymous> (node_modules/jose/dist/node/cjs/lib/buffer_utils.js:5:23)
      at Object.<anonymous> (node_modules/jose/dist/node/cjs/runtime/base64url.js:5:27)
      at Object.<anonymous> (node_modules/jose/dist/node/cjs/jwe/flattened/decrypt.js:4:24)
      at Object.<anonymous> (node_modules/jose/dist/node/cjs/jwe/compact/decrypt.js:4:22)
      at Object.<anonymous> (node_modules/jose/dist/node/cjs/index.js:4:20)
      at Object.<anonymous> (src/pages/Account.tsx:4:1)
      at Object.<anonymous> (src/AuthenticatedApp.tsx:13:1)
      at Object.<anonymous> (src/App.tsx:4:1)
      at Object.<anonymous> (src/App.test.tsx:5:1)
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
      at runJest (node_modules/@jest/core/build/runJest.js:404:19)

I think that this ticket on GitHub for jsdom represents the core problem: Add support for TextEncoder and TextDecoder #2524.

What Didn’t Work

One of the first “rabbit holes” I went down by only partially reading Stack Overflow threads and not fully thinking through the problem involved attempting to install and configure jest-environment-jsdom. I think that jsdom is no longer included in recent Jest versions. However, I believe that happened in major version 28, and that the Ionic CLI installed version 27.5.1 in my project.

After installing jest-environment-jsdom at the same major version as jest, I attempted to include the following comment at the top of my test file:

/**
 * @jest-environment jsdom
 */

But I ended up just getting the following error:

    ReferenceError: global is not defined

      at Object.<anonymous> (node_modules/graceful-fs/graceful-fs.js:92:1)
      at Object.<anonymous> (node_modules/expect/build/toThrowMatchers.js:10:24)
      at Object.<anonymous> (node_modules/expect/build/index.js:35:48)
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
      at runJest (node_modules/@jest/core/build/runJest.js:404:19)

Based on a Stack Overflow thread and the jsdom ticket linked towards the top of this post, I attempted an iteration of my final solution at the bottom of this post that created a different problem:

import { TextEncoder, TextDecoder } from 'util';

global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;

This allowed my tests to pass, but created a TypeScript error when trying to run the application:

TS2322: Type 'typeof TextDecoder' is not assignable to type '{ new (label?: string | undefined, options?: TextDecoderOptions | undefined): TextDecoder; prototype: TextDecoder; }'.
  The types of 'prototype.decode' are incompatible between these types.
    Type '(input?: ArrayBufferView | ArrayBuffer | null | undefined, options?: { stream?: boolean | undefined; } | undefined) => string' is not assignable to type '{ (input?: BufferSource | undefined, options?: TextDecodeOptions | undefined): string; (input?: BufferSource | undefined, options?: TextDecodeOptions | undefined): string; }'.
      Types of parameters 'input' and 'input' are incompatible.
        Type 'BufferSource | undefined' is not assignable to type 'ArrayBufferView | ArrayBuffer | null | undefined'.
          Type 'ArrayBufferView' is not assignable to type 'ArrayBufferView | ArrayBuffer | null | undefined'.
            Type 'ArrayBufferView' is missing the following properties from type 'DataView': getFloat32, getFloat64, getInt8, getInt16, and 17 more.
     5 |
     6 | global.TextEncoder = TextEncoder;
  >  7 | global.TextDecoder = TextDecoder;

It probably could work, depending on what file you included it in and what environment conditionals you created.

What Worked on My Machine

I switched to the workaround offered at the very bottom of the GitHub ticket on the jsdom repository. In setupTests.ts:

import { TextEncoder, TextDecoder } from 'util';

Object.defineProperty(window, 'TextEncoder', {
  writable: true,
  value: TextEncoder
});

Object.defineProperty(window, 'TextDecoder', {
  writable: true,
  value: TextDecoder
});