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

Fix diffing object contain readonly symbol key object #10414

1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

### Fixes

- `[jest-matcher-utils]` Fix diffing object contain readonly symbol key object ([#10414](https://github.com/facebook/jest/pull/10414))
- `[jest-reporters]` Fixes notify reporter on Linux (using notify-send) ([#10393](https://github.com/facebook/jest/pull/10400))
- `[jest-snapshot]` Correctly handles arrays and property matchers in snapshots ([#10404](https://github.com/facebook/jest/pull/10404))

Expand Down
17 changes: 11 additions & 6 deletions packages/jest-matcher-utils/src/Replaceable.ts
Expand Up @@ -32,12 +32,17 @@ export default class Replaceable {

forEach(cb: ReplaceableForEachCallBack): void {
if (this.type === 'object') {
Object.entries(this.object).forEach(([key, value]) => {
cb(value, key, this.object);
});
Object.getOwnPropertySymbols(this.object).forEach(key => {
cb(this.object[key], key, this.object);
});
const descriptors = Object.getOwnPropertyDescriptors(this.object);
[
...Object.keys(descriptors),
...Object.getOwnPropertySymbols(descriptors),
]
//@ts-expect-error because typescript do not support symbol key in object
//https://github.com/microsoft/TypeScript/issues/1863
.filter(key => descriptors[key].enumerable)
.forEach(key => {
cb(this.object[key], key, this.object);
});
} else {
this.object.forEach(cb);
}
Expand Down
24 changes: 24 additions & 0 deletions packages/jest-matcher-utils/src/__tests__/Replaceable.test.ts
Expand Up @@ -132,6 +132,30 @@ describe('Replaceable', () => {
expect(cb.mock.calls[0]).toEqual([1, 'a', map]);
expect(cb.mock.calls[1]).toEqual([2, 'b', map]);
});

test('forEach should ignore nonenumerable property', () => {
const symbolKey = Symbol('jest');
const symbolKey2 = Symbol('awesome');
const object = {a: 1, [symbolKey]: 3};
Object.defineProperty(object, 'b', {
configurable: true,
enumerable: false,
value: 2,
writable: true,
});
Object.defineProperty(object, symbolKey2, {
configurable: true,
enumerable: false,
value: 4,
writable: true,
});
const replaceable = new Replaceable(object);
const cb = jest.fn();
replaceable.forEach(cb);
expect(cb).toHaveBeenCalledTimes(2);
expect(cb.mock.calls[0]).toEqual([1, 'a', object]);
expect(cb.mock.calls[1]).toEqual([3, symbolKey, object]);
});
});

describe('isReplaceable', () => {
Expand Down
Expand Up @@ -198,3 +198,16 @@ exports[`printDiffOrStringify has no common after clean up chaff one-line 1`] =
Expected: <g>"delete"</>
Received: <r>"insert"</>
`;

exports[`printDiffOrStringify object contain readonly symbol key object 1`] = `
<g>- Expected - 1</>
<r>+ Received + 1</>

<d> Object {</>
<g>- "b": 2,</>
<r>+ "b": 1,</>
<d> Symbol(key): Object {</>
<d> "a": 1,</>
<d> },</>
<d> }</>
`;
Expand Up @@ -43,13 +43,20 @@ test('convert accessor descriptor into value descriptor', () => {
});
});

test('skips non-enumerables', () => {
test('shuold not skips non-enumerables', () => {
const obj = {};
Object.defineProperty(obj, 'foo', {enumerable: false, value: 'bar'});

const copy = deepCyclicCopyReplaceable(obj);

expect(Object.getOwnPropertyDescriptors(copy)).toEqual({});
expect(Object.getOwnPropertyDescriptors(copy)).toEqual({
foo: {
configurable: true,
enumerable: false,
value: 'bar',
writable: true,
},
});
});

test('copies symbols', () => {
Expand Down Expand Up @@ -113,3 +120,22 @@ test('return same value for built-in object type except array, map and object',
expect(deepCyclicCopyReplaceable(regexp)).toBe(regexp);
expect(deepCyclicCopyReplaceable(set)).toBe(set);
});

test('should copy object symbol key property', () => {
const symbolKey = Symbol.for('key');
expect(deepCyclicCopyReplaceable({[symbolKey]: 1})).toEqual({[symbolKey]: 1});
});

test('should set writable, configurable to true', () => {
const a = {};
Object.defineProperty(a, 'key', {
configurable: false,
enumerable: true,
value: 1,
writable: false,
});
const copied = deepCyclicCopyReplaceable(a);
expect(Object.getOwnPropertyDescriptors(copied)).toEqual({
key: {configurable: true, enumerable: true, value: 1, writable: true},
});
});
Expand Up @@ -50,6 +50,29 @@ describe('printDiffOrStringify', () => {
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
});

test('object contain readonly symbol key object', () => {
const expected = {b: 2};
const received = {b: 1};
const symbolKey = Symbol.for('key');
Object.defineProperty(expected, symbolKey, {
configurable: true,
enumerable: true,
value: {
a: 1,
},
writable: false,
});
Object.defineProperty(received, symbolKey, {
configurable: true,
enumerable: true,
value: {
a: 1,
},
writable: false,
});
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
});

describe('MAX_DIFF_STRING_LENGTH', () => {
const lessChange = INVERTED_COLOR('single ');
const less = 'single line';
Expand Down
35 changes: 24 additions & 11 deletions packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts
Expand Up @@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/

import {plugins} from 'pretty-format';

const builtInObject = [
Array,
Buffer,
Expand Down Expand Up @@ -42,6 +44,9 @@ export default function deepCyclicCopyReplaceable<T>(
return deepCyclicCopyMap(value, cycles);
} else if (isBuiltInObject(value)) {
return value;
} else if (plugins.DOMElement.test(value)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (plugins.DOMElement.test(value)) {
} else if (typeof value.cloneNode === 'function') {

maybe? not sure if it's better or not

//@ts-expect-error skip casting to Node
return value.cloneNode(true);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need a suggestion.
I don't think use @ts-expect-error to ignore Property 'cloneNode' does not exist on type 'T' is a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return value.cloneNode(true);
return (((value as unknown) as Element).cloneNode(true) as unknown) as T;

Can fix error with this, but a little bit ugly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment override is fine

} else {
return deepCyclicCopyObject(value, cycles);
}
Expand All @@ -55,25 +60,33 @@ function deepCyclicCopyObject<T>(object: T, cycles: WeakMap<any, any>): T {

cycles.set(object, newObject);

Object.keys(descriptors).forEach(key => {
if (descriptors[key].enumerable) {
descriptors[key] = {
const newDescriptors = [
...Object.keys(descriptors),
...Object.getOwnPropertySymbols(descriptors),
].reduce(
//@ts-expect-error because typescript do not support symbol key in object
//https://github.com/microsoft/TypeScript/issues/1863
(newDescriptors: {[x: string]: PropertyDescriptor}, key: string) => {
const enumerable = descriptors[key].enumerable;

newDescriptors[key] = {
configurable: true,
enumerable: true,
enumerable,
value: deepCyclicCopyReplaceable(
// this accesses the value or getter, depending. We just care about the value anyways, and this allows us to not mess with accessors
// it has the side effect of invoking the getter here though, rather than copying it over
(object as Record<string, unknown>)[key],
(object as Record<string | symbol, unknown>)[key],
cycles,
),
writable: true,
};
} else {
delete descriptors[key];
}
});

return Object.defineProperties(newObject, descriptors);
return newDescriptors;
},
{},
);
//@ts-expect-error because typescript do not support symbol key in object
//https://github.com/microsoft/TypeScript/issues/1863
return Object.defineProperties(newObject, newDescriptors);
}

function deepCyclicCopyArray<T>(array: Array<T>, cycles: WeakMap<any, any>): T {
Expand Down