/
githubmirror
/
jest
Обзор
Документация
Войти
/
githubmirror
/
jest
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/expect-utils/src/__tests__/utils.test.ts
840 строк
25 KB
Michael Hughes
fix(expect-utils): iterableEquality should not throw on exotic iterables (#15952)
05 май 2026, 21:34
Не верифицирован
05 май 2026, 21:34
314df33
Код
Авторство
О чём код?
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import {List, OrderedMap, OrderedSet, Record} from 'immutable'; import {stringify} from 'jest-matcher-utils'; import {createContext, runInContext} from 'vm'; import {equals} from '../jasmineUtils'; import { arrayBufferEquality, emptyObject, getObjectSubset, getPath, iterableEquality, subsetEquality, typeEquality, } from '../utils'; describe('getPath()', () => { test('property exists', () => { expect(getPath({a: {b: {c: 5}}}, 'a.b.c')).toEqual({ endPropIsDefined: true, hasEndProp: true, lastTraversedObject: {c: 5}, traversedPath: ['a', 'b', 'c'], value: 5, }); expect(getPath({a: {b: {c: {d: 1}}}}, 'a.b.c.d')).toEqual({ endPropIsDefined: true, hasEndProp: true, lastTraversedObject: {d: 1}, traversedPath: ['a', 'b', 'c', 'd'], value: 1, }); }); test('property doesnt exist', () => { expect(getPath({a: {b: {}}}, 'a.b.c')).toEqual({ endPropIsDefined: false, hasEndProp: false, lastTraversedObject: {}, traversedPath: ['a', 'b'], value: undefined, }); }); test('property exist but undefined', () => { expect(getPath({a: {b: {c: undefined}}}, 'a.b.c')).toEqual({ endPropIsDefined: true, hasEndProp: true, lastTraversedObject: {c: undefined}, traversedPath: ['a', 'b', 'c'], value: undefined, }); }); test('property is a getter on class instance', () => { class A { get a() { return 'a'; } get b() { return {c: 'c'}; } } expect(getPath(new A(), 'a')).toEqual({ endPropIsDefined: true, hasEndProp: true, lastTraversedObject: new A(), traversedPath: ['a'], value: 'a', }); expect(getPath(new A(), 'b.c')).toEqual({ endPropIsDefined: true, hasEndProp: true, lastTraversedObject: {c: 'c'}, traversedPath: ['b', 'c'], value: 'c', }); }); test('property is inherited', () => { class A {} // @ts-expect-error A.prototype.a = 'a'; expect(getPath(new A(), 'a')).toEqual({ endPropIsDefined: true, hasEndProp: true, lastTraversedObject: new A(), traversedPath: ['a'], value: 'a', }); }); test('path breaks', () => { expect(getPath({a: {}}, 'a.b.c')).toEqual({ hasEndProp: false, lastTraversedObject: {}, traversedPath: ['a'], value: undefined, }); }); test('empty object at the end', () => { expect(getPath({a: {b: {c: {}}}}, 'a.b.c.d')).toEqual({ endPropIsDefined: false, hasEndProp: false, lastTraversedObject: {}, traversedPath: ['a', 'b', 'c'], value: undefined, }); }); }); describe('getObjectSubset', () => { for (const [object, subset, expected] of [ [{a: 'b', c: 'd'}, {a: 'd'}, {a: 'b'}], [{a: [1, 2], b: 'b'}, {a: [3, 4]}, {a: [1, 2]}], [[{a: 'b', c: 'd'}], [{a: 'z'}], [{a: 'b'}]], [ [1, 2], [1, 2, 3], [1, 2], ], [{a: [1]}, {a: [1, 2]}, {a: [1]}], [new Date('2015-11-30'), new Date('2016-12-30'), new Date('2015-11-30')], ]) { test( `expect(getObjectSubset(${stringify(object)}, ${stringify(subset)}))` + `.toEqual(${stringify(expected)})`, () => { expect(getObjectSubset(object, subset)).toEqual(expected); }, ); } describe('returns the object instance if the subset has no extra properties', () => { test('Date', () => { const object = new Date('2015-11-30'); const subset = new Date('2016-12-30'); expect(getObjectSubset(object, subset)).toBe(object); }); }); describe('returns the subset instance if its property values are equal', () => { test('Object', () => { const object = {key0: 'zero', key1: 'one', key2: 'two'}; const subset = {key0: 'zero', key2: 'two'}; expect(getObjectSubset(object, subset)).toBe(subset); }); describe('Uint8Array', () => { const equalObject = {'0': 0, '1': 0, '2': 0}; const typedArray = new Uint8Array(3); test('expected', () => { const object = equalObject; const subset = typedArray; expect(getObjectSubset(object, subset)).toBe(subset); }); test('received', () => { const object = typedArray; const subset = equalObject; expect(getObjectSubset(object, subset)).toBe(subset); }); }); }); describe('calculating subsets of objects with circular references', () => { test('simple circular references', () => { type CircularObj = {a?: string; b?: string; ref?: unknown}; const nonCircularObj = {a: 'world', b: 'something'}; const circularObjA: CircularObj = {a: 'hello'}; circularObjA.ref = circularObjA; const circularObjB: CircularObj = {a: 'world'}; circularObjB.ref = circularObjB; const primitiveInsteadOfRef: CircularObj = {b: 'something'}; primitiveInsteadOfRef.ref = 'not a ref'; const nonCircularRef: CircularObj = {b: 'something'}; nonCircularRef.ref = {}; expect(getObjectSubset(circularObjA, nonCircularObj)).toEqual({ a: 'hello', }); expect(getObjectSubset(nonCircularObj, circularObjA)).toEqual({ a: 'world', }); expect(getObjectSubset(circularObjB, circularObjA)).toEqual(circularObjB); expect(getObjectSubset(primitiveInsteadOfRef, circularObjA)).toEqual({ ref: 'not a ref', }); expect(getObjectSubset(nonCircularRef, circularObjA)).toEqual({ ref: {}, }); }); test('transitive circular references', () => { type CircularObj = {a?: string; nestedObj?: unknown}; const nonCircularObj = {a: 'world', b: 'something'}; const transitiveCircularObjA: CircularObj = {a: 'hello'}; transitiveCircularObjA.nestedObj = {parentObj: transitiveCircularObjA}; const transitiveCircularObjB: CircularObj = {a: 'world'}; transitiveCircularObjB.nestedObj = {parentObj: transitiveCircularObjB}; const primitiveInsteadOfRef: CircularObj = {}; primitiveInsteadOfRef.nestedObj = {otherProp: 'not the parent ref'}; const nonCircularRef: CircularObj = {}; nonCircularRef.nestedObj = {otherProp: {}}; expect(getObjectSubset(transitiveCircularObjA, nonCircularObj)).toEqual({ a: 'hello', }); expect(getObjectSubset(nonCircularObj, transitiveCircularObjA)).toEqual({ a: 'world', }); expect( getObjectSubset(transitiveCircularObjB, transitiveCircularObjA), ).toEqual(transitiveCircularObjB); expect( getObjectSubset(primitiveInsteadOfRef, transitiveCircularObjA), ).toEqual({nestedObj: {otherProp: 'not the parent ref'}}); expect(getObjectSubset(nonCircularRef, transitiveCircularObjA)).toEqual({ nestedObj: {otherProp: {}}, }); }); }); }); describe('emptyObject()', () => { test('matches an empty object', () => { expect(emptyObject({})).toBe(true); }); test('does not match an object with keys', () => { expect(emptyObject({foo: undefined})).toBe(false); }); test('does not match a non-object', () => { expect(emptyObject(null)).toBe(false); expect(emptyObject(34)).toBe(false); }); }); describe('subsetEquality()', () => { test('matching object returns true', () => { expect(subsetEquality({foo: 'bar'}, {foo: 'bar'})).toBe(true); }); test('object without keys is undefined', () => { expect(subsetEquality('foo', 'bar')).toBeUndefined(); }); test('objects to not match', () => { expect(subsetEquality({foo: 'bar'}, {foo: 'baz'})).toBe(false); expect(subsetEquality('foo', {foo: 'baz'})).toBe(false); }); test('null does not return errors', () => { expect(subsetEquality(null, {foo: 'bar'})).not.toBeTruthy(); }); test('undefined does not return errors', () => { expect(subsetEquality(undefined, {foo: 'bar'})).not.toBeTruthy(); }); describe('matching subsets with circular references', () => { test('simple circular references', () => { type CircularObj = {a?: string; ref?: unknown}; const circularObjA1: CircularObj = {a: 'hello'}; circularObjA1.ref = circularObjA1; const circularObjA2: CircularObj = {a: 'hello'}; circularObjA2.ref = circularObjA2; const circularObjB: CircularObj = {a: 'world'}; circularObjB.ref = circularObjB; const primitiveInsteadOfRef: CircularObj = {}; primitiveInsteadOfRef.ref = 'not a ref'; expect(subsetEquality(circularObjA1, {})).toBe(true); expect(subsetEquality({}, circularObjA1)).toBe(false); expect(subsetEquality(circularObjA2, circularObjA1)).toBe(true); expect(subsetEquality(circularObjB, circularObjA1)).toBe(false); expect(subsetEquality(primitiveInsteadOfRef, circularObjA1)).toBe(false); }); test('referenced object on same level should not regarded as circular reference', () => { const referencedObj = {abc: 'def'}; const object = { a: {abc: 'def'}, b: {abc: 'def', zzz: 'zzz'}, }; const thisIsNotCircular = { a: referencedObj, b: referencedObj, }; expect(subsetEquality(object, thisIsNotCircular)).toBeTruthy(); }); test('transitive circular references', () => { type CircularObj = {a: string; nestedObj?: unknown}; const transitiveCircularObjA1: CircularObj = {a: 'hello'}; transitiveCircularObjA1.nestedObj = {parentObj: transitiveCircularObjA1}; const transitiveCircularObjA2: CircularObj = {a: 'hello'}; transitiveCircularObjA2.nestedObj = { parentObj: transitiveCircularObjA2, }; const transitiveCircularObjB: CircularObj = {a: 'world'}; transitiveCircularObjB.nestedObj = { parentObj: transitiveCircularObjB, }; const primitiveInsteadOfRef = { parentObj: 'not the parent ref', }; expect(subsetEquality(transitiveCircularObjA1, {})).toBe(true); expect(subsetEquality({}, transitiveCircularObjA1)).toBe(false); expect( subsetEquality(transitiveCircularObjA2, transitiveCircularObjA1), ).toBe(true); expect( subsetEquality(transitiveCircularObjB, transitiveCircularObjA1), ).toBe(false); expect( subsetEquality(primitiveInsteadOfRef, transitiveCircularObjA1), ).toBe(false); }); }); describe('matching subsets with symbols', () => { describe('same symbol', () => { test('objects to not match with value diff', () => { const symbol = Symbol('foo'); expect(subsetEquality({[symbol]: 1}, {[symbol]: 2})).toBe(false); }); test('objects to match with non-enumerable symbols', () => { const symbol = Symbol('foo'); const foo = {}; Object.defineProperty(foo, symbol, { enumerable: false, value: 1, }); const bar = {}; Object.defineProperty(bar, symbol, { enumerable: false, value: 2, }); expect(subsetEquality(foo, bar)).toBe(true); }); }); describe('different symbol', () => { test('objects to not match with same value', () => { expect(subsetEquality({[Symbol('foo')]: 1}, {[Symbol('foo')]: 2})).toBe( false, ); }); test('objects to match with non-enumerable symbols', () => { const foo = {}; Object.defineProperty(foo, Symbol('foo'), { enumerable: false, value: 1, }); const bar = {}; Object.defineProperty(bar, Symbol('foo'), { enumerable: false, value: 2, }); expect(subsetEquality(foo, bar)).toBe(true); }); }); }); describe('subset is not object with keys', () => { test('returns true if subset has keys', () => { expect(subsetEquality({foo: 'bar'}, {foo: 'bar'})).toBe(true); }); test('returns true if subset has Symbols', () => { const symbol = Symbol('foo'); expect(subsetEquality({[symbol]: 'bar'}, {[symbol]: 'bar'})).toBe(true); }); test('returns undefined if subset has no keys', () => { expect(subsetEquality('foo', 'bar')).toBeUndefined(); }); test('returns undefined if subset is null', () => { expect(subsetEquality({foo: 'bar'}, null)).toBeUndefined(); }); test('returns undefined if subset is Error', () => { expect(subsetEquality({foo: 'bar'}, new Error())).toBeUndefined(); }); test('returns undefined if subset is Array', () => { expect(subsetEquality({foo: 'bar'}, [])).toBeUndefined(); }); test('returns undefined if subset is Date', () => { expect(subsetEquality({foo: 'bar'}, new Date())).toBeUndefined(); }); test('returns undefined if subset is Set', () => { expect(subsetEquality({foo: 'bar'}, new Set())).toBeUndefined(); }); test('returns undefined if subset is Map', () => { expect(subsetEquality({foo: 'bar'}, new Map())).toBeUndefined(); }); }); }); describe('iterableEquality', () => { test('returns true when given circular iterators', () => { class Iter { *[Symbol.iterator]() { yield this; } } const a = new Iter(); const b = new Iter(); expect(iterableEquality(a, b)).toBe(true); }); test('returns true when given circular Set', () => { const a = new Set(); a.add(a); const b = new Set(); b.add(b); expect(iterableEquality(a, b)).toBe(true); }); test('returns true when given nested Sets', () => { expect( iterableEquality( new Set([new Set([[1]]), new Set([[2]])]), new Set([new Set([[2]]), new Set([[1]])]), ), ).toBe(true); expect( iterableEquality( new Set([new Set([[1]]), new Set([[2]])]), new Set([new Set([[3]]), new Set([[1]])]), ), ).toBe(false); }); test('returns false when given inequal set within a set', () => { expect( iterableEquality(new Set([new Set([2])]), new Set([new Set([1, 2])])), ).toBe(false); expect( iterableEquality(new Set([new Set([2])]), new Set([new Set([1, 2])])), ).toBe(false); }); test('returns false when given inequal map within a set', () => { expect( iterableEquality( new Set([new Map([['a', 2]])]), new Set([new Map([['a', 3]])]), ), ).toBe(false); expect( iterableEquality(new Set([new Set([2])]), new Set([new Set([1, 2])])), ).toBe(false); }); test('returns false when given inequal set within a map', () => { expect( iterableEquality( new Map([['one', new Set([2])]]), new Map([['one', new Set([1, 2])]]), ), ).toBe(false); }); test('returns true when given iterator within equal objects', () => { const a = { [Symbol.iterator]: () => ({next: () => ({done: true})}), a: [], }; const b = { [Symbol.iterator]: () => ({next: () => ({done: true})}), a: [], }; expect(iterableEquality(a, b)).toBe(true); }); test('returns false when given iterator within inequal objects', () => { const a = { [Symbol.iterator]: () => ({next: () => ({done: true})}), a: [1], }; const b = { [Symbol.iterator]: () => ({next: () => ({done: true})}), a: [], }; expect(iterableEquality(a, b)).toBe(false); }); test('returns false when given iterator within inequal nested objects', () => { const a = { [Symbol.iterator]: () => ({next: () => ({done: true})}), a: { b: [1], }, }; const b = { [Symbol.iterator]: () => ({next: () => ({done: true})}), a: { b: [], }, }; expect(iterableEquality(a, b)).toBe(false); }); test('returns true when given circular Set shape', () => { const a1 = new Set(); const a2 = new Set(); a1.add(a2); a2.add(a1); const b = new Set(); b.add(b); expect(iterableEquality(a1, b)).toBe(true); }); test('returns true when given circular key in Map', () => { const a = new Map(); a.set(a, 'a'); const b = new Map(); b.set(b, 'a'); expect(iterableEquality(a, b)).toBe(true); }); test('returns true when given nested Maps', () => { expect( iterableEquality( new Map([['hello', new Map([['world', 'foobar']])]]), new Map([['hello', new Map([['world', 'qux']])]]), ), ).toBe(false); expect( iterableEquality( new Map([['hello', new Map([['world', 'foobar']])]]), new Map([['hello', new Map([['world', 'foobar']])]]), ), ).toBe(true); }); test('returns true when given circular key and value in Map', () => { const a = new Map(); a.set(a, a); const b = new Map(); b.set(b, b); expect(iterableEquality(a, b)).toBe(true); }); test('returns true when given circular value in Map', () => { const a = new Map(); a.set('a', a); const b = new Map(); b.set('a', b); expect(iterableEquality(a, b)).toBe(true); }); test('returns true when given Immutable Lists without an OwnerID', () => { const a = List([1, 2, 3]); const b = a.filter(v => v > 0); expect(iterableEquality(a, b)).toBe(true); }); test('returns true when given Immutable OrderedMaps without an OwnerID', () => { const a = OrderedMap().set('saving', true); const b = OrderedMap().merge({saving: true}); expect(iterableEquality(a, b)).toBe(true); }); test('returns true when given Immutable OrderedSets without an OwnerID', () => { const a = OrderedSet().add('newValue'); const b = List(['newValue']).toOrderedSet(); expect(iterableEquality(a, b)).toBe(true); }); test('returns true when given Immutable Record without an OwnerID', () => { class TestRecord extends Record({dummy: ''}) {} const a = new TestRecord().merge({dummy: 'data'}); const b = new TestRecord().set('dummy', 'data'); expect(iterableEquality(a, b)).toBe(true); }); test('returns true when given a symbols keys within equal objects', () => { const KEY = Symbol(); const a = { [Symbol.iterator]: () => ({next: () => ({done: true})}), [KEY]: [], }; const b = { [Symbol.iterator]: () => ({next: () => ({done: true})}), [KEY]: [], }; expect(iterableEquality(a, b)).toBe(true); }); test('returns false when given a symbols keys within inequal objects', () => { const KEY = Symbol(); const a = { [Symbol.iterator]: () => ({next: () => ({done: true})}), [KEY]: [1], }; const b = { [Symbol.iterator]: () => ({next: () => ({done: true})}), [KEY]: [], }; expect(iterableEquality(a, b)).toBe(false); }); test('does not throw when iterating an object with a TypedArray iterator', () => { const badIterable = { [Symbol.iterator]: Uint8Array.prototype[Symbol.iterator], }; expect(() => iterableEquality(badIterable, badIterable)).not.toThrow(); // Returns undefined so equals() can fall through to Object.is / property checks. expect(iterableEquality(badIterable, badIterable)).toBeUndefined(); // Same reference must still be considered equal via equals(). expect(equals(badIterable, badIterable, [iterableEquality])).toBe(true); }); }); describe('typeEquality', () => { test('returns undefined if given mock.calls and []', () => { expect(typeEquality(jest.fn().mock.calls, [])).toBeUndefined(); }); test('returns undefined for cross-realm plain objects', () => { const context = createContext({}); const otherRealmObject = runInContext('({key: "test"})', context); expect(typeEquality(otherRealmObject, {key: 'test'})).toBeUndefined(); }); test('returns undefined for cross-realm Maps', () => { const context = createContext({}); const otherRealmMap = runInContext('new Map([["key", "value"]])', context); expect( typeEquality(otherRealmMap, new Map([['key', 'value']])), ).toBeUndefined(); }); test('returns undefined for cross-realm Sets', () => { const context = createContext({}); const otherRealmSet = runInContext('new Set(["test"])', context); expect(typeEquality(otherRealmSet, new Set(['test']))).toBeUndefined(); }); test('returns undefined for cross-realm Dates', () => { const context = createContext({}); const otherRealmDate = runInContext('new Date("2023-01-01")', context); expect( typeEquality(otherRealmDate, new Date('2023-01-01')), ).toBeUndefined(); }); test('returns undefined for cross-realm RegExps', () => { const context = createContext({}); const otherRealmRegExp = runInContext('/test/gi', context); expect(typeEquality(otherRealmRegExp, /test/gi)).toBeUndefined(); }); test('returns false for different user-defined classes with same name', () => { class Child { a: number; constructor(a: number) { this.a = a; } } const OtherChild = class Child { a: number; constructor(a: number) { this.a = a; } }; expect(typeEquality(new Child(1), new OtherChild(1))).toBe(false); }); }); describe('iterableEquality (cross-realm)', () => { test('returns true for cross-realm Sets with same values', () => { const context = createContext({}); const otherRealmSet = runInContext('new Set([1, 2, 3])', context); expect(iterableEquality(otherRealmSet, new Set([1, 2, 3]))).toBe(true); }); test('returns true for cross-realm Maps with same entries', () => { const context = createContext({}); const otherRealmMap = runInContext( 'new Map([["a", 1], ["b", 2]])', context, ); expect( iterableEquality( otherRealmMap, new Map([ ['a', 1], ['b', 2], ]), ), ).toBe(true); }); test('returns false for cross-realm Sets with different values', () => { const context = createContext({}); const otherRealmSet = runInContext('new Set([1, 2])', context); expect(iterableEquality(otherRealmSet, new Set([1, 2, 3]))).toBe(false); }); }); describe('arrayBufferEquality', () => { test('returns undefined if given a non instance of ArrayBuffer', () => { expect(arrayBufferEquality(2, 's')).toBeUndefined(); expect(arrayBufferEquality(undefined, 2)).toBeUndefined(); expect(arrayBufferEquality(new Date(), new ArrayBuffer(2))).toBeUndefined(); }); test('returns false when given non-matching buffers', () => { const a = Uint8Array.from([2, 4]).buffer; const b = Uint16Array.from([1, 7]).buffer; expect(arrayBufferEquality(a, b)).toBe(false); }); test('returns false when given matching buffers of different byte length', () => { const a = Uint8Array.from([1, 2]).buffer; const b = Uint16Array.from([1, 2]).buffer; expect(arrayBufferEquality(a, b)).toBe(false); }); test('returns true when given matching buffers', () => { const a = Uint8Array.from([1, 2]).buffer; const b = Uint8Array.from([1, 2]).buffer; expect(arrayBufferEquality(a, b)).toBe(true); }); test('returns true when given matching DataView', () => { const a = new DataView(Uint8Array.from([1, 2, 3]).buffer); const b = new DataView(Uint8Array.from([1, 2, 3]).buffer); expect(arrayBufferEquality(a, b)).toBe(true); }); test('returns false when given non-matching DataView', () => { const a = new DataView(Uint8Array.from([1, 2, 3]).buffer); const b = new DataView(Uint8Array.from([3, 2, 1]).buffer); expect(arrayBufferEquality(a, b)).toBe(false); }); test('returns true when given matching Float64Array', () => { const a = Float64Array.from(Array.from({length: 10})); const b = Float64Array.from(Array.from({length: 10})); expect(arrayBufferEquality(a, b)).toBe(true); }); test('returns false when given non-matching Float64Array', () => { const a = Float64Array.from(Array.from({length: 10})); const b = Float64Array.from(Array.from({length: 100})); expect(arrayBufferEquality(a, b)).toBe(false); }); test('returns true when given matching URL', () => { const a = new URL('https://jestjs.io/'); const b = new URL('https://jestjs.io/'); expect(equals(a, b)).toBe(true); }); test('returns false when given non-matching URL', () => { const a = new URL('https://jestjs.io/docs/getting-started'); const b = new URL('https://jestjs.io/docs/getting-started#using-babel'); expect(equals(a, b)).toBe(false); }); }); describe('jasmineUtils primitives comparison', () => { const falseCases: Array<[any, any]> = [ [null, undefined], [null, 0], [false, 0], [false, ''], ]; for (const [a, b] of falseCases) { test(`${JSON.stringify(a)} and ${JSON.stringify(b)} returns false`, () => { expect(equals(a, b)).toBe(false); }); } const trueCases: Array<any> = [null, 0, false, '', undefined]; for (const value of trueCases) { test(`${JSON.stringify(value)} returns true`, () => { expect(equals(value, value)).toBe(true); }); } });