Skip to content

Commit ba355a4

Browse files
authored
feat: allow to customize the serializable properties during error report (#14893)
1 parent ca363c8 commit ba355a4

6 files changed

Lines changed: 111 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- `[jest-environment-node]` Update jest environment with dispose symbols `Symbol` ([#14888](https://github.com/jestjs/jest/pull/14888) & [#14909](https://github.com/jestjs/jest/pull/14909))
1919
- `[@jest/fake-timers]` [**BREAKING**] Upgrade `@sinonjs/fake-timers` to v11 ([#14544](https://github.com/jestjs/jest/pull/14544))
2020
- `[@jest/fake-timers]` Exposing new modern timers function `advanceTimersToFrame()` which advances all timers by the needed milliseconds to execute callbacks currently scheduled with `requestAnimationFrame` ([#14598](https://github.com/jestjs/jest/pull/14598))
21+
- `[jest-matcher-utils]` Add `SERIALIZABLE_PROPERTIES` to allow custom serialization of objects ([#14893](https://github.com/jestjs/jest/pull/14893))
2122
- `[jest-mock]` Add support for the Explicit Resource Management proposal to use the `using` keyword with `jest.spyOn(object, methodName)` ([#14895](https://github.com/jestjs/jest/pull/14895))
2223
- `[jest-runtime]` Exposing new modern timers function `jest.advanceTimersToFrame()` from `@jest/fake-timers` ([#14598](https://github.com/jestjs/jest/pull/14598))
2324
- `[jest-runtime]` Support `import.meta.filename` and `import.meta.dirname` (available from [Node 20.11](https://nodejs.org/en/blog/release/v20.11.0)) ([#14854](https://github.com/jestjs/jest/pull/14854))

docs/ExpectAPI.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,3 +1757,39 @@ it('transitions as expected', () => {
17571757
expect(state).toMatchStateInlineSnapshot(`"done"`);
17581758
});
17591759
```
1760+
1761+
## Serializable properties
1762+
1763+
### `SERIALIZABLE_PROPERTIES`
1764+
1765+
Serializable properties is a set of properties that are considered serializable by Jest. This set is used to determine if a property should be serializable or not. If an object has a property that is not in this set, it is considered not serializable and will not be printed in error messages.
1766+
1767+
You can add your own properties to this set to make sure that your objects are printed correctly. For example, if you have a `Volume` class, and you want to make sure that only the `amount` and `unit` properties are printed, you can add it to `SERIALIZABLE_PROPERTIES`:
1768+
1769+
```js
1770+
import {SERIALIZABLE_PROPERTIES} from 'jest-matcher-utils';
1771+
1772+
class Volume {
1773+
constructor(amount, unit) {
1774+
this.amount = amount;
1775+
this.unit = unit;
1776+
}
1777+
1778+
get label() {
1779+
throw new Error('Not implemented');
1780+
}
1781+
}
1782+
1783+
Volume.prototype[SERIALIZABLE_PROPERTIES] = ['amount', 'unit'];
1784+
1785+
expect(new Volume(1, 'L')).toEqual(new Volume(10, 'L'));
1786+
```
1787+
1788+
This will print only the `amount` and `unit` properties in the error message, ignoring the `label` property.
1789+
1790+
```bash
1791+
expect(received).toEqual(expected) // deep equality
1792+
1793+
Expected: {"amount": 10, "unit": "L"}
1794+
Received: {"amount": 1, "unit": "L"}
1795+
```

packages/jest-matcher-utils/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ To add this package as a dependency of a project, run either of the following co
2121

2222
### Constants
2323

24-
`EXPECTED_COLOR` `RECEIVED_COLOR` `INVERTED_COLOR` `BOLD_WEIGHT` `DIM_COLOR` `SUGGEST_TO_CONTAIN_EQUAL`
24+
`EXPECTED_COLOR` `RECEIVED_COLOR` `INVERTED_COLOR` `BOLD_WEIGHT` `DIM_COLOR` `SUGGEST_TO_CONTAIN_EQUAL` `SERIALIZABLE_PROPERTIES`

packages/jest-matcher-utils/src/__tests__/deepCyclicCopyReplaceable.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
*
77
*/
88

9-
import deepCyclicCopyReplaceable from '../deepCyclicCopyReplaceable';
9+
import deepCyclicCopyReplaceable, {
10+
SERIALIZABLE_PROPERTIES,
11+
} from '../deepCyclicCopyReplaceable';
1012

1113
test('returns the same value for primitive or function values', () => {
1214
const fn = () => {};
@@ -151,3 +153,24 @@ test('should set writable, configurable to true', () => {
151153
key: {configurable: true, enumerable: true, value: 1, writable: true},
152154
});
153155
});
156+
157+
test('should only copy the properties mapped to be serializable', () => {
158+
class Foo {
159+
foo = 'foo';
160+
bar = ['bar'];
161+
get baz() {
162+
throw new Error('should not call getter');
163+
}
164+
}
165+
166+
// @ts-expect-error: Testing purpose
167+
Foo.prototype[SERIALIZABLE_PROPERTIES] = ['foo', 'bar'];
168+
169+
const obj = new Foo();
170+
171+
const copied = deepCyclicCopyReplaceable(obj);
172+
expect(Object.getOwnPropertyDescriptors(copied)).toEqual({
173+
bar: {configurable: true, enumerable: true, value: ['bar'], writable: true},
174+
foo: {configurable: true, enumerable: true, value: 'foo', writable: true},
175+
});
176+
});

packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ if (typeof Buffer !== 'undefined') {
2828
builtInObject.push(Buffer);
2929
}
3030

31+
export const SERIALIZABLE_PROPERTIES = Symbol.for(
32+
'@jest/serializableProperties',
33+
);
34+
3135
const isBuiltInObject = (object: any) =>
3236
builtInObject.includes(object.constructor);
3337

@@ -57,14 +61,27 @@ export default function deepCyclicCopyReplaceable<T>(
5761

5862
function deepCyclicCopyObject<T>(object: T, cycles: WeakMap<any, unknown>): T {
5963
const newObject = Object.create(Object.getPrototypeOf(object));
60-
let descriptors: Record<string, PropertyDescriptor> = {};
64+
let descriptors: Record<string | symbol, PropertyDescriptor> = {};
6165
let obj = object;
6266
do {
63-
descriptors = Object.assign(
64-
{},
65-
Object.getOwnPropertyDescriptors(obj),
66-
descriptors,
67-
);
67+
const serializableProperties = getSerializableProperties(obj);
68+
69+
if (serializableProperties === undefined) {
70+
descriptors = Object.assign(
71+
{},
72+
Object.getOwnPropertyDescriptors(obj),
73+
descriptors,
74+
);
75+
} else {
76+
for (const property of serializableProperties) {
77+
if (!descriptors[property]) {
78+
descriptors[property] = Object.getOwnPropertyDescriptor(
79+
obj,
80+
property,
81+
)!;
82+
}
83+
}
84+
}
6885
} while (
6986
(obj = Object.getPrototypeOf(obj)) &&
7087
obj !== Object.getPrototypeOf({})
@@ -131,3 +148,24 @@ function deepCyclicCopyMap<T>(
131148

132149
return newMap as any;
133150
}
151+
152+
function getSerializableProperties<T>(
153+
obj: T,
154+
): Array<string | symbol> | undefined {
155+
if (typeof obj !== 'object' || obj === null) {
156+
return;
157+
}
158+
159+
const serializableProperties: unknown = (obj as Record<string | symbol, any>)[
160+
SERIALIZABLE_PROPERTIES
161+
];
162+
163+
if (!Array.isArray(serializableProperties)) {
164+
return;
165+
}
166+
167+
return serializableProperties.filter(
168+
(key): key is string | symbol =>
169+
typeof key === 'string' || typeof key === 'symbol',
170+
);
171+
}

packages/jest-matcher-utils/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import {
2424
plugins as prettyFormatPlugins,
2525
} from 'pretty-format';
2626
import Replaceable from './Replaceable';
27-
import deepCyclicCopyReplaceable from './deepCyclicCopyReplaceable';
27+
import deepCyclicCopyReplaceable, {
28+
SERIALIZABLE_PROPERTIES,
29+
} from './deepCyclicCopyReplaceable';
30+
31+
export {SERIALIZABLE_PROPERTIES};
2832

2933
const {
3034
AsymmetricMatcher,

0 commit comments

Comments
 (0)