paulmillr / noble-hashes

Audited & minimal JS implementation of hash functions, MACs and KDFs.
https://paulmillr.com/noble
MIT License
545 stars 46 forks source link

Jest TextEncoder missing, Uint8Array failing instanceof checks workaround #48

Closed sublimator closed 1 year ago

sublimator commented 1 year ago

When using jsdom environment the tests sometimes fail with Uint8Array is not defined etc.

You can workaround this by setting a testEnvironment in your jest.config.js:

...
testEnvironment: `${__dirname}/commands/jest/customizedTestEnvironment.js`,
...

Impl a custom environment:

const { TestEnvironment } = require('jest-environment-jsdom')

/**
 * Jest creates a new VM context for each test and doesn't add in all 
 * the node globals. 
 * We rectify this here.
 */
class CustomizedTestEnvironment extends TestEnvironment {
  constructor(config, context) {
    super(config, context)
  }

  async setup() {
    await super.setup()

    // These seem to be needed for @noble/hashes
    this.global.TextEncoder = globalThis.TextEncoder
    this.global.Uint8Array = globalThis.Uint8Array
  }
}

module.exports = CustomizedTestEnvironment

It would be nice if didn't need to do this. May need to patch the jsdom environment?

In any case leaving this here in case it helps others.

paulmillr commented 1 year ago

It would be nice if didn't need to do this.

What's your suggested solution from the noble-hashes end?

sublimator commented 1 year ago

Not sure there is much you can do other than document it ?(this issue hopefully enough, even if closed). When I get a chance I will open a PR for the jsdom environment.

sublimator commented 1 year ago

I'll edit issue to remove fetch mentions when not on mobile. Opened when tired trying to be good citizen. Fetch is irrelevant

MTG2000 commented 1 year ago

Hey @sublimator So I also came across this problem in my project. My project uses create-react-app & jest, but when I tried to do what you basically did but in setupTests.ts file:

import "@testing-library/jest-dom";
import { TextEncoder, TextDecoder } from "util";

global.TextEncoder = TextEncoder;
global.Uint8Array = Uint8Array;

const encoder = new TextEncoder();
const data = encoder.encode("Hello World");
console.log(data instanceof Uint8Array); // always false

As you can see, instanceof always returns false...

Any idea why it's not working??

I tried globalThis.Uint8Array btw, same result.

sublimator commented 1 year ago

On mobile, will look tomorrow. For now, can say Jest creates VM contexts to run the tests. So where you set up the globals probably matters.

MTG2000 commented 1 year ago

On mobile, will look tomorrow. For now, can say Jest creates VM contexts to run the tests. So where you set up the globals probably matters.

In a create-react-app, I don't think you can supply a testEnvironment option. Any changes that you want to do there should supposedly be done in a setupTests file.

sublimator commented 1 year ago
global.TextEncoder = TextEncoder;
global.Uint8Array = Uint8Array;

I think that's essentially a noop ?

I don't think you can supply a testEnvironment option

Annoying! Been a while since I used CRA, but can you mess with jest cli args ?
https://jestjs.io/docs/cli#--envenvironment

MTG2000 commented 1 year ago

Yeah, for anyone who face this problem in the future, here is how I managed to get it working.

1- Add a env flag to your test script: "test": "react-scripts test --env=./src/utils/testing/jest-environment-jsdom.js", 2- Create a jest env file that has the following:

const { TextDecoder, TextEncoder } = require("util");

const JsdomEnvironment = require("jest-environment-jsdom");

export default class CustomJsdomEnvironment extends JsdomEnvironment {
  async setup() {
    await super.setup();

    if (!this.global.TextDecoder) {
      this.global.TextDecoder = TextDecoder;
    } else {
      throw new Error(`Unnecessary polyfill "TextDecoder"`);
    }

    if (!this.global.TextEncoder) {
      this.global.TextEncoder = TextEncoder;
      this.global.Uint8Array = Uint8Array;
    } else {
      throw new Error(`Unnecessary polyfill "TextEncoder"`);
    }
  }
}

Only overriding the TextEncoder/Decoder wasn't enough for me, I had to also add Uint8Array.

What's your suggested solution from the noble-hashes end?

@paulmillr One thing that noble-hashes can do to help developers avoid this problem is: by not using data instanceof Uint8Array. Instead, maybe use the isUint8Array that is provided by utils/types. I tried it, and it seems to be working fine. https://nodejs.org/api/util.html#utiltypesisuint8arrayvalue

paulmillr commented 1 year ago

@MTG2000 noble-hashes is not node.js library. It is cross-platform. There is no utils/types in browser.

Every solution wrt uint8array type check has its drawbacks. We have tried changing logic in the past.

sublimator commented 1 year ago

@MTG2000

 if (!this.global.TextDecoder) {
      this.global.TextDecoder = TextDecoder;
    } else {
      throw new Error(`Unnecessary polyfill "TextDecoder"`);
    }

    if (!this.global.TextEncoder) {
      this.global.TextEncoder = TextEncoder;
      this.global.Uint8Array = Uint8Array;
    } else {
      throw new Error(`Unnecessary polyfill "TextEncoder"`);
    }

Your logic looks strange here

MTG2000 commented 1 year ago

Your logic looks strange here

Why?? (And btw, this is not something I came up with, this is how someone else facing the same problem fixed it)

sublimator commented 1 year ago

Nevermind, I was looking while busy. On second glance it seems OK But you might want to create a whole new block (with specific error) for the Uint8Array?

1cherbear1 commented 1 year ago

Hi, I don't know if I am the issue or not. However, at one point in my Blockchain/crypto endeavors I was able to validate my account and it was referencing unit8 however, somehow all of my funds disappeared right before my very eyes in metamask while on a mobile device. Since that point nothing I do seems to yield profits. I apologize profusely if I have somehow caused this problem. Also, I don't know if anyone can or would be willing to assist me in the recovery of my account which I am sure is a lost cause now. I am trying to move forward in life in every way possible even with my repeated knock downs. I am a humble person so it takes a lot to ask for assistance but I don't know what to do anymore aside from admit defeat and begin anew. Thank you for hearing me out and I appreciate any advice.