Skip to content

fix(expect-utils): honour SERIALIZABLE_PROPERTIES in equality checks - #16461

Open
snmsoodan wants to merge 1 commit into
jestjs:mainfrom
snmsoodan:fix/15073-serializable-properties
Open

snmsoodan wants to merge 1 commit into
jestjs:mainfrom
snmsoodan:fix/15073-serializable-properties

Conversation

@snmsoodan

Copy link
Copy Markdown

Summary

SERIALIZABLE_PROPERTIES currently only reaches one half of the story: jest-matcher-utils consults it when printing a diff, but the equality matchers ignore it, so an object that declares which of its properties are serializable still fails toEqual on the properties it told Jest not to care about:

const {SERIALIZABLE_PROPERTIES} = require('jest-matcher-utils');

class Volume {
  constructor(amount, unit, label) {
    this.amount = amount;
    this.unit = unit;
    this.label = label;
  }
}

Volume.prototype[SERIALIZABLE_PROPERTIES] = ['amount', 'unit'];

test('only compares the serializable properties', () => {
  // prints a diff mentioning only `amount`/`unit`…
  expect(new Volume(1, 'L', 'first')).toEqual(new Volume(2, 'L', 'second'));
});

On main the diff printer filters label out while eq() still walks it, so the matcher reports a difference on a property it deliberately hides from the printed diff. This aligns the two, as discussed in the issue:

  • @SimenB: "I think it makes sense to align, but I'll defer to @georgekaran"
  • @georgekaran (who added the feature): "Personally the second option [apply the properties to the equality checkers] makes more sense to me"

What the change does

eq()'s keys() helper now skips own enumerable string and symbol keys that the object declared as not serializable, mirroring getSerializableProperties() from jest-matcher-utils/src/deepCyclicCopyReplaceable.ts (looked up via Symbol.for('@jest/serializableProperties'), so @jest/expect-utils keeps no dependency on jest-matcher-utils). The declaration is inherited, so setting it on a class prototype covers every instance, exactly like the printing path.

The behaviour stays opt-in: an object that declares nothing is compared exactly as before.

@Fryuni's objection

In the issue, @Fryuni argued that equality is about runtime values rather than serialized representations, with Array(2) vs [undefined, undefined] as the counterexample. That concern is the reason for the explicit exclusion list in isBuiltInObject(): arrays, typed arrays, ArrayBuffer, Date, Error, Map, Promise, RegExp, Set and WeakMap/WeakSet never consult the declaration, because they already have their own comparison semantics (iterableEquality, item-wise comparison, message, and so on). The sparse-array example therefore behaves exactly as it does on main, and so do asymmetric matchers. A user has to opt in on a class of their own.

One visible consequence, which follows directly from aligning with the printer and is documented in docs/ExpectAPI.md: for an instance that declares a subset, toEqual(instance, plainObjectWithTheSameKeys) is now true while toStrictEqual stays false, since the latter still compares prototypes.

Fixes #15073

Tests

packages/expect-utils/src/__tests__/jasmineUtils.test.ts covers string keys, symbol keys, nested objects/arrays/Maps, inherited declarations, empty declarations, a throwing getter and the excluded built-ins, plus no-regression cases (a class that declares nothing, an object that declares undefined, built-ins that carry the symbol).

packages/expect/src/__tests__/serializableProperties.test.ts covers toEqual, toStrictEqual, toHaveBeenCalledWith and the asymmetric matcher path end to end.

Locally (with the new tests in place and packages/expect-utils packages/expect packages/jest-matcher-utils packages/jest-snapshot packages/jest-circus):

  • main: 51 suites, 1519 passed, 0 failed
  • this branch: 53 suites, 1532 passed, 0 failed — i.e. +2 suites / +13 tests, no regressions

yarn build:ts, yarn typecheck:tests, yarn eslint and yarn prettier --check are clean.

@netlify

netlify Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for jestjs ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 44c3c57
🔍 Latest deploy log https://app.netlify.com/projects/jestjs/deploys/6ab42270fe6c8e0009c07432
😎 Deploy Preview https://deploy-preview-16461--jestjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@snmsoodan
snmsoodan force-pushed the fix/15073-serializable-properties branch from 0a4378a to b9f2b1a Compare September 23, 2026 18:48
@github-actions github-actions Bot added the require-changelog If a PR does requires a changelog entry label Sep 23, 2026
@pkg-pr-new

pkg-pr-new Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

babel-jest

npm i https://pkg.pr.new/babel-jest@16461

babel-plugin-jest-hoist

npm i https://pkg.pr.new/babel-plugin-jest-hoist@16461

babel-preset-jest

npm i https://pkg.pr.new/babel-preset-jest@16461

create-jest

npm i https://pkg.pr.new/create-jest@16461

@jest/diff-sequences

npm i https://pkg.pr.new/@jest/diff-sequences@16461

expect

npm i https://pkg.pr.new/expect@16461

@jest/expect-utils

npm i https://pkg.pr.new/@jest/expect-utils@16461

jest

npm i https://pkg.pr.new/jest@16461

jest-changed-files

npm i https://pkg.pr.new/jest-changed-files@16461

jest-circus

npm i https://pkg.pr.new/jest-circus@16461

jest-cli

npm i https://pkg.pr.new/jest-cli@16461

jest-config

npm i https://pkg.pr.new/jest-config@16461

@jest/console

npm i https://pkg.pr.new/@jest/console@16461

@jest/core

npm i https://pkg.pr.new/@jest/core@16461

@jest/create-cache-key-function

npm i https://pkg.pr.new/@jest/create-cache-key-function@16461

jest-diff

npm i https://pkg.pr.new/jest-diff@16461

jest-docblock

npm i https://pkg.pr.new/jest-docblock@16461

jest-each

npm i https://pkg.pr.new/jest-each@16461

@jest/environment

npm i https://pkg.pr.new/@jest/environment@16461

jest-environment-jsdom

npm i https://pkg.pr.new/jest-environment-jsdom@16461

@jest/environment-jsdom-abstract

npm i https://pkg.pr.new/@jest/environment-jsdom-abstract@16461

jest-environment-node

npm i https://pkg.pr.new/jest-environment-node@16461

@jest/expect

npm i https://pkg.pr.new/@jest/expect@16461

@jest/fake-timers

npm i https://pkg.pr.new/@jest/fake-timers@16461

@jest/get-type

npm i https://pkg.pr.new/@jest/get-type@16461

@jest/globals

npm i https://pkg.pr.new/@jest/globals@16461

jest-haste-map

npm i https://pkg.pr.new/jest-haste-map@16461

jest-jasmine2

npm i https://pkg.pr.new/jest-jasmine2@16461

jest-leak-detector

npm i https://pkg.pr.new/jest-leak-detector@16461

jest-matcher-utils

npm i https://pkg.pr.new/jest-matcher-utils@16461

jest-message-util

npm i https://pkg.pr.new/jest-message-util@16461

jest-mock

npm i https://pkg.pr.new/jest-mock@16461

@jest/pattern

npm i https://pkg.pr.new/@jest/pattern@16461

jest-phabricator

npm i https://pkg.pr.new/jest-phabricator@16461

jest-regex-util

npm i https://pkg.pr.new/jest-regex-util@16461

@jest/reporters

npm i https://pkg.pr.new/@jest/reporters@16461

jest-resolve

npm i https://pkg.pr.new/jest-resolve@16461

jest-resolve-dependencies

npm i https://pkg.pr.new/jest-resolve-dependencies@16461

jest-runner

npm i https://pkg.pr.new/jest-runner@16461

jest-runtime

npm i https://pkg.pr.new/jest-runtime@16461

@jest/schemas

npm i https://pkg.pr.new/@jest/schemas@16461

jest-snapshot

npm i https://pkg.pr.new/jest-snapshot@16461

@jest/snapshot-utils

npm i https://pkg.pr.new/@jest/snapshot-utils@16461

@jest/source-map

npm i https://pkg.pr.new/@jest/source-map@16461

@jest/test-result

npm i https://pkg.pr.new/@jest/test-result@16461

@jest/test-sequencer

npm i https://pkg.pr.new/@jest/test-sequencer@16461

@jest/transform

npm i https://pkg.pr.new/@jest/transform@16461

@jest/types

npm i https://pkg.pr.new/@jest/types@16461

jest-util

npm i https://pkg.pr.new/jest-util@16461

jest-validate

npm i https://pkg.pr.new/jest-validate@16461

jest-watcher

npm i https://pkg.pr.new/jest-watcher@16461

jest-worker

npm i https://pkg.pr.new/jest-worker@16461

pretty-format

npm i https://pkg.pr.new/pretty-format@16461

commit: 44c3c57

`SERIALIZABLE_PROPERTIES` lets an object declare which of its properties
are serializable, but it was only honoured while printing. Equality
compared every own enumerable property instead, so a class could not
exclude its internal fields from `toEqual`/`toStrictEqual`.

`equals` now looks the symbol up with `Symbol.for`, which keeps
`@jest/expect-utils` free of a dependency on `jest-matcher-utils`, and
only compares the declared properties of plain objects and class
instances. Arrays, typed arrays, `Map`s, `Set`s, `Date`s, `RegExp`s,
`Error`s, promises and DOM nodes keep their own comparison semantics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7ee9c5ce-96b9-4d2b-a163-a6e5a4a4eb38
@snmsoodan
snmsoodan force-pushed the fix/15073-serializable-properties branch from b9f2b1a to 44c3c57 Compare September 23, 2026 19:03

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

require-changelog If a PR does requires a changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: New SERIALIZABLE_PROPERTIES is only used by matcher-utils

1 participant