Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow to customize the serializable properties during error report #14893

Merged
merged 3 commits into from
Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- `[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))
- `[@jest/fake-timers]` [**BREAKING**] Upgrade `@sinonjs/fake-timers` to v11 ([#14544](https://github.com/jestjs/jest/pull/14544))
- `[@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))
- `[jest-matcher-utils]` Add `SERIALIZABLE_PROPERTIES` to allow custom serialization of objects ([#14893](https://github.com/jestjs/jest/pull/14893))
- `[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))
- `[jest-runtime]` Exposing new modern timers function `jest.advanceTimersToFrame()` from `@jest/fake-timers` ([#14598](https://github.com/jestjs/jest/pull/14598))
- `[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))
Expand Down
36 changes: 36 additions & 0 deletions docs/ExpectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1757,3 +1757,39 @@ it('transitions as expected', () => {
expect(state).toMatchStateInlineSnapshot(`"done"`);
});
```

## Serializable properties

### `SERIALIZABLE_PROPERTIES`

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.

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`:

```js
import {SERIALIZABLE_PROPERTIES} from 'jest-matcher-utils';

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

get label() {
throw new Error('Not implemented');
}
}

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

expect(new Volume(1, 'L')).toEqual(new Volume(10, 'L'));
```

This will print only the `amount` and `unit` properties in the error message, ignoring the `label` property.

```bash
expect(received).toEqual(expected) // deep equality

Expected: {"amount": 10, "unit": "L"}
Received: {"amount": 1, "unit": "L"}
```
2 changes: 1 addition & 1 deletion packages/jest-matcher-utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ To add this package as a dependency of a project, run either of the following co

### Constants

`EXPECTED_COLOR` `RECEIVED_COLOR` `INVERTED_COLOR` `BOLD_WEIGHT` `DIM_COLOR` `SUGGEST_TO_CONTAIN_EQUAL`
`EXPECTED_COLOR` `RECEIVED_COLOR` `INVERTED_COLOR` `BOLD_WEIGHT` `DIM_COLOR` `SUGGEST_TO_CONTAIN_EQUAL` `SERIALIZABLE_PROPERTIES`
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*
*/

import deepCyclicCopyReplaceable from '../deepCyclicCopyReplaceable';
import deepCyclicCopyReplaceable, {
SERIALIZABLE_PROPERTIES,
} from '../deepCyclicCopyReplaceable';

test('returns the same value for primitive or function values', () => {
const fn = () => {};
Expand Down Expand Up @@ -151,3 +153,24 @@ test('should set writable, configurable to true', () => {
key: {configurable: true, enumerable: true, value: 1, writable: true},
});
});

test('should only copy the properties mapped to be serializable', () => {
class Foo {
foo = 'foo';
bar = ['bar'];
get baz() {
throw new Error('should not call getter');
}
}

// @ts-expect-error: Testing purpose
Foo.prototype[SERIALIZABLE_PROPERTIES] = ['foo', 'bar'];

const obj = new Foo();

const copied = deepCyclicCopyReplaceable(obj);
expect(Object.getOwnPropertyDescriptors(copied)).toEqual({
bar: {configurable: true, enumerable: true, value: ['bar'], writable: true},
foo: {configurable: true, enumerable: true, value: 'foo', writable: true},
});
});
50 changes: 44 additions & 6 deletions packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ if (typeof Buffer !== 'undefined') {
builtInObject.push(Buffer);
}

export const SERIALIZABLE_PROPERTIES = Symbol.for(
'@jest/serializableProperties',
);

const isBuiltInObject = (object: any) =>
builtInObject.includes(object.constructor);

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

function deepCyclicCopyObject<T>(object: T, cycles: WeakMap<any, unknown>): T {
const newObject = Object.create(Object.getPrototypeOf(object));
let descriptors: Record<string, PropertyDescriptor> = {};
let descriptors: Record<string | symbol, PropertyDescriptor> = {};
let obj = object;
do {
descriptors = Object.assign(
{},
Object.getOwnPropertyDescriptors(obj),
descriptors,
);
const serializableProperties = getSerializableProperties(obj);

if (serializableProperties === undefined) {
descriptors = Object.assign(
{},
Object.getOwnPropertyDescriptors(obj),
descriptors,
);
} else {
for (const property of serializableProperties) {
if (!descriptors[property]) {
descriptors[property] = Object.getOwnPropertyDescriptor(
obj,
property,
)!;
}
}
}
} while (
(obj = Object.getPrototypeOf(obj)) &&
obj !== Object.getPrototypeOf({})
Expand Down Expand Up @@ -131,3 +148,24 @@ function deepCyclicCopyMap<T>(

return newMap as any;
}

function getSerializableProperties<T>(
obj: T,
): Array<string | symbol> | undefined {
if (typeof obj !== 'object' || obj === null) {
return;
}

const serializableProperties: unknown = (obj as Record<string | symbol, any>)[
SERIALIZABLE_PROPERTIES
];

if (!Array.isArray(serializableProperties)) {
return;
}

return serializableProperties.filter(
(key): key is string | symbol =>
typeof key === 'string' || typeof key === 'symbol',
);
}
6 changes: 5 additions & 1 deletion packages/jest-matcher-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import {
plugins as prettyFormatPlugins,
} from 'pretty-format';
import Replaceable from './Replaceable';
import deepCyclicCopyReplaceable from './deepCyclicCopyReplaceable';
import deepCyclicCopyReplaceable, {
SERIALIZABLE_PROPERTIES,
} from './deepCyclicCopyReplaceable';

export {SERIALIZABLE_PROPERTIES};

const {
AsymmetricMatcher,
Expand Down
Loading