jest: [Bug]: `structuredClone` fails on `toStrictEqual`

Version

29.4.0

Steps to reproduce

  1. Clone my repository at https://github.com/JamieMagee/jest-structuredclone-strictequal/blob/master/index.spec.js
  2. npm install
  3. npm test

Expected behavior

I expect to see objects cloned with structuredClone to pass toStrictEqual

Actual behavior

The values do not pass toStrictEqual (but do pass toEqual).

jest › toStrictEqual › structured clone

    expect(received).toStrictEqual(expected) // deep equality

    Expected: {"value": "test"}
    Received: serializes to the same string

       6 |     describe('toStrictEqual', () => {
       7 |         it('structured clone', () => {
    >  8 |             expect(structuredClone(value)).toStrictEqual(value);
         |                                            ^
       9 |         });
      10 |         it('JSON clone', () => {
      11 |             expect(JSON.parse(JSON.stringify(value))).toStrictEqual(value);

      at Object.toStrictEqual (index.spec.js:8:44)

However JSON.parse(JSON.stringify()) does pass toStrictEqual

Additional context

The test output can be seen in this GitHub Actions run.

Environment

System:
  OS: Linux 6.2 NixOS 23.05 (Stoat) 23.05 (Stoat)
  CPU: (32) x64 AMD Ryzen 9 7950X 16-Core Processor
Binaries:
  Node: 19.7.0 - /run/current-system/sw/bin/node
  Yarn: 1.22.19 - /run/current-system/sw/bin/yarn
  npm: 9.5.0 - /run/current-system/sw/bin/npm

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 7
  • Comments: 44 (24 by maintainers)

Most upvoted comments

This is still relevant.

@thernstig it specifically says The prototype chain is not walked or duplicated. which means after structuredClone the new object has no object sets for its prototype, meaning it cannot be the same. Does that not explain it?

I don’t think that’s correct. My reading is that it won’t recursively deep clone the prototype chain, which makes sense.

This can be seen by trying out these two lines in your browser or Node.js repl:

structuredClone([]).constructor === [].constructor
// => true

Object.getPrototypeOf([]) === Object.getPrototypeOf(structuredClone([]))
// => true

I think that this is Jest specific, because trying the same within the Jest runner results in another behaviour:

console.error(structuredClone([]).constructor === [].constructor)
  ● Console

    console.error
      false

    > 119 |       console.error(structuredClone([]).constructor === [].constructor)
          |               ^


My guess is that this has something to do with worker threads/sandboxing/custom global environment, where maybe the global Array isn’t the same as the Array constructor that structuredClone uses. But I’m not familiar enough with the inner workings of Jest to say for sure…

Not stale

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 30 days.

I suppose this is still a great feature to have.

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 30 days.

this is still open IMO.

pls no stale

@mrazauskas sorry for asking, but I am a bit confused as to the conclusion of this now. Is the suggestion for everyone to switch to jest-light-runner. Considering that Node assert also handles this.

@Grub4K Thanks. Good catch!

Seems like this is simply #2549. Using jest-light-runner solves the problem. Reference: https://github.com/jestjs/jest/issues/2549#issuecomment-1098071474

With jest-light-runner I got:

 PASS  tests/quick.test.js
  expect(structuredClone(value)).toStrictEqual(value)
    ✓ value=string (1 ms)
    ✓ value=1234 (0 ms)
    ✓ value=[] (0 ms)
    ✓ value=[ 1, 2, 3, 4 ] (0 ms)
    ✓ value={} (0 ms)
    ✓ value={ '0': 0 } (0 ms)
    ✓ value=2023-08-16T10:11:58.274Z (0 ms)
  assert.deepStrictEqual(value, structuredClone(value))
    ✓ value=string (0 ms)
    ✓ value=1234 (0 ms)
    ✓ value=[] (0 ms)
    ✓ value=[ 1, 2, 3, 4 ] (0 ms)
    ✓ value={} (0 ms)
    ✓ value={ '0': 0 } (0 ms)
    ✓ value=2023-08-16T10:11:58.274Z (0 ms)

Would be interesting to try this out wider.

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 30 days.

Hi there ! I just stumbled upon the same issue, and I agree that, given structuredClone is now part of the standard library and more and more codebases, there should be an appropriate test for it.

One particular sample that should pass IMO is the following:

describe('foo', () => {
    test('date check', () => {
        const date = new Date();
        expect(structuredClone(date)).toStrictEqual(date);
    });
});

This currently fails with


expect(received).toStrictEqual(expected) // deep equality

    Expected: 2023-05-23T13:12:11.576Z
    Received: serializes to the same string

This is key to me because using the JSON.parse(JSON.stringify(...)) technique does not accommodate nested dates objects (they are serialised to strings), where structuredClone does. I don’t really get what Jest sees to differentiate the original and the clone in the date exemple above, aren’t the prototypes the same?

Yeah, I agree it is subjective.

Would a new toBeClone() add too many similar APIs?

Okay, it looks like this might be a limitation of structuredClone and we should use toEqual instead. From MDN (emphasis mine):

Certain object properties are not preserved:

  • The lastIndex property of RegExp objects is not preserved.
  • Property descriptors, setters, getters, and similar metadata-like features are not duplicated. For example, if an object is marked readonly with a property descriptor, it will be read/write in the duplicate, since that’s the default.
  • The prototype chain is not walked or duplicated.

Might be this is the explanation:

Object.getPrototypeOf(value) === Object.getPrototypeOf(JSON.parse(JSON.stringify(value))) // true

Object.getPrototypeOf(value) === Object.getPrototypeOf(structuredClone(value)) // false