diff --git a/CHANGELOG.md b/CHANGELOG.md index 828a9175cba3..990180184b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - `[@jest/types]` Mark deprecated configuration options as `@deprecated` ([#11913](https://github.com/facebook/jest/pull/11913)) - `[jest-cli]` Improve `--help` printout by removing defunct `--browser` option ([#11914](https://github.com/facebook/jest/pull/11914)) - `[jest-haste-map]` Use distinct cache paths for different values of `computeDependencies` ([#11916](https://github.com/facebook/jest/pull/11916)) +- `[jest-util]` Install fix for `instanceof` as part of other globals into test environments ([#5995](https://github.com/facebook/jest/pull/5995)) ### Chore & Maintenance diff --git a/e2e/__tests__/instanceof.test.ts b/e2e/__tests__/instanceof.test.ts new file mode 100644 index 000000000000..3f191b00bf82 --- /dev/null +++ b/e2e/__tests__/instanceof.test.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import runJest from '../runJest'; + +test('suite with `instanceof` checks', () => { + const {exitCode} = runJest('instanceof'); + + expect(exitCode).toBe(0); +}); diff --git a/e2e/instanceof/__tests__/test.js b/e2e/instanceof/__tests__/test.js new file mode 100644 index 000000000000..fbc6d976e596 --- /dev/null +++ b/e2e/instanceof/__tests__/test.js @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +const fs = require('fs'); +const http = require('http'); +const util = require('util'); +const v8 = require('v8'); + +const readFile = util.promisify(fs.readFile); + +function getSuperClass(cls) { + const prototype = Object.getPrototypeOf(cls.prototype); + return prototype ? prototype.constructor : null; +} + +const buffers = fs.readdirSync(__dirname); +const buffer = fs.readFileSync(__filename); +const error = (() => { + try { + fs.readFileSync('/'); + } catch (e) { + return e; + } +})(); +const promise = readFile(__filename); + +const nodeArrayType = buffers.constructor; +const nodeErrorType = error.constructor; +const nodePromiseType = promise.constructor; +const nodeUint8ArrayType = getSuperClass(buffer.constructor); +const nodeTypedArrayType = getSuperClass(nodeUint8ArrayType); +const nodeObjectType = getSuperClass(nodeTypedArrayType); + +const globalTypedArrayType = getSuperClass(Uint8Array); + +test('fs Error', () => { + expect.hasAssertions(); + + try { + fs.readFileSync('does not exist'); + } catch (err) { + expect(err).toBeInstanceOf(Error); + } +}); + +test('http error', done => { + const request = http.request('http://does-not-exist/blah', res => { + console.log(`STATUS: ${res.statusCode}`); + res.on('end', () => { + done(new Error('Ended before failure')); + }); + }); + + request.once('error', err => { + expect(err).toBeInstanceOf(Error); + done(); + }); +}); + +test('Array', () => { + expect([]).toBeInstanceOf(Array); +}); + +test('array type', () => { + expect(buffers).toBeInstanceOf(Array); + expect(buffers).toBeInstanceOf(Object); + expect([]).toBeInstanceOf(nodeArrayType); + expect([]).toBeInstanceOf(nodeObjectType); +}); + +test('error type', () => { + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(Object); + expect(new Error()).toBeInstanceOf(nodeErrorType); + expect(new Error()).toBeInstanceOf(nodeObjectType); +}); + +test('promise type', () => { + expect(promise).toBeInstanceOf(Promise); + expect(promise).toBeInstanceOf(Object); + expect(new Promise(resolve => resolve())).toBeInstanceOf(nodePromiseType); + expect(new Promise(resolve => resolve())).toBeInstanceOf(nodeObjectType); +}); + +test('Uint8Array type', () => { + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer).toBeInstanceOf(Uint8Array); + expect(buffer).toBeInstanceOf(globalTypedArrayType); + expect(buffer).toBeInstanceOf(Object); + expect(new Uint8Array([])).toBeInstanceOf(nodeUint8ArrayType); + expect(new Uint8Array([])).toBeInstanceOf(nodeTypedArrayType); + expect(new Uint8Array([])).toBeInstanceOf(nodeObjectType); +}); + +test('recognizes typed arrays as objects', () => { + expect(new Uint8Array([1, 2, 3])).toBeInstanceOf(Object); + expect(new Uint8ClampedArray([1, 2, 3])).toBeInstanceOf(Object); + expect(new Uint16Array([1, 2, 3])).toBeInstanceOf(Object); + expect(new Uint32Array([1, 2, 3])).toBeInstanceOf(Object); + expect(new BigUint64Array([])).toBeInstanceOf(Object); + expect(new Int8Array([1, 2, 3])).toBeInstanceOf(Object); + expect(new Int16Array([1, 2, 3])).toBeInstanceOf(Object); + expect(new Int32Array([1, 2, 3])).toBeInstanceOf(Object); + expect(new BigInt64Array([])).toBeInstanceOf(Object); + expect(new Float32Array([1, 2, 3])).toBeInstanceOf(Object); + expect(new Float64Array([1, 2, 3])).toBeInstanceOf(Object); +}); + +test('recognizes typed arrays as instances of TypedArray', () => { + expect(new Uint8Array([1, 2, 3])).toBeInstanceOf(globalTypedArrayType); + expect(new Uint8ClampedArray([1, 2, 3])).toBeInstanceOf(globalTypedArrayType); + expect(new Uint16Array([1, 2, 3])).toBeInstanceOf(globalTypedArrayType); + expect(new Uint32Array([1, 2, 3])).toBeInstanceOf(globalTypedArrayType); + expect(new BigUint64Array([])).toBeInstanceOf(globalTypedArrayType); + expect(new Int8Array([1, 2, 3])).toBeInstanceOf(globalTypedArrayType); + expect(new Int16Array([1, 2, 3])).toBeInstanceOf(globalTypedArrayType); + expect(new Int32Array([1, 2, 3])).toBeInstanceOf(globalTypedArrayType); + expect(new BigInt64Array([])).toBeInstanceOf(globalTypedArrayType); + expect(new Float32Array([1, 2, 3])).toBeInstanceOf(globalTypedArrayType); + expect(new Float64Array([1, 2, 3])).toBeInstanceOf(globalTypedArrayType); +}); + +test('v8 serialize/deserialize', () => { + const m1 = new Map(); + const m2 = v8.deserialize(v8.serialize(m1)); + expect(m1).toEqual(m2); +}); diff --git a/e2e/instanceof/package.json b/e2e/instanceof/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/instanceof/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/packages/jest-util/src/__tests__/addInstanceOfAlias.test.ts b/packages/jest-util/src/__tests__/addInstanceOfAlias.test.ts new file mode 100644 index 000000000000..82b414c85158 --- /dev/null +++ b/packages/jest-util/src/__tests__/addInstanceOfAlias.test.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import addInstanceOfAlias from '../addInstanceOfAlias'; + +describe('addInstanceOfAlias', () => { + let Color; + let Purple; + let OtherPurple; + let Black; + beforeEach(() => { + Color = class Color {}; + Purple = class Purple extends Color {}; + OtherPurple = class OtherPurple extends Color {}; + Black = class Black extends Color {}; + }); + + it('adds the given alias to the target as an object to include in an instanceof call', () => { + const purple = new Purple(); + expect(purple instanceof OtherPurple).toBe(false); + addInstanceOfAlias(OtherPurple, Purple); + expect(purple instanceof OtherPurple).toBe(true); + }); + + it('preserves the prior instanceof lookup', () => { + const purple = new Purple(); + addInstanceOfAlias(OtherPurple, Purple); + expect(purple instanceof Purple).toBe(true); + expect(purple instanceof Color).toBe(true); + expect(purple instanceof Black).toBe(false); + }); + + describe('when using the same value for the target and alias', () => { + it('throws an error', () => { + expect(() => { + addInstanceOfAlias(Purple, Purple); + }).toThrow(Error); + }); + }); +}); diff --git a/packages/jest-util/src/addInstanceOfAlias.ts b/packages/jest-util/src/addInstanceOfAlias.ts new file mode 100644 index 000000000000..fc833910fa9b --- /dev/null +++ b/packages/jest-util/src/addInstanceOfAlias.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// using `Function` is on purpose - it's exactly what we want in this case +// eslint-disable-next-line @typescript-eslint/ban-types +type InstanceOfThing = Function; + +export default function addInstanceOfAlias( + target: InstanceOfThing, + alias: InstanceOfThing, +): void { + if (target === alias) { + throw new Error( + 'Attempted to call addInstanceOfAlias with the same object for both ' + + 'the target and the alias. This will create an infinite loop in ' + + 'instanceof checks.', + ); + } + + const originalHasInstance = target[Symbol.hasInstance]; + Object.defineProperty(target, Symbol.hasInstance, { + configurable: true, + value: function aliasedHasInstance(potentialInstance: unknown) { + return ( + potentialInstance instanceof alias || + originalHasInstance.call(this, potentialInstance) + ); + }, + writable: true, + }); +} diff --git a/packages/jest-util/src/installCommonGlobals.ts b/packages/jest-util/src/installCommonGlobals.ts index 5931f672e3e8..944575d4a39b 100644 --- a/packages/jest-util/src/installCommonGlobals.ts +++ b/packages/jest-util/src/installCommonGlobals.ts @@ -7,6 +7,7 @@ import * as fs from 'graceful-fs'; import type {Config} from '@jest/types'; +import addInstanceOfAlias from './addInstanceOfAlias'; import createProcessObject from './createProcessObject'; import deepCyclicCopy from './deepCyclicCopy'; @@ -62,5 +63,10 @@ export default function ( }; }); + addInstanceOfAlias(globalObject.Error, Error); + addInstanceOfAlias(globalObject.Promise, Promise); + addInstanceOfAlias(globalObject.Object, Object); + addInstanceOfAlias(globalObject.Function, Function); + return Object.assign(globalObject, deepCyclicCopy(globals)); }