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