From 39b3ed76a196d7c642af025e66f242b26e02e4d0 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 15 Aug 2020 20:56:27 -0700 Subject: [PATCH 01/11] === BEGIN jest-globals-2 === Upgrade chalk to reduce dupes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c7ee3c..d9d21a0 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@rollup/plugin-node-resolve": "^8.4.0", "babel-loader": "^8.1.0", "babel-plugin-istanbul": "^6.0.0", - "chalk": "^2.3.0", + "chalk": "^4.1.0", "core-js": "^3.6.5", "dlv": "^1.1.3", "errorstacks": "^1.3.0", From 6d3e4c42184203dd8d4ff6723e447d0040685d5d Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 15 Aug 2020 19:51:23 -0700 Subject: [PATCH 02/11] Add --skip-install flag to e2e script --- scripts/run-e2e-tests.mjs | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/scripts/run-e2e-tests.mjs b/scripts/run-e2e-tests.mjs index 7bc6456..0d5ac22 100644 --- a/scripts/run-e2e-tests.mjs +++ b/scripts/run-e2e-tests.mjs @@ -177,9 +177,10 @@ async function npmInstall(cwd, prefix) { /** * @param {string} projectPath * @param {string} prefix + * @param {Config} config * @returns {Promise<() => Promise>} */ -async function setupTests(projectPath, prefix) { +async function setupTests(projectPath, prefix, config) { const name = path.basename(projectPath); const log = (...msgs) => console.log(`${info(prefix)}`, ...msgs); @@ -204,7 +205,9 @@ async function setupTests(projectPath, prefix) { await fs.writeFile(pkgJsonPath, newContents, 'utf8'); } - await npmInstall(projectPath, prefix); + if (!config.skipInstall) { + await npmInstall(projectPath, prefix); + } return async () => { let cmd, args, opts; @@ -235,10 +238,25 @@ async function setupTests(projectPath, prefix) { }; } +/** + * @typedef Config + * @property {boolean} skipInstall + */ +const defaultConfig = { + skipInstall: false, +}; + /** * @param {string[]} args */ async function main(args) { + /** + * + */ + const config = { + ...defaultConfig, + }; + if (args.includes('--help')) { console.log( `\nRun Karmatic E2E Tests.\n\n` + @@ -249,6 +267,11 @@ async function main(args) { return; } + if (args.includes('--skip-install')) { + config.skipInstall = true; + args.splice(args.indexOf('--skip-install'), 1); + } + process.on('exit', (code) => { if (code !== 0) { console.log( @@ -280,7 +303,9 @@ async function main(args) { // installing using symlinks let runners = []; for (let project of projects) { - runners.push(await setupTests(e2eRoot(project), getPrefix(project))); + runners.push( + await setupTests(e2eRoot(project), getPrefix(project), config) + ); } console.log('Running karmatic...'); From 8c4f7c5b4101ebbe1cbe578da554809de33ca8ac Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 14 Aug 2020 22:14:59 -0700 Subject: [PATCH 03/11] Build jest-globals.js separately to target web; add test to verify it works --- e2e-test/webpack-default/test/jest-style.test.js | 6 ++++++ package.json | 2 +- src/configure.js | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/e2e-test/webpack-default/test/jest-style.test.js b/e2e-test/webpack-default/test/jest-style.test.js index 7292d79..9cc0459 100644 --- a/e2e-test/webpack-default/test/jest-style.test.js +++ b/e2e-test/webpack-default/test/jest-style.test.js @@ -6,4 +6,10 @@ describe('jest-style', () => { expect('How are you?').toEqual(expect.not.stringContaining(expected)); }); }); + + describe('jest.fn', () => { + it('exists', () => { + expect(typeof jest).toBe('object'); + }); + }); }); diff --git a/package.json b/package.json index d9d21a0..d79bb49 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "bin": "dist/cli.js", "scripts": { "prepare": "npm t", - "build": "microbundle --target node -f cjs --no-compress src/index.js src/cli.js src/appender.js", + "build": "microbundle --target node -f cjs --no-compress src/index.js src/cli.js src/appender.js && microbundle -f cjs --no-compress -i src/lib/jest-globals.js -o dist/lib/jest-globals.js", "test:build": "cd e2e-test/webpack-default && npm test", "test:watch": "cd e2e-test/webpack-default && npm run test:watch", "test:e2e": "node ./scripts/run-e2e-tests.mjs", diff --git a/src/configure.js b/src/configure.js index d6400a2..cf3bc95 100644 --- a/src/configure.js +++ b/src/configure.js @@ -121,6 +121,8 @@ export default async function configure(options) { const flags = ['--no-sandbox']; + const jestGlobalsPath = path.resolve(__dirname, './lib/jest-globals.js'); + let generatedConfig = { basePath: cwd, plugins: PLUGINS.map((req) => require.resolve(req)), @@ -185,7 +187,7 @@ export default async function configure(options) { served: true, }, { - pattern: path.resolve(__dirname, './lib/jest-globals.js'), + pattern: jestGlobalsPath, watched: false, included: true, served: true, @@ -209,6 +211,7 @@ export default async function configure(options) { ), preprocessors: { + [jestGlobalsPath]: preprocessors, [rootFiles + '/**/*']: preprocessors, [rootFiles]: preprocessors, }, From ccc5549d25ced2ada6071e33ccc7412d08bcb07c Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 14 Aug 2020 22:30:17 -0700 Subject: [PATCH 04/11] Expose expect through jest globals file --- src/configure.js | 10 ---------- src/lib/jest-globals.js | 4 ++++ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/configure.js b/src/configure.js index cf3bc95..6db60eb 100644 --- a/src/configure.js +++ b/src/configure.js @@ -176,16 +176,6 @@ export default async function configure(options) { ], files: [ - // Inject Jest matchers: - { - pattern: path.resolve( - __dirname, - '../node_modules/expect/build-es5/index.js' - ), - watched: false, - included: true, - served: true, - }, { pattern: jestGlobalsPath, watched: false, diff --git a/src/lib/jest-globals.js b/src/lib/jest-globals.js index efe9ef7..657f696 100644 --- a/src/lib/jest-globals.js +++ b/src/lib/jest-globals.js @@ -1,7 +1,11 @@ +import expect from 'expect'; + function notImplemented() { throw Error(`Not Implemented`); } +global.expect = expect; + // @todo expect.extend() et al global.jest = { From 76d55de37df8ce810fe7761ef201c01b45cef6ab Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 15 Aug 2020 19:52:49 -0700 Subject: [PATCH 05/11] Upgrade expect; bundle jest globals using microbundle --- package.json | 6 ++- src/configure.js | 2 +- src/lib/jest-globals.js | 4 +- src/lib/jest/messageUtilFake.js | 83 +++++++++++++++++++++++++++++++++ src/lib/jest/nodeJSGlobals.js | 10 ++++ 5 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 src/lib/jest/messageUtilFake.js create mode 100644 src/lib/jest/nodeJSGlobals.js diff --git a/package.json b/package.json index d79bb49..aee45c2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "bin": "dist/cli.js", "scripts": { "prepare": "npm t", - "build": "microbundle --target node -f cjs --no-compress src/index.js src/cli.js src/appender.js && microbundle -f cjs --no-compress -i src/lib/jest-globals.js -o dist/lib/jest-globals.js", + "build": "npm run build:node && npm run build:web", + "build:node": "microbundle --target node -f cjs --no-compress src/index.js src/cli.js src/appender.js", + "build:web": "microbundle -f iife --no-compress --external none --alias jest-message-util=C:\\code\\github\\developit\\karmatic\\src\\lib\\jest\\messageUtilFake.js --define process.env.NODE_ENV=production -i src/lib/jest-globals.js -o dist/lib/jest-globals.js", "test:build": "cd e2e-test/webpack-default && npm test", "test:watch": "cd e2e-test/webpack-default && npm run test:watch", "test:e2e": "node ./scripts/run-e2e-tests.mjs", @@ -55,7 +57,7 @@ "core-js": "^3.6.5", "dlv": "^1.1.3", "errorstacks": "^1.3.0", - "expect": "^24.9.0", + "expect": "^26.4.0", "karma": "^5.1.1", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.3", diff --git a/src/configure.js b/src/configure.js index 6db60eb..9e02d17 100644 --- a/src/configure.js +++ b/src/configure.js @@ -201,7 +201,7 @@ export default async function configure(options) { ), preprocessors: { - [jestGlobalsPath]: preprocessors, + // [jestGlobalsPath]: preprocessors, [rootFiles + '/**/*']: preprocessors, [rootFiles]: preprocessors, }, diff --git a/src/lib/jest-globals.js b/src/lib/jest-globals.js index 657f696..b736cf3 100644 --- a/src/lib/jest-globals.js +++ b/src/lib/jest-globals.js @@ -1,9 +1,11 @@ +import './jest/nodeJSGlobals'; import expect from 'expect'; function notImplemented() { throw Error(`Not Implemented`); } +const global = window; global.expect = expect; // @todo expect.extend() et al @@ -52,7 +54,7 @@ global.jest = { }, isolateModules: notImplemented, mock: jasmine.createSpy, // @todo check - requireActual: require, + // requireActual: require, requireMock: notImplemented, resetAllMocks: notImplemented, resetModuleRegistry: notImplemented, diff --git a/src/lib/jest/messageUtilFake.js b/src/lib/jest/messageUtilFake.js new file mode 100644 index 0000000..9e1a65b --- /dev/null +++ b/src/lib/jest/messageUtilFake.js @@ -0,0 +1,83 @@ +// As of writing, the [jest-message-util] package has a dependency on graceful-fs +// to read file contents mentioned in the stack trace to produce code frames for +// errors. Since this module is running in the browser and not in Node, we'll +// mock out this module for now so `expect` (and other Jest packages) can run in +// the browser. Karmatic adds code frames when errors are reported from the +// browser to the Karma server which has file system access to add code frames. +// +// jest-message-util: +// https://npmfs.com/package/jest-message-util/26.3.0/package.json#L20 + +// Based on https://github.com/facebook/jest/blob/c9c8dba4dd8de34269bdb971173659399bcbfd55/packages/jest-message-util/src/index.ts + +/** + * @param {Error} error + * @returns {string} + */ +export function formatExecError(error) { + return error.stack; +} + +/** + * @param {string} stack + * @returns {string[]} + */ +export function getStackTraceLines(stack) { + return stack.split(/\n/); +} + +/** + * @param {string[]} lines + * @returns {Frame} + */ +export function getTopFrame(lines) { + throw new Error('Not implemented: messageUtilFake.js:getTopFrame'); +} + +/** + * @param {string} stack + * @returns {string} + */ +export function formatStackTrace(stack) { + return stack; +} + +export function formatResultsErrors() { + throw new Error('Not implemented: messageUtilsFake.js:formatResultsErrors'); +} + +const errorRegexp = /^Error:?\s*$/; + +/** @type {(str: string) => string} */ +const removeBlankErrorLine = (str) => + str + .split('\n') + // Lines saying just `Error:` are useless + .filter((line) => !errorRegexp.test(line)) + .join('\n') + .trimRight(); + +/** + * @param {string} content + * @returns {{ message: string; stack: string; }} + */ +export function separateMessageFromStack(content) { + if (!content) { + return { message: '', stack: '' }; + } + + // All lines up to what looks like a stack -- or if nothing looks like a stack + // (maybe it's a code frame instead), just the first non-empty line. + // If the error is a plain "Error:" instead of a SyntaxError or TypeError we + // remove the prefix from the message because it is generally not useful. + const messageMatch = content.match( + /^(?:Error: )?([\s\S]*?(?=\n\s*at\s.*:\d*:\d*)|\s*.*)([\s\S]*)$/ + ); + if (!messageMatch) { + // For typescript + throw new Error('If you hit this error, the regex above is buggy.'); + } + const message = removeBlankErrorLine(messageMatch[1]); + const stack = removeBlankErrorLine(messageMatch[2]); + return { message, stack }; +} diff --git a/src/lib/jest/nodeJSGlobals.js b/src/lib/jest/nodeJSGlobals.js new file mode 100644 index 0000000..01c4a35 --- /dev/null +++ b/src/lib/jest/nodeJSGlobals.js @@ -0,0 +1,10 @@ +// As of writing, the [jest-matcher-utils] package expects there to be a +// `Buffer` global available. It only uses its constructor, and doesn't +// instantiate or call any methods off of it. So for browsers, we are just gonna +// create a `Buffer` global that maps to a Uint8Array since that is the closest +// browser primitive that matches Buffer +// +// [jest-matcher-utils]: +// https://npmfs.com/package/jest-matcher-utils/26.4.0/build/deepCyclicCopyReplaceable.js#L16 + +window.Buffer = Uint8Array; From 90226657d7079114f442b64bc134c76ac618091e Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 15 Aug 2020 21:57:36 -0700 Subject: [PATCH 06/11] Bundle jest globals through webpack --- package.json | 2 +- src/configure.js | 2 +- src/lib/jest-globals.js | 4 ++-- src/lib/jest/nodeJSGlobals.js | 2 +- src/webpack.js | 3 +++ 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index aee45c2..c1bc41f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "prepare": "npm t", "build": "npm run build:node && npm run build:web", "build:node": "microbundle --target node -f cjs --no-compress src/index.js src/cli.js src/appender.js", - "build:web": "microbundle -f iife --no-compress --external none --alias jest-message-util=C:\\code\\github\\developit\\karmatic\\src\\lib\\jest\\messageUtilFake.js --define process.env.NODE_ENV=production -i src/lib/jest-globals.js -o dist/lib/jest-globals.js", + "build:web": "microbundle -f cjs --no-compress -i src/lib/jest-globals.js -o dist/lib/jest-globals.js", "test:build": "cd e2e-test/webpack-default && npm test", "test:watch": "cd e2e-test/webpack-default && npm run test:watch", "test:e2e": "node ./scripts/run-e2e-tests.mjs", diff --git a/src/configure.js b/src/configure.js index 9e02d17..6db60eb 100644 --- a/src/configure.js +++ b/src/configure.js @@ -201,7 +201,7 @@ export default async function configure(options) { ), preprocessors: { - // [jestGlobalsPath]: preprocessors, + [jestGlobalsPath]: preprocessors, [rootFiles + '/**/*']: preprocessors, [rootFiles]: preprocessors, }, diff --git a/src/lib/jest-globals.js b/src/lib/jest-globals.js index b736cf3..bd990f6 100644 --- a/src/lib/jest-globals.js +++ b/src/lib/jest-globals.js @@ -1,11 +1,11 @@ -import './jest/nodeJSGlobals'; +// import './jest/nodeJSGlobals'; import expect from 'expect'; function notImplemented() { throw Error(`Not Implemented`); } -const global = window; +// const global = window; global.expect = expect; // @todo expect.extend() et al diff --git a/src/lib/jest/nodeJSGlobals.js b/src/lib/jest/nodeJSGlobals.js index 01c4a35..a09d081 100644 --- a/src/lib/jest/nodeJSGlobals.js +++ b/src/lib/jest/nodeJSGlobals.js @@ -7,4 +7,4 @@ // [jest-matcher-utils]: // https://npmfs.com/package/jest-matcher-utils/26.4.0/build/deepCyclicCopyReplaceable.js#L16 -window.Buffer = Uint8Array; +// window.Buffer = Uint8Array; diff --git a/src/webpack.js b/src/webpack.js index d02f69d..9efe27e 100644 --- a/src/webpack.js +++ b/src/webpack.js @@ -114,6 +114,7 @@ export function addWebpackConfig(karmaConfig, pkg, options) { karmaConfig.plugins.push(require.resolve('karma-webpack')); + const messageUtilPath = path.join(__dirname, 'jest/messageUtilFake.js'); karmaConfig.webpack = { devtool: 'inline-source-map', // devtool: 'module-source-map', @@ -143,6 +144,7 @@ export function addWebpackConfig(karmaConfig, pkg, options) { alias: webpackProp('resolve.alias', { [pkg.name]: res('.'), src: res('src'), + 'jest-message-util': messageUtilPath, }), }), resolveLoader: webpackProp('resolveLoader', { @@ -153,6 +155,7 @@ export function addWebpackConfig(karmaConfig, pkg, options) { alias: webpackProp('resolveLoader.alias', { [pkg.name]: res('.'), src: res('src'), + 'jest-message-util': messageUtilPath, }), }), plugins: (webpackConfig.plugins || []).filter((plugin) => { From 219a90a4fedcbc07eaab7185414a71d1a455a426 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 17 Aug 2020 10:53:39 -0700 Subject: [PATCH 07/11] Revert "Bundle jest globals through webpack" This reverts commit 795983120254bed5625ae6ce757d53ac989f09ea. --- package.json | 2 +- src/configure.js | 2 +- src/lib/jest-globals.js | 4 ++-- src/lib/jest/nodeJSGlobals.js | 2 +- src/webpack.js | 3 --- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index c1bc41f..aee45c2 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "prepare": "npm t", "build": "npm run build:node && npm run build:web", "build:node": "microbundle --target node -f cjs --no-compress src/index.js src/cli.js src/appender.js", - "build:web": "microbundle -f cjs --no-compress -i src/lib/jest-globals.js -o dist/lib/jest-globals.js", + "build:web": "microbundle -f iife --no-compress --external none --alias jest-message-util=C:\\code\\github\\developit\\karmatic\\src\\lib\\jest\\messageUtilFake.js --define process.env.NODE_ENV=production -i src/lib/jest-globals.js -o dist/lib/jest-globals.js", "test:build": "cd e2e-test/webpack-default && npm test", "test:watch": "cd e2e-test/webpack-default && npm run test:watch", "test:e2e": "node ./scripts/run-e2e-tests.mjs", diff --git a/src/configure.js b/src/configure.js index 6db60eb..9e02d17 100644 --- a/src/configure.js +++ b/src/configure.js @@ -201,7 +201,7 @@ export default async function configure(options) { ), preprocessors: { - [jestGlobalsPath]: preprocessors, + // [jestGlobalsPath]: preprocessors, [rootFiles + '/**/*']: preprocessors, [rootFiles]: preprocessors, }, diff --git a/src/lib/jest-globals.js b/src/lib/jest-globals.js index bd990f6..b736cf3 100644 --- a/src/lib/jest-globals.js +++ b/src/lib/jest-globals.js @@ -1,11 +1,11 @@ -// import './jest/nodeJSGlobals'; +import './jest/nodeJSGlobals'; import expect from 'expect'; function notImplemented() { throw Error(`Not Implemented`); } -// const global = window; +const global = window; global.expect = expect; // @todo expect.extend() et al diff --git a/src/lib/jest/nodeJSGlobals.js b/src/lib/jest/nodeJSGlobals.js index a09d081..01c4a35 100644 --- a/src/lib/jest/nodeJSGlobals.js +++ b/src/lib/jest/nodeJSGlobals.js @@ -7,4 +7,4 @@ // [jest-matcher-utils]: // https://npmfs.com/package/jest-matcher-utils/26.4.0/build/deepCyclicCopyReplaceable.js#L16 -// window.Buffer = Uint8Array; +window.Buffer = Uint8Array; diff --git a/src/webpack.js b/src/webpack.js index 9efe27e..d02f69d 100644 --- a/src/webpack.js +++ b/src/webpack.js @@ -114,7 +114,6 @@ export function addWebpackConfig(karmaConfig, pkg, options) { karmaConfig.plugins.push(require.resolve('karma-webpack')); - const messageUtilPath = path.join(__dirname, 'jest/messageUtilFake.js'); karmaConfig.webpack = { devtool: 'inline-source-map', // devtool: 'module-source-map', @@ -144,7 +143,6 @@ export function addWebpackConfig(karmaConfig, pkg, options) { alias: webpackProp('resolve.alias', { [pkg.name]: res('.'), src: res('src'), - 'jest-message-util': messageUtilPath, }), }), resolveLoader: webpackProp('resolveLoader', { @@ -155,7 +153,6 @@ export function addWebpackConfig(karmaConfig, pkg, options) { alias: webpackProp('resolveLoader.alias', { [pkg.name]: res('.'), src: res('src'), - 'jest-message-util': messageUtilPath, }), }), plugins: (webpackConfig.plugins || []).filter((plugin) => { From d7431ea857f52881d96af70c7450ad8a5f4571eb Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 14 Aug 2020 23:39:48 -0700 Subject: [PATCH 08/11] Add jest-mock with tests from Jests --- .../test/jest/moduleMocker.test.js | 1413 +++++++++++++++++ package.json | 1 + src/lib/jest-globals.js | 36 +- 3 files changed, 1441 insertions(+), 9 deletions(-) create mode 100644 e2e-test/webpack-default/test/jest/moduleMocker.test.js diff --git a/e2e-test/webpack-default/test/jest/moduleMocker.test.js b/e2e-test/webpack-default/test/jest/moduleMocker.test.js new file mode 100644 index 0000000..34fd4a0 --- /dev/null +++ b/e2e-test/webpack-default/test/jest/moduleMocker.test.js @@ -0,0 +1,1413 @@ +/* eslint-disable */ +// Disable eslint since this file is mostly directly copied from another source + +// Based on https://github.com/facebook/jest/blob/e8b7f57e05e3c785c18a91556dcbc7212826a573/packages/jest-mock/src/__tests__/index.test.ts +/** + * Original License: + * + * 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. + * + */ + +const vm = { + createContext() { + return window; + }, + runInNewContext(code, context) { + var f = new Function('runInNewContext_Code', `return eval(\`${code}\`)`); + return f.call(context); + }, + runInContext(code, context) { + return this.runInNewContext(code, context); + }, +}; + +describe('moduleMocker', () => { + let moduleMocker; + let mockContext; + let mockGlobals; + + beforeEach(() => { + mockContext = vm.createContext(); + mockGlobals = vm.runInNewContext('this', mockContext); + moduleMocker = new global.ModuleMocker(mockGlobals); + }); + + describe('getMetadata', () => { + it('returns the function `name` property', () => { + function x() {} + const metadata = moduleMocker.getMetadata(x); + expect(x.name).toBe('x'); + expect(metadata.name).toBe('x'); + }); + + it('mocks constant values', () => { + const metadata = moduleMocker.getMetadata(Symbol.for('bowties.are.cool')); + expect(metadata.value).toEqual(Symbol.for('bowties.are.cool')); + expect(moduleMocker.getMetadata('banana').value).toEqual('banana'); + expect(moduleMocker.getMetadata(27).value).toEqual(27); + expect(moduleMocker.getMetadata(false).value).toEqual(false); + expect(moduleMocker.getMetadata(Infinity).value).toEqual(Infinity); + }); + + it('does not retrieve metadata for arrays', () => { + const array = [1, 2, 3]; + const metadata = moduleMocker.getMetadata(array); + expect(metadata.value).toBeUndefined(); + expect(metadata.members).toBeUndefined(); + expect(metadata.type).toEqual('array'); + }); + + it('does not retrieve metadata for undefined', () => { + const metadata = moduleMocker.getMetadata(undefined); + expect(metadata.value).toBeUndefined(); + expect(metadata.members).toBeUndefined(); + expect(metadata.type).toEqual('undefined'); + }); + + it('does not retrieve metadata for null', () => { + const metadata = moduleMocker.getMetadata(null); + expect(metadata.value).toBeNull(); + expect(metadata.members).toBeUndefined(); + expect(metadata.type).toEqual('null'); + }); + + it('retrieves metadata for ES6 classes', () => { + class ClassFooMock { + bar() {} + } + const fooInstance = new ClassFooMock(); + const metadata = moduleMocker.getMetadata(fooInstance); + expect(metadata.type).toEqual('object'); + expect(metadata.members.constructor.name).toEqual('ClassFooMock'); + }); + + it('retrieves synchronous function metadata', () => { + function functionFooMock() {} + const metadata = moduleMocker.getMetadata(functionFooMock); + expect(metadata.type).toEqual('function'); + expect(metadata.name).toEqual('functionFooMock'); + }); + + it('retrieves asynchronous function metadata', () => { + async function asyncFunctionFooMock() {} + const metadata = moduleMocker.getMetadata(asyncFunctionFooMock); + expect(metadata.type).toEqual('function'); + expect(metadata.name).toEqual('asyncFunctionFooMock'); + }); + + it("retrieves metadata for object literals and it's members", () => { + const metadata = moduleMocker.getMetadata({ + bar: 'two', + foo: 1, + }); + expect(metadata.type).toEqual('object'); + expect(metadata.members.bar.value).toEqual('two'); + expect(metadata.members.bar.type).toEqual('constant'); + expect(metadata.members.foo.value).toEqual(1); + expect(metadata.members.foo.type).toEqual('constant'); + }); + + it('retrieves Date object metadata', () => { + const metadata = moduleMocker.getMetadata(Date); + expect(metadata.type).toEqual('function'); + expect(metadata.name).toEqual('Date'); + expect(metadata.members.now.name).toEqual('now'); + expect(metadata.members.parse.name).toEqual('parse'); + expect(metadata.members.UTC.name).toEqual('UTC'); + }); + }); + + describe('generateFromMetadata', () => { + it('forwards the function name property', () => { + function foo() {} + const mock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(foo) + ); + expect(mock.name).toBe('foo'); + }); + + it('fixes illegal function name properties', () => { + function getMockFnWithOriginalName(name) { + const fn = () => {}; + Object.defineProperty(fn, 'name', { value: name }); + + return moduleMocker.generateFromMetadata(moduleMocker.getMetadata(fn)); + } + + expect(getMockFnWithOriginalName('1').name).toBe('$1'); + expect(getMockFnWithOriginalName('foo-bar').name).toBe('foo$bar'); + expect(getMockFnWithOriginalName('foo-bar-2').name).toBe('foo$bar$2'); + expect(getMockFnWithOriginalName('foo-bar-3').name).toBe('foo$bar$3'); + expect(getMockFnWithOriginalName('foo/bar').name).toBe('foo$bar'); + expect(getMockFnWithOriginalName('foo𠮷bar').name).toBe('foo𠮷bar'); + }); + + it('special cases the mockConstructor name', () => { + function mockConstructor() {} + const mock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(mockConstructor) + ); + // Depends on node version + expect(!mock.name || mock.name === 'mockConstructor').toBeTruthy(); + }); + + it('wont interfere with previous mocks on a shared prototype', () => { + const ClassFoo = function() {}; + ClassFoo.prototype.x = () => {}; + const ClassFooMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(ClassFoo) + ); + const foo = new ClassFooMock(); + const bar = new ClassFooMock(); + + foo.x.mockImplementation(() => 'Foo'); + bar.x.mockImplementation(() => 'Bar'); + + expect(foo.x()).toBe('Foo'); + expect(bar.x()).toBe('Bar'); + }); + + it('does not mock non-enumerable getters', () => { + const foo = Object.defineProperties( + {}, + { + nonEnumGetter: { + get: () => { + throw new Error(); + }, + }, + nonEnumMethod: { + value: () => {}, + }, + } + ); + const mock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(foo) + ); + + expect(typeof foo.nonEnumMethod).toBe('function'); + + expect(mock.nonEnumMethod.mock).toBeDefined(); + expect(mock.nonEnumGetter).toBeUndefined(); + }); + + it('mocks getters of ES modules', () => { + const foo = Object.defineProperties( + {}, + { + __esModule: { + value: true, + }, + enumGetter: { + enumerable: true, + get: () => 10, + }, + } + ); + const mock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(foo) + ); + expect(mock.enumGetter).toBeDefined(); + }); + + it('mocks ES2015 non-enumerable methods', () => { + class ClassFoo { + foo() {} + toString() { + return 'Foo'; + } + } + + const ClassFooMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(ClassFoo) + ); + const foo = new ClassFooMock(); + + const instanceFoo = new ClassFoo(); + const instanceFooMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(instanceFoo) + ); + + expect(typeof foo.foo).toBe('function'); + expect(typeof instanceFooMock.foo).toBe('function'); + expect(instanceFooMock.foo.mock).toBeDefined(); + + expect(instanceFooMock.toString.mock).toBeDefined(); + }); + + it('mocks ES2015 non-enumerable static properties and methods', () => { + class ClassFoo {} + ClassFoo.foo = () => {}; + ClassFoo.fooProp = () => {}; + + class ClassBar extends ClassFoo {} + + const ClassBarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(ClassBar) + ); + + expect(typeof ClassBarMock.foo).toBe('function'); + expect(typeof ClassBarMock.fooProp).toBe('function'); + expect(ClassBarMock.foo.mock).toBeDefined(); + expect(ClassBarMock.fooProp.mock).toBeDefined(); + }); + + it('mocks methods in all the prototype chain (null prototype)', () => { + const Foo = Object.assign(Object.create(null), { foo() {} }); + const Bar = Object.assign(Object.create(Foo), { bar() {} }); + + const BarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(Bar) + ); + expect(typeof BarMock.foo).toBe('function'); + expect(typeof BarMock.bar).toBe('function'); + }); + + it('does not mock methods from Object.prototype', () => { + const Foo = { foo() {} }; + const Bar = Object.assign(Object.create(Foo), { bar() {} }); + + const BarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(Bar) + ); + + expect(BarMock).toBeInstanceOf(mockGlobals.Object); + expect( + Object.prototype.hasOwnProperty.call(BarMock, 'hasOwnProperty') + ).toBe(false); + expect(BarMock.hasOwnProperty).toBe( + mockGlobals.Object.prototype.hasOwnProperty + ); + }); + + it('does not mock methods from Object.prototype (in mock context)', () => { + const Bar = vm.runInContext( + ` + const Foo = { foo() {} }; + const Bar = Object.assign(Object.create(Foo), { bar() {} }); + Bar; + `, + mockContext + ); + + const BarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(Bar) + ); + + expect(BarMock).toBeInstanceOf(mockGlobals.Object); + expect( + Object.prototype.hasOwnProperty.call(BarMock, 'hasOwnProperty') + ).toBe(false); + expect(BarMock.hasOwnProperty).toBe( + mockGlobals.Object.prototype.hasOwnProperty + ); + }); + + it('does not mock methods from Function.prototype', () => { + class Foo {} + class Bar extends Foo {} + + const BarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(Bar) + ); + + expect(BarMock).toBeInstanceOf(mockGlobals.Function); + expect(Object.prototype.hasOwnProperty.call(BarMock, 'bind')).toBe(false); + expect(BarMock.bind).toBe(mockGlobals.Function.prototype.bind); + }); + + it('does not mock methods from Function.prototype (in mock context)', () => { + const Bar = vm.runInContext( + ` + class Foo {} + class Bar extends Foo {} + Bar; + `, + mockContext + ); + + const BarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(Bar) + ); + + expect(BarMock).toBeInstanceOf(mockGlobals.Function); + expect(Object.prototype.hasOwnProperty.call(BarMock, 'bind')).toBe(false); + expect(BarMock.bind).toBe(mockGlobals.Function.prototype.bind); + }); + + it('does not mock methods from RegExp.prototype', () => { + const bar = /bar/; + + const barMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(bar) + ); + + expect(barMock).toBeInstanceOf(mockGlobals.RegExp); + expect(Object.prototype.hasOwnProperty.call(barMock, 'test')).toBe(false); + expect(barMock.test).toBe(mockGlobals.RegExp.prototype.test); + }); + + it('does not mock methods from RegExp.prototype (in mock context)', () => { + const bar = vm.runInContext( + ` + const bar = /bar/; + bar; + `, + mockContext + ); + + const barMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(bar) + ); + + expect(barMock).toBeInstanceOf(mockGlobals.RegExp); + expect(Object.prototype.hasOwnProperty.call(barMock, 'test')).toBe(false); + expect(barMock.test).toBe(mockGlobals.RegExp.prototype.test); + }); + + it('mocks methods that are bound multiple times', () => { + const func = function func() {}; + const multipleBoundFunc = func.bind(null).bind(null); + + const multipleBoundFuncMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(multipleBoundFunc) + ); + + expect(typeof multipleBoundFuncMock).toBe('function'); + }); + + it('mocks methods that are bound after mocking', () => { + const fooMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(() => {}) + ); + + const barMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(fooMock.bind(null)) + ); + + expect(barMock).not.toThrow(); + }); + + it('mocks regexp instances', () => { + expect(() => + moduleMocker.generateFromMetadata(moduleMocker.getMetadata(/a/)) + ).not.toThrow(); + }); + + it('mocks functions with numeric names', () => { + const obj = { + 1: () => {}, + }; + + const objMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(obj) + ); + + expect(typeof objMock[1]).toBe('function'); + }); + + describe('mocked functions', () => { + it('tracks calls to mocks', () => { + const fn = moduleMocker.fn(); + expect(fn.mock.calls).toEqual([]); + + fn(1, 2, 3); + expect(fn.mock.calls).toEqual([[1, 2, 3]]); + + fn('a', 'b', 'c'); + expect(fn.mock.calls).toEqual([ + [1, 2, 3], + ['a', 'b', 'c'], + ]); + }); + + it('tracks instances made by mocks', () => { + const fn = moduleMocker.fn(); + expect(fn.mock.instances).toEqual([]); + + const instance1 = new fn(); + expect(fn.mock.instances[0]).toBe(instance1); + + const instance2 = new fn(); + expect(fn.mock.instances[1]).toBe(instance2); + }); + + it('supports clearing mock calls', () => { + const fn = moduleMocker.fn(); + expect(fn.mock.calls).toEqual([]); + + fn(1, 2, 3); + expect(fn.mock.calls).toEqual([[1, 2, 3]]); + + fn.mockReturnValue('abcd'); + + fn.mockClear(); + expect(fn.mock.calls).toEqual([]); + + fn('a', 'b', 'c'); + expect(fn.mock.calls).toEqual([['a', 'b', 'c']]); + + expect(fn()).toEqual('abcd'); + }); + + it('supports clearing mocks', () => { + const fn = moduleMocker.fn(); + expect(fn.mock.calls).toEqual([]); + + fn(1, 2, 3); + expect(fn.mock.calls).toEqual([[1, 2, 3]]); + + fn.mockClear(); + expect(fn.mock.calls).toEqual([]); + + fn('a', 'b', 'c'); + expect(fn.mock.calls).toEqual([['a', 'b', 'c']]); + }); + + it('supports clearing all mocks', () => { + const fn1 = moduleMocker.fn(); + fn1.mockImplementation(() => 'abcd'); + fn1(1, 2, 3); + expect(fn1.mock.calls).toEqual([[1, 2, 3]]); + + const fn2 = moduleMocker.fn(); + fn2.mockReturnValue('abcde'); + fn2('a', 'b', 'c', 'd'); + expect(fn2.mock.calls).toEqual([['a', 'b', 'c', 'd']]); + + moduleMocker.clearAllMocks(); + expect(fn1.mock.calls).toEqual([]); + expect(fn2.mock.calls).toEqual([]); + expect(fn1()).toEqual('abcd'); + expect(fn2()).toEqual('abcde'); + }); + + it('supports resetting mock return values', () => { + const fn = moduleMocker.fn(); + fn.mockReturnValue('abcd'); + + const before = fn(); + expect(before).toEqual('abcd'); + + fn.mockReset(); + + const after = fn(); + expect(after).not.toEqual('abcd'); + }); + + it('supports resetting single use mock return values', () => { + const fn = moduleMocker.fn(); + fn.mockReturnValueOnce('abcd'); + + fn.mockReset(); + + const after = fn(); + expect(after).not.toEqual('abcd'); + }); + + it('supports resetting mock implementations', () => { + const fn = moduleMocker.fn(); + fn.mockImplementation(() => 'abcd'); + + const before = fn(); + expect(before).toEqual('abcd'); + + fn.mockReset(); + + const after = fn(); + expect(after).not.toEqual('abcd'); + }); + + it('supports resetting single use mock implementations', () => { + const fn = moduleMocker.fn(); + fn.mockImplementationOnce(() => 'abcd'); + + fn.mockReset(); + + const after = fn(); + expect(after).not.toEqual('abcd'); + }); + + it('supports resetting all mocks', () => { + const fn1 = moduleMocker.fn(); + fn1.mockImplementation(() => 'abcd'); + fn1(1, 2, 3); + expect(fn1.mock.calls).toEqual([[1, 2, 3]]); + + const fn2 = moduleMocker.fn(); + fn2.mockReturnValue('abcd'); + fn2('a', 'b', 'c'); + expect(fn2.mock.calls).toEqual([['a', 'b', 'c']]); + + moduleMocker.resetAllMocks(); + expect(fn1.mock.calls).toEqual([]); + expect(fn2.mock.calls).toEqual([]); + expect(fn1()).not.toEqual('abcd'); + expect(fn2()).not.toEqual('abcd'); + }); + + it('maintains function arity', () => { + const mockFunctionArity1 = moduleMocker.fn((x) => x); + const mockFunctionArity2 = moduleMocker.fn((x, y) => y); + + expect(mockFunctionArity1.length).toBe(1); + expect(mockFunctionArity2.length).toBe(2); + }); + }); + + it('mocks the method in the passed object itself', () => { + const parent = { func: () => 'abcd' }; + const child = Object.create(parent); + + moduleMocker.spyOn(child, 'func').mockReturnValue('efgh'); + + expect(child.hasOwnProperty('func')).toBe(true); + expect(child.func()).toEqual('efgh'); + expect(parent.func()).toEqual('abcd'); + }); + + it('should delete previously inexistent methods when restoring', () => { + const parent = { func: () => 'abcd' }; + const child = Object.create(parent); + + moduleMocker.spyOn(child, 'func').mockReturnValue('efgh'); + + moduleMocker.restoreAllMocks(); + expect(child.func()).toEqual('abcd'); + + moduleMocker.spyOn(parent, 'func').mockReturnValue('jklm'); + + expect(child.hasOwnProperty('func')).toBe(false); + expect(child.func()).toEqual('jklm'); + }); + + it('supports mock value returning undefined', () => { + const obj = { + func: () => 'some text', + }; + + moduleMocker.spyOn(obj, 'func').mockReturnValue(undefined); + + expect(obj.func()).not.toEqual('some text'); + }); + + it('supports mock value once returning undefined', () => { + const obj = { + func: () => 'some text', + }; + + moduleMocker.spyOn(obj, 'func').mockReturnValueOnce(undefined); + + expect(obj.func()).not.toEqual('some text'); + }); + + it('mockReturnValueOnce mocks value just once', () => { + const fake = jest.fn((a) => a + 2); + fake.mockReturnValueOnce(42); + expect(fake(2)).toEqual(42); + expect(fake(2)).toEqual(4); + }); + + it('supports mocking resolvable async functions', () => { + const fn = moduleMocker.fn(); + fn.mockResolvedValue('abcd'); + + const promise = fn(); + + expect(promise).toBeInstanceOf(Promise); + + return expect(promise).resolves.toBe('abcd'); + }); + + it('supports mocking resolvable async functions only once', () => { + const fn = moduleMocker.fn(); + fn.mockResolvedValue('abcd'); + fn.mockResolvedValueOnce('abcde'); + + return Promise.all([ + expect(fn()).resolves.toBe('abcde'), + expect(fn()).resolves.toBe('abcd'), + ]); + }); + + it('supports mocking rejectable async functions', () => { + const err = new Error('rejected'); + const fn = moduleMocker.fn(); + fn.mockRejectedValue(err); + + const promise = fn(); + + expect(promise).toBeInstanceOf(Promise); + + return expect(promise).rejects.toBe(err); + }); + + it('supports mocking rejectable async functions only once', () => { + const defaultErr = new Error('default rejected'); + const err = new Error('rejected'); + const fn = moduleMocker.fn(); + fn.mockRejectedValue(defaultErr); + fn.mockRejectedValueOnce(err); + + return Promise.all([ + expect(fn()).rejects.toBe(err), + expect(fn()).rejects.toBe(defaultErr), + ]); + }); + + describe('return values', () => { + it('tracks return values', () => { + const fn = moduleMocker.fn((x) => x * 2); + + expect(fn.mock.results).toEqual([]); + + fn(1); + fn(2); + + expect(fn.mock.results).toEqual([ + { + type: 'return', + value: 2, + }, + { + type: 'return', + value: 4, + }, + ]); + }); + + it('tracks mocked return values', () => { + const fn = moduleMocker.fn((x) => x * 2); + fn.mockReturnValueOnce('MOCKED!'); + + fn(1); + fn(2); + + expect(fn.mock.results).toEqual([ + { + type: 'return', + value: 'MOCKED!', + }, + { + type: 'return', + value: 4, + }, + ]); + }); + + it('supports resetting return values', () => { + const fn = moduleMocker.fn((x) => x * 2); + + expect(fn.mock.results).toEqual([]); + + fn(1); + fn(2); + + expect(fn.mock.results).toEqual([ + { + type: 'return', + value: 2, + }, + { + type: 'return', + value: 4, + }, + ]); + + fn.mockReset(); + + expect(fn.mock.results).toEqual([]); + }); + }); + + it('tracks thrown errors without interfering with other tracking', () => { + const error = new Error('ODD!'); + const fn = moduleMocker.fn((x, y) => { + // multiply params + const result = x * y; + + if (result % 2 === 1) { + // throw error if result is odd + throw error; + } else { + return result; + } + }); + + expect(fn(2, 4)).toBe(8); + + // Mock still throws the error even though it was internally + // caught and recorded + expect(() => { + fn(3, 5); + }).toThrow('ODD!'); + + expect(fn(6, 3)).toBe(18); + + // All call args tracked + expect(fn.mock.calls).toEqual([ + [2, 4], + [3, 5], + [6, 3], + ]); + // Results are tracked + expect(fn.mock.results).toEqual([ + { + type: 'return', + value: 8, + }, + { + type: 'throw', + value: error, + }, + { + type: 'return', + value: 18, + }, + ]); + }); + + it(`a call that throws undefined is tracked properly`, () => { + const fn = moduleMocker.fn(() => { + // eslint-disable-next-line no-throw-literal + throw undefined; + }); + + try { + fn(2, 4); + } catch (error) { + // ignore error + } + + // All call args tracked + expect(fn.mock.calls).toEqual([[2, 4]]); + // Results are tracked + expect(fn.mock.results).toEqual([ + { + type: 'throw', + value: undefined, + }, + ]); + }); + + it('results of recursive calls are tracked properly', () => { + // sums up all integers from 0 -> value, using recursion + const fn = moduleMocker.fn((value) => { + if (value === 0) { + return 0; + } else { + return value + fn(value - 1); + } + }); + + fn(4); + + // All call args tracked + expect(fn.mock.calls).toEqual([[4], [3], [2], [1], [0]]); + // Results are tracked + // (in correct order of calls, rather than order of returns) + expect(fn.mock.results).toEqual([ + { + type: 'return', + value: 10, + }, + { + type: 'return', + value: 6, + }, + { + type: 'return', + value: 3, + }, + { + type: 'return', + value: 1, + }, + { + type: 'return', + value: 0, + }, + ]); + }); + + it('test results of recursive calls from within the recursive call', () => { + // sums up all integers from 0 -> value, using recursion + const fn = moduleMocker.fn((value) => { + if (value === 0) { + return 0; + } else { + const recursiveResult = fn(value - 1); + + if (value === 3) { + // All recursive calls have been made at this point. + expect(fn.mock.calls).toEqual([[4], [3], [2], [1], [0]]); + // But only the last 3 calls have returned at this point. + expect(fn.mock.results).toEqual([ + { + type: 'incomplete', + value: undefined, + }, + { + type: 'incomplete', + value: undefined, + }, + { + type: 'return', + value: 3, + }, + { + type: 'return', + value: 1, + }, + { + type: 'return', + value: 0, + }, + ]); + } + + return value + recursiveResult; + } + }); + + fn(4); + }); + + it('call mockClear inside recursive mock', () => { + // sums up all integers from 0 -> value, using recursion + const fn = moduleMocker.fn((value) => { + if (value === 3) { + fn.mockClear(); + } + + if (value === 0) { + return 0; + } else { + return value + fn(value - 1); + } + }); + + fn(3); + + // All call args (after the call that cleared the mock) are tracked + expect(fn.mock.calls).toEqual([[2], [1], [0]]); + // Results (after the call that cleared the mock) are tracked + expect(fn.mock.results).toEqual([ + { + type: 'return', + value: 3, + }, + { + type: 'return', + value: 1, + }, + { + type: 'return', + value: 0, + }, + ]); + }); + + describe('invocationCallOrder', () => { + it('tracks invocationCallOrder made by mocks', () => { + const fn1 = moduleMocker.fn(); + expect(fn1.mock.invocationCallOrder).toEqual([]); + + fn1(1, 2, 3); + expect(fn1.mock.invocationCallOrder[0]).toBe(1); + + fn1('a', 'b', 'c'); + expect(fn1.mock.invocationCallOrder[1]).toBe(2); + + fn1(1, 2, 3); + expect(fn1.mock.invocationCallOrder[2]).toBe(3); + + const fn2 = moduleMocker.fn(); + expect(fn2.mock.invocationCallOrder).toEqual([]); + + fn2('d', 'e', 'f'); + expect(fn2.mock.invocationCallOrder[0]).toBe(4); + + fn2(4, 5, 6); + expect(fn2.mock.invocationCallOrder[1]).toBe(5); + }); + + it('supports clearing mock invocationCallOrder', () => { + const fn = moduleMocker.fn(); + expect(fn.mock.invocationCallOrder).toEqual([]); + + fn(1, 2, 3); + expect(fn.mock.invocationCallOrder).toEqual([1]); + + fn.mockReturnValue('abcd'); + + fn.mockClear(); + expect(fn.mock.invocationCallOrder).toEqual([]); + + fn('a', 'b', 'c'); + expect(fn.mock.invocationCallOrder).toEqual([2]); + + expect(fn()).toEqual('abcd'); + }); + + it('supports clearing all mocks invocationCallOrder', () => { + const fn1 = moduleMocker.fn(); + fn1.mockImplementation(() => 'abcd'); + + fn1(1, 2, 3); + expect(fn1.mock.invocationCallOrder).toEqual([1]); + + const fn2 = moduleMocker.fn(); + + fn2.mockReturnValue('abcde'); + fn2('a', 'b', 'c', 'd'); + expect(fn2.mock.invocationCallOrder).toEqual([2]); + + moduleMocker.clearAllMocks(); + expect(fn1.mock.invocationCallOrder).toEqual([]); + expect(fn2.mock.invocationCallOrder).toEqual([]); + expect(fn1()).toEqual('abcd'); + expect(fn2()).toEqual('abcde'); + }); + + it('handles a property called `prototype`', () => { + const mock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata({ prototype: 1 }) + ); + + expect(mock.prototype).toBe(1); + }); + }); + }); + + describe('getMockImplementation', () => { + it('should mock calls to a mock function', () => { + const mockFn = moduleMocker.fn(); + + mockFn.mockImplementation(() => 'Foo'); + + expect(typeof mockFn.getMockImplementation()).toBe('function'); + expect(mockFn.getMockImplementation()()).toBe('Foo'); + }); + }); + + describe('mockImplementationOnce', () => { + it('should mock constructor', () => { + const mock1 = jest.fn(); + const mock2 = jest.fn(); + const Module = jest.fn(() => ({ someFn: mock1 })); + const testFn = function() { + const m = new Module(); + m.someFn(); + }; + + Module.mockImplementationOnce(() => ({ someFn: mock2 })); + + testFn(); + expect(mock2).toHaveBeenCalled(); + expect(mock1).not.toHaveBeenCalled(); + testFn(); + expect(mock1).toHaveBeenCalled(); + }); + + it('should mock single call to a mock function', () => { + const mockFn = moduleMocker.fn(); + + mockFn + .mockImplementationOnce(() => 'Foo') + .mockImplementationOnce(() => 'Bar'); + + expect(mockFn()).toBe('Foo'); + expect(mockFn()).toBe('Bar'); + expect(mockFn()).toBeUndefined(); + }); + + it('should fallback to default mock function when no specific mock is available', () => { + const mockFn = moduleMocker.fn(); + + mockFn + .mockImplementationOnce(() => 'Foo') + .mockImplementationOnce(() => 'Bar') + .mockImplementation(() => 'Default'); + + expect(mockFn()).toBe('Foo'); + expect(mockFn()).toBe('Bar'); + expect(mockFn()).toBe('Default'); + expect(mockFn()).toBe('Default'); + }); + }); + + test('mockReturnValue does not override mockImplementationOnce', () => { + const mockFn = jest + .fn() + .mockReturnValue(1) + .mockImplementationOnce(() => 2); + expect(mockFn()).toBe(2); + expect(mockFn()).toBe(1); + }); + + test('mockImplementation resets the mock', () => { + const fn = jest.fn(); + expect(fn()).toBeUndefined(); + fn.mockReturnValue('returnValue'); + fn.mockImplementation(() => 'foo'); + expect(fn()).toBe('foo'); + }); + + it('should recognize a mocked function', () => { + const mockFn = moduleMocker.fn(); + + expect(moduleMocker.isMockFunction(() => {})).toBe(false); + expect(moduleMocker.isMockFunction(mockFn)).toBe(true); + }); + + test('default mockName is jest.fn()', () => { + const fn = jest.fn(); + expect(fn.getMockName()).toBe('jest.fn()'); + }); + + test('mockName sets the mock name', () => { + const fn = jest.fn(); + fn.mockName('myMockFn'); + expect(fn.getMockName()).toBe('myMockFn'); + }); + + test('mockName gets reset by mockReset', () => { + const fn = jest.fn(); + expect(fn.getMockName()).toBe('jest.fn()'); + fn.mockName('myMockFn'); + expect(fn.getMockName()).toBe('myMockFn'); + fn.mockReset(); + expect(fn.getMockName()).toBe('jest.fn()'); + }); + + test('mockName gets reset by mockRestore', () => { + const fn = jest.fn(); + expect(fn.getMockName()).toBe('jest.fn()'); + fn.mockName('myMockFn'); + expect(fn.getMockName()).toBe('myMockFn'); + fn.mockRestore(); + expect(fn.getMockName()).toBe('jest.fn()'); + }); + + test('mockName is not reset by mockClear', () => { + const fn = jest.fn(() => false); + fn.mockName('myMockFn'); + expect(fn.getMockName()).toBe('myMockFn'); + fn.mockClear(); + expect(fn.getMockName()).toBe('myMockFn'); + }); + + describe('spyOn', () => { + it('should work', () => { + let isOriginalCalled = false; + let originalCallThis; + let originalCallArguments; + const obj = { + method() { + isOriginalCalled = true; + originalCallThis = this; + originalCallArguments = arguments; + }, + }; + + const spy = moduleMocker.spyOn(obj, 'method'); + + const thisArg = { this: true }; + const firstArg = { first: true }; + const secondArg = { second: true }; + obj.method.call(thisArg, firstArg, secondArg); + expect(isOriginalCalled).toBe(true); + expect(originalCallThis).toBe(thisArg); + expect(originalCallArguments.length).toBe(2); + expect(originalCallArguments[0]).toBe(firstArg); + expect(originalCallArguments[1]).toBe(secondArg); + expect(spy).toHaveBeenCalled(); + + isOriginalCalled = false; + originalCallThis = null; + originalCallArguments = null; + spy.mockRestore(); + obj.method.call(thisArg, firstArg, secondArg); + expect(isOriginalCalled).toBe(true); + expect(originalCallThis).toBe(thisArg); + expect(originalCallArguments.length).toBe(2); + expect(originalCallArguments[0]).toBe(firstArg); + expect(originalCallArguments[1]).toBe(secondArg); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should throw on invalid input', () => { + expect(() => { + moduleMocker.spyOn(null, 'method'); + }).toThrow(); + expect(() => { + moduleMocker.spyOn({}, 'method'); + }).toThrow(); + expect(() => { + moduleMocker.spyOn({ method: 10 }, 'method'); + }).toThrow(); + }); + + it('supports restoring all spies', () => { + let methodOneCalls = 0; + let methodTwoCalls = 0; + const obj = { + methodOne() { + methodOneCalls++; + }, + methodTwo() { + methodTwoCalls++; + }, + }; + + const spy1 = moduleMocker.spyOn(obj, 'methodOne'); + const spy2 = moduleMocker.spyOn(obj, 'methodTwo'); + + // First, we call with the spies: both spies and both original functions + // should be called. + obj.methodOne(); + obj.methodTwo(); + expect(methodOneCalls).toBe(1); + expect(methodTwoCalls).toBe(1); + expect(spy1.mock.calls.length).toBe(1); + expect(spy2.mock.calls.length).toBe(1); + + moduleMocker.restoreAllMocks(); + + // Then, after resetting all mocks, we call methods again. Only the real + // methods should bump their count, not the spies. + obj.methodOne(); + obj.methodTwo(); + expect(methodOneCalls).toBe(2); + expect(methodTwoCalls).toBe(2); + expect(spy1.mock.calls.length).toBe(1); + expect(spy2.mock.calls.length).toBe(1); + }); + }); + + describe('spyOnProperty', () => { + it('should work - getter', () => { + let isOriginalCalled = false; + let originalCallThis; + let originalCallArguments; + const obj = { + get method() { + return function() { + isOriginalCalled = true; + originalCallThis = this; + originalCallArguments = arguments; + }; + }, + }; + + const spy = moduleMocker.spyOn(obj, 'method', 'get'); + + const thisArg = { this: true }; + const firstArg = { first: true }; + const secondArg = { second: true }; + obj.method.call(thisArg, firstArg, secondArg); + expect(isOriginalCalled).toBe(true); + expect(originalCallThis).toBe(thisArg); + expect(originalCallArguments.length).toBe(2); + expect(originalCallArguments[0]).toBe(firstArg); + expect(originalCallArguments[1]).toBe(secondArg); + expect(spy).toHaveBeenCalled(); + + isOriginalCalled = false; + originalCallThis = null; + originalCallArguments = null; + spy.mockRestore(); + obj.method.call(thisArg, firstArg, secondArg); + expect(isOriginalCalled).toBe(true); + expect(originalCallThis).toBe(thisArg); + expect(originalCallArguments.length).toBe(2); + expect(originalCallArguments[0]).toBe(firstArg); + expect(originalCallArguments[1]).toBe(secondArg); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should work - setter', () => { + const obj = { + _property: false, + set property(value) { + this._property = value; + }, + get property() { + return this._property; + }, + }; + + const spy = moduleMocker.spyOn(obj, 'property', 'set'); + obj.property = true; + expect(spy).toHaveBeenCalled(); + expect(obj.property).toBe(true); + obj.property = false; + spy.mockRestore(); + obj.property = true; + expect(spy).not.toHaveBeenCalled(); + expect(obj.property).toBe(true); + }); + + it('should throw on invalid input', () => { + expect(() => { + moduleMocker.spyOn(null, 'method'); + }).toThrow(); + expect(() => { + moduleMocker.spyOn({}, 'method'); + }).toThrow(); + expect(() => { + moduleMocker.spyOn({ method: 10 }, 'method'); + }).toThrow(); + }); + + it('supports restoring all spies', () => { + let methodOneCalls = 0; + let methodTwoCalls = 0; + const obj = { + get methodOne() { + return function() { + methodOneCalls++; + }; + }, + get methodTwo() { + return function() { + methodTwoCalls++; + }; + }, + }; + + const spy1 = moduleMocker.spyOn(obj, 'methodOne', 'get'); + const spy2 = moduleMocker.spyOn(obj, 'methodTwo', 'get'); + + // First, we call with the spies: both spies and both original functions + // should be called. + obj.methodOne(); + obj.methodTwo(); + expect(methodOneCalls).toBe(1); + expect(methodTwoCalls).toBe(1); + expect(spy1.mock.calls.length).toBe(1); + expect(spy2.mock.calls.length).toBe(1); + + moduleMocker.restoreAllMocks(); + + // Then, after resetting all mocks, we call methods again. Only the real + // methods should bump their count, not the spies. + obj.methodOne(); + obj.methodTwo(); + expect(methodOneCalls).toBe(2); + expect(methodTwoCalls).toBe(2); + expect(spy1.mock.calls.length).toBe(1); + expect(spy2.mock.calls.length).toBe(1); + }); + + it('should work with getters on the prototype chain', () => { + let isOriginalCalled = false; + let originalCallThis; + let originalCallArguments; + const prototype = { + get method() { + return function() { + isOriginalCalled = true; + originalCallThis = this; + originalCallArguments = arguments; + }; + }, + }; + const obj = Object.create(prototype, {}); + + const spy = moduleMocker.spyOn(obj, 'method', 'get'); + + const thisArg = { this: true }; + const firstArg = { first: true }; + const secondArg = { second: true }; + obj.method.call(thisArg, firstArg, secondArg); + expect(isOriginalCalled).toBe(true); + expect(originalCallThis).toBe(thisArg); + expect(originalCallArguments.length).toBe(2); + expect(originalCallArguments[0]).toBe(firstArg); + expect(originalCallArguments[1]).toBe(secondArg); + expect(spy).toHaveBeenCalled(); + + isOriginalCalled = false; + originalCallThis = null; + originalCallArguments = null; + spy.mockRestore(); + obj.method.call(thisArg, firstArg, secondArg); + expect(isOriginalCalled).toBe(true); + expect(originalCallThis).toBe(thisArg); + expect(originalCallArguments.length).toBe(2); + expect(originalCallArguments[0]).toBe(firstArg); + expect(originalCallArguments[1]).toBe(secondArg); + expect(spy).not.toHaveBeenCalled(); + }); + + test('should work with setters on the prototype chain', () => { + const prototype = { + _property: false, + set property(value) { + this._property = value; + }, + get property() { + return this._property; + }, + }; + const obj = Object.create(prototype, {}); + + const spy = moduleMocker.spyOn(obj, 'property', 'set'); + obj.property = true; + expect(spy).toHaveBeenCalled(); + expect(obj.property).toBe(true); + obj.property = false; + spy.mockRestore(); + obj.property = true; + expect(spy).not.toHaveBeenCalled(); + expect(obj.property).toBe(true); + }); + + it('supports restoring all spies on the prototype chain', () => { + let methodOneCalls = 0; + let methodTwoCalls = 0; + const prototype = { + get methodOne() { + return function() { + methodOneCalls++; + }; + }, + get methodTwo() { + return function() { + methodTwoCalls++; + }; + }, + }; + const obj = Object.create(prototype, {}); + + const spy1 = moduleMocker.spyOn(obj, 'methodOne', 'get'); + const spy2 = moduleMocker.spyOn(obj, 'methodTwo', 'get'); + + // First, we call with the spies: both spies and both original functions + // should be called. + obj.methodOne(); + obj.methodTwo(); + expect(methodOneCalls).toBe(1); + expect(methodTwoCalls).toBe(1); + expect(spy1.mock.calls.length).toBe(1); + expect(spy2.mock.calls.length).toBe(1); + + moduleMocker.restoreAllMocks(); + + // Then, after resetting all mocks, we call methods again. Only the real + // methods should bump their count, not the spies. + obj.methodOne(); + obj.methodTwo(); + expect(methodOneCalls).toBe(2); + expect(methodTwoCalls).toBe(2); + expect(spy1.mock.calls.length).toBe(1); + expect(spy2.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/package.json b/package.json index aee45c2..fde6272 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "dlv": "^1.1.3", "errorstacks": "^1.3.0", "expect": "^26.4.0", + "jest-mock": "^26.3.0", "karma": "^5.1.1", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.3", diff --git a/src/lib/jest-globals.js b/src/lib/jest-globals.js index b736cf3..6cb6ff7 100644 --- a/src/lib/jest-globals.js +++ b/src/lib/jest-globals.js @@ -1,15 +1,27 @@ import './jest/nodeJSGlobals'; import expect from 'expect'; +import { ModuleMocker } from 'jest-mock'; function notImplemented() { throw Error(`Not Implemented`); } const global = window; +global.ModuleMocker = ModuleMocker; global.expect = expect; +const moduleMocker = new ModuleMocker(global); + // @todo expect.extend() et al +// @todo Consider this teardown function: https://github.com/facebook/jest/blob/e8b7f57e05e3c785c18a91556dcbc7212826a573/packages/jest-runtime/src/index.ts#L871 + +// @todo - check if jasmine allows `it` without `describe` +global.test = it; + +// @todo - add it.skip, etc. + +// Based on https://github.com/facebook/jest/blob/e8b7f57e05e3c785c18a91556dcbc7212826a573/packages/jest-runtime/src/index.ts#L1501-L1578 global.jest = { addMatchers(matchers) { jasmine.addMatchers(matchers); @@ -24,7 +36,10 @@ global.jest = { }, autoMockOff: notImplemented, autoMockOn: notImplemented, - clearAllMocks: notImplemented, + clearAllMocks() { + moduleMocker.clearAllMocks(); + return this; + }, clearAllTimers() { // _getFakeTimers().clearAllTimers(); notImplemented(); @@ -38,7 +53,7 @@ global.jest = { doMock: notImplemented, dontMock: notImplemented, enableAutomock: notImplemented, - fn: jasmine.createSpy, + fn: moduleMocker.fn.bind(moduleMocker), genMockFromModule(moduleName) { // return this._generateMock(from, moduleName); notImplemented(); @@ -48,18 +63,21 @@ global.jest = { // return _getFakeTimers().getTimerCount(); notImplemented(); }, - isMockFunction(fn) { - // check if spy/mock - notImplemented(); - }, + isMockFunction: moduleMocker.isMockFunction, isolateModules: notImplemented, mock: jasmine.createSpy, // @todo check // requireActual: require, requireMock: notImplemented, - resetAllMocks: notImplemented, + resetAllMocks() { + moduleMocker.resetAllMocks(); + return this; + }, resetModuleRegistry: notImplemented, resetModules: notImplemented, - restoreAllMocks: notImplemented, + restoreAllMocks() { + moduleMocker.restoreAllMocks(); + return this; + }, retryTimes: notImplemented, runAllImmediates() { notImplemented(); @@ -73,7 +91,7 @@ global.jest = { notImplemented(); }, setTimeout, - spyOn: jasmine.createSpy, // @todo check + spyOn: moduleMocker.spyOn.bind(moduleMocker), unmock: (mock) => mock.restore(), // @todo check useFakeTimers: notImplemented, useRealTimers: notImplemented, From fe17dd23c2c15f0c41a90ebbe64179aa3b67a864 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 14 Aug 2020 23:10:05 -0700 Subject: [PATCH 09/11] WIP: Add initial timer implementation TODO: add tests --- package.json | 1 + src/lib/jest-globals.js | 49 +++++++------ src/lib/jest/fakeTimers.js | 144 +++++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 src/lib/jest/fakeTimers.js diff --git a/package.json b/package.json index fde6272..920010a 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@rollup/plugin-babel": "^5.1.0", "@rollup/plugin-commonjs": "^14.0.0", "@rollup/plugin-node-resolve": "^8.4.0", + "@sinonjs/fake-timers": "^6.0.1", "babel-loader": "^8.1.0", "babel-plugin-istanbul": "^6.0.0", "chalk": "^4.1.0", diff --git a/src/lib/jest-globals.js b/src/lib/jest-globals.js index 6cb6ff7..7fc75e1 100644 --- a/src/lib/jest-globals.js +++ b/src/lib/jest-globals.js @@ -1,6 +1,7 @@ import './jest/nodeJSGlobals'; import expect from 'expect'; import { ModuleMocker } from 'jest-mock'; +import FakeTimers from './jest/fakeTimers'; function notImplemented() { throw Error(`Not Implemented`); @@ -11,6 +12,7 @@ global.ModuleMocker = ModuleMocker; global.expect = expect; const moduleMocker = new ModuleMocker(global); +const fakeTimers = new FakeTimers({ global }); // @todo expect.extend() et al @@ -27,12 +29,10 @@ global.jest = { jasmine.addMatchers(matchers); }, advanceTimersByTime(msToRun) { - // _getFakeTimers().advanceTimersByTime(msToRun); - notImplemented(); + fakeTimers.advanceTimersByTime(msToRun); }, advanceTimersToNextTimer(steps) { - // _getFakeTimers().advanceTimersToNextTimer(steps); - notImplemented(); + fakeTimers.advanceTimersToNextTimer(steps); }, autoMockOff: notImplemented, autoMockOn: notImplemented, @@ -40,10 +40,7 @@ global.jest = { moduleMocker.clearAllMocks(); return this; }, - clearAllTimers() { - // _getFakeTimers().clearAllTimers(); - notImplemented(); - }, + clearAllTimers: () => fakeTimers.clearAllTimers(), createMockFromModule(moduleName) { // return this._generateMock(from, moduleName); notImplemented(); @@ -58,11 +55,10 @@ global.jest = { // return this._generateMock(from, moduleName); notImplemented(); }, - getRealSystemTime: notImplemented, - getTimerCount() { - // return _getFakeTimers().getTimerCount(); - notImplemented(); + getRealSystemTime() { + return fakeTimers.getRealSystemTime(); }, + getTimerCount: () => fakeTimers.getTimerCount(), isMockFunction: moduleMocker.isMockFunction, isolateModules: notImplemented, mock: jasmine.createSpy, // @todo check @@ -79,20 +75,27 @@ global.jest = { return this; }, retryTimes: notImplemented, - runAllImmediates() { - notImplemented(); - }, - runAllTicks: notImplemented, - runAllTimers: notImplemented, - runOnlyPendingTimers: notImplemented, - runTimersToTime: notImplemented, + runAllImmediates: notImplemented, + runAllTicks: () => fakeTimers.runAllTicks(), + runAllTimers: () => fakeTimers.runAllTimers(), + runOnlyPendingTimers: () => fakeTimers.runOnlyPendingTimers(), + runTimersToTime: (msToRun) => fakeTimers.advanceTimersByTime(msToRun), setMock: notImplemented, setSystemTime(now) { - notImplemented(); + fakeTimers.setSystemTime(now); + }, + setTimeout(timeout) { + jasmine._DEFAULT_TIMEOUT_INTERVAL = timeout; + return this; }, - setTimeout, spyOn: moduleMocker.spyOn.bind(moduleMocker), unmock: (mock) => mock.restore(), // @todo check - useFakeTimers: notImplemented, - useRealTimers: notImplemented, + useFakeTimers() { + fakeTimers.useFakeTimers(); + return this; + }, + useRealTimers() { + fakeTimers.useRealTimers(); + return this; + }, }; diff --git a/src/lib/jest/fakeTimers.js b/src/lib/jest/fakeTimers.js new file mode 100644 index 0000000..cd81ceb --- /dev/null +++ b/src/lib/jest/fakeTimers.js @@ -0,0 +1,144 @@ +import { withGlobal } from '@sinonjs/fake-timers'; + +// TODO: add tests from https://github.com/facebook/jest/blob/e8b7f57e05e3c785c18a91556dcbc7212826a573/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts +// Based on https://github.com/facebook/jest/blob/e8b7f57e05e3c785c18a91556dcbc7212826a573/packages/jest-fake-timers/src/modernFakeTimers.ts +export default class FakeTimers { + /** + * @param {{ global: Window, maxLoops: number }} options + */ + constructor({ + global, + // config, + maxLoops, + }) { + this._global = global; + // this._config = config; + this._maxLoops = maxLoops || 100000; + + this._fakingTime = false; + this._fakeTimers = withGlobal(global); + } + + clearAllTimers() { + if (this._fakingTime) { + this._clock.reset(); + } + } + + dispose() { + this.useRealTimers(); + } + + runAllTimers() { + if (this._checkFakeTimers()) { + this._clock.runAll(); + } + } + + runOnlyPendingTimers() { + if (this._checkFakeTimers()) { + this._clock.runToLast(); + } + } + + advanceTimersToNextTimer(steps = 1) { + if (this._checkFakeTimers()) { + for (let i = steps; i > 0; i--) { + this._clock.next(); + // Fire all timers at this point: https://github.com/sinonjs/fake-timers/issues/250 + this._clock.tick(0); + + if (this._clock.countTimers() === 0) { + break; + } + } + } + } + + /** + * @param {number} msToRun + */ + advanceTimersByTime(msToRun) { + if (this._checkFakeTimers()) { + this._clock.tick(msToRun); + } + } + + runAllTicks() { + if (this._checkFakeTimers()) { + // @ts-expect-error + this._clock.runMicrotasks(); + } + } + + useRealTimers() { + if (this._fakingTime) { + this._clock.uninstall(); + this._fakingTime = false; + } + } + + useFakeTimers() { + if (!this._fakingTime) { + /** @type {Array} */ + // @ts-expect-error + const toFake = Object.keys(this._fakeTimers.timers); + + this._clock = this._fakeTimers.install({ + loopLimit: this._maxLoops, + now: Date.now(), + target: this._global, + toFake, + }); + + this._fakingTime = true; + } + } + + reset() { + if (this._checkFakeTimers()) { + const { now } = this._clock; + this._clock.reset(); + this._clock.setSystemTime(now); + } + } + + /** + * @param {number | Date} [now] + */ + setSystemTime(now) { + if (this._checkFakeTimers()) { + this._clock.setSystemTime(now); + } + } + + getRealSystemTime() { + return Date.now(); + } + + getTimerCount() { + if (this._checkFakeTimers()) { + return this._clock.countTimers(); + } + + return 0; + } + + _checkFakeTimers() { + if (!this._fakingTime) { + // @ts-expect-error + this._global.console.warn( + 'A function to advance timers was called but the timers API is not ' + + 'mocked with fake timers. Call `jest.useFakeTimers()` in this test or ' + + 'enable fake timers globally by setting `"timers": "fake"` in the ' + + 'configuration file\nStack Trace:\n' + + new Error().stack + // formatStackTrace(new Error().stack!, this._config, { + // noStackTrace: false, + // }), + ); + } + + return this._fakingTime; + } +} From de82dcaf274b37b3b4090788cccc3e55ce12babb Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 17 Aug 2020 11:54:50 -0700 Subject: [PATCH 10/11] Replace copy of fakeTimers.js with ModernFakerTimers import --- package.json | 2 +- src/lib/jest-globals.js | 11 ++- src/lib/jest/fakeTimers.js | 144 ------------------------------------- 3 files changed, 10 insertions(+), 147 deletions(-) delete mode 100644 src/lib/jest/fakeTimers.js diff --git a/package.json b/package.json index 920010a..75ea46e 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,10 @@ "@babel/core": "^7.11.0", "@babel/plugin-transform-react-jsx": "^7.10.3", "@babel/preset-env": "^7.11.0", + "@jest/fake-timers": "^26.3.0", "@rollup/plugin-babel": "^5.1.0", "@rollup/plugin-commonjs": "^14.0.0", "@rollup/plugin-node-resolve": "^8.4.0", - "@sinonjs/fake-timers": "^6.0.1", "babel-loader": "^8.1.0", "babel-plugin-istanbul": "^6.0.0", "chalk": "^4.1.0", diff --git a/src/lib/jest-globals.js b/src/lib/jest-globals.js index 7fc75e1..a47b2ad 100644 --- a/src/lib/jest-globals.js +++ b/src/lib/jest-globals.js @@ -1,27 +1,34 @@ import './jest/nodeJSGlobals'; import expect from 'expect'; import { ModuleMocker } from 'jest-mock'; -import FakeTimers from './jest/fakeTimers'; +import ModernFakeTimers from '@jest/fake-timers/build/modernFakeTimers'; function notImplemented() { throw Error(`Not Implemented`); } const global = window; +global.FakeTimers = ModernFakeTimers; global.ModuleMocker = ModuleMocker; global.expect = expect; const moduleMocker = new ModuleMocker(global); -const fakeTimers = new FakeTimers({ global }); +const fakeTimers = new ModernFakeTimers({ global }); // @todo expect.extend() et al // @todo Consider this teardown function: https://github.com/facebook/jest/blob/e8b7f57e05e3c785c18a91556dcbc7212826a573/packages/jest-runtime/src/index.ts#L871 +// @todo And this teardown function: https://github.com/facebook/jest/blob/9ffd368330a3aa05a7db9836be44891419b0b97d/packages/jest-environment-jsdom/src/index.ts#L106 +// Definitely need to auto dispose of fakeTimers.dispose in teardown +afterEach(() => { + fakeTimers.dispose(); +}); // @todo - check if jasmine allows `it` without `describe` global.test = it; // @todo - add it.skip, etc. +// @todo - add alias for '@jest/globals' that allows users to import these globals: https://jestjs.io/docs/en/api // Based on https://github.com/facebook/jest/blob/e8b7f57e05e3c785c18a91556dcbc7212826a573/packages/jest-runtime/src/index.ts#L1501-L1578 global.jest = { diff --git a/src/lib/jest/fakeTimers.js b/src/lib/jest/fakeTimers.js deleted file mode 100644 index cd81ceb..0000000 --- a/src/lib/jest/fakeTimers.js +++ /dev/null @@ -1,144 +0,0 @@ -import { withGlobal } from '@sinonjs/fake-timers'; - -// TODO: add tests from https://github.com/facebook/jest/blob/e8b7f57e05e3c785c18a91556dcbc7212826a573/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts -// Based on https://github.com/facebook/jest/blob/e8b7f57e05e3c785c18a91556dcbc7212826a573/packages/jest-fake-timers/src/modernFakeTimers.ts -export default class FakeTimers { - /** - * @param {{ global: Window, maxLoops: number }} options - */ - constructor({ - global, - // config, - maxLoops, - }) { - this._global = global; - // this._config = config; - this._maxLoops = maxLoops || 100000; - - this._fakingTime = false; - this._fakeTimers = withGlobal(global); - } - - clearAllTimers() { - if (this._fakingTime) { - this._clock.reset(); - } - } - - dispose() { - this.useRealTimers(); - } - - runAllTimers() { - if (this._checkFakeTimers()) { - this._clock.runAll(); - } - } - - runOnlyPendingTimers() { - if (this._checkFakeTimers()) { - this._clock.runToLast(); - } - } - - advanceTimersToNextTimer(steps = 1) { - if (this._checkFakeTimers()) { - for (let i = steps; i > 0; i--) { - this._clock.next(); - // Fire all timers at this point: https://github.com/sinonjs/fake-timers/issues/250 - this._clock.tick(0); - - if (this._clock.countTimers() === 0) { - break; - } - } - } - } - - /** - * @param {number} msToRun - */ - advanceTimersByTime(msToRun) { - if (this._checkFakeTimers()) { - this._clock.tick(msToRun); - } - } - - runAllTicks() { - if (this._checkFakeTimers()) { - // @ts-expect-error - this._clock.runMicrotasks(); - } - } - - useRealTimers() { - if (this._fakingTime) { - this._clock.uninstall(); - this._fakingTime = false; - } - } - - useFakeTimers() { - if (!this._fakingTime) { - /** @type {Array} */ - // @ts-expect-error - const toFake = Object.keys(this._fakeTimers.timers); - - this._clock = this._fakeTimers.install({ - loopLimit: this._maxLoops, - now: Date.now(), - target: this._global, - toFake, - }); - - this._fakingTime = true; - } - } - - reset() { - if (this._checkFakeTimers()) { - const { now } = this._clock; - this._clock.reset(); - this._clock.setSystemTime(now); - } - } - - /** - * @param {number | Date} [now] - */ - setSystemTime(now) { - if (this._checkFakeTimers()) { - this._clock.setSystemTime(now); - } - } - - getRealSystemTime() { - return Date.now(); - } - - getTimerCount() { - if (this._checkFakeTimers()) { - return this._clock.countTimers(); - } - - return 0; - } - - _checkFakeTimers() { - if (!this._fakingTime) { - // @ts-expect-error - this._global.console.warn( - 'A function to advance timers was called but the timers API is not ' + - 'mocked with fake timers. Call `jest.useFakeTimers()` in this test or ' + - 'enable fake timers globally by setting `"timers": "fake"` in the ' + - 'configuration file\nStack Trace:\n' + - new Error().stack - // formatStackTrace(new Error().stack!, this._config, { - // noStackTrace: false, - // }), - ); - } - - return this._fakingTime; - } -} From e4b58379f9873a15010e532deb17676b49ead2d6 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 17 Aug 2020 12:17:33 -0700 Subject: [PATCH 11/11] WIP: Add fakeTimers tests Still failing though - it seems that proper clean up isn't happening for a few tests that get an actual reference to the `window` object. May need to rewrite these tests. Perhaps something to contribute upstream? --- .../test/jest/fakeTimers.test.js | 849 ++++++++++++++++++ 1 file changed, 849 insertions(+) create mode 100644 e2e-test/webpack-default/test/jest/fakeTimers.test.js diff --git a/e2e-test/webpack-default/test/jest/fakeTimers.test.js b/e2e-test/webpack-default/test/jest/fakeTimers.test.js new file mode 100644 index 0000000..7971338 --- /dev/null +++ b/e2e-test/webpack-default/test/jest/fakeTimers.test.js @@ -0,0 +1,849 @@ +// Based on https://github.com/facebook/jest/blob/e8b7f57e05e3c785c18a91556dcbc7212826a573/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts +describe('FakeTimers', () => { + const FakeTimers = global.FakeTimers; + const setTimeout = window.setTimeout.bind(window); + const clearTimeout = window.clearTimeout.bind(window); + + describe('construction', () => { + it('installs setTimeout mock', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + expect(global.setTimeout).not.toBe(undefined); + }); + + it('installs clearTimeout mock', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + expect(global.clearTimeout).not.toBe(undefined); + }); + + it('installs setInterval mock', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + expect(global.setInterval).not.toBe(undefined); + }); + + it('installs clearInterval mock', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + expect(global.clearInterval).not.toBe(undefined); + }); + + it('mocks process.nextTick if it exists on global', () => { + const origNextTick = () => {}; + const global = { + Date, + clearTimeout, + process: { + nextTick: origNextTick, + }, + setTimeout, + }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + expect(global.process.nextTick).not.toBe(origNextTick); + }); + + it('mocks setImmediate if it exists on global', () => { + const origSetImmediate = () => {}; + const global = { + Date, + clearTimeout, + process, + setImmediate: origSetImmediate, + setTimeout, + }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + expect(global.setImmediate).not.toBe(origSetImmediate); + }); + + it('mocks clearImmediate if setImmediate is on global', () => { + const origSetImmediate = () => {}; + const origClearImmediate = () => {}; + const global = { + Date, + clearImmediate: origClearImmediate, + clearTimeout, + process, + setImmediate: origSetImmediate, + setTimeout, + }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + expect(global.clearImmediate).not.toBe(origClearImmediate); + }); + }); + + describe('runAllTicks', () => { + it('runs all ticks, in order', () => { + const global = { + Date, + clearTimeout, + process: { + nextTick: () => {}, + }, + setTimeout, + }; + + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const runOrder = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + + global.process.nextTick(mock1); + global.process.nextTick(mock2); + + expect(mock1).toHaveBeenCalledTimes(0); + expect(mock2).toHaveBeenCalledTimes(0); + + timers.runAllTicks(); + + expect(mock1).toHaveBeenCalledTimes(1); + expect(mock2).toHaveBeenCalledTimes(1); + expect(runOrder).toEqual(['mock1', 'mock2']); + }); + + it('does nothing when no ticks have been scheduled', () => { + const nextTick = jest.fn(); + const global = { + Date, + clearTimeout, + process: { + nextTick, + }, + setTimeout, + }; + + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + timers.runAllTicks(); + + expect(nextTick).toHaveBeenCalledTimes(0); + }); + + it('only runs a scheduled callback once', () => { + const global = { + Date, + clearTimeout, + process: { + nextTick: () => {}, + }, + setTimeout, + }; + + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.process.nextTick(mock1); + expect(mock1).toHaveBeenCalledTimes(0); + + timers.runAllTicks(); + expect(mock1).toHaveBeenCalledTimes(1); + + timers.runAllTicks(); + expect(mock1).toHaveBeenCalledTimes(1); + }); + + it('throws before allowing infinite recursion', () => { + const global = { + Date, + clearTimeout, + process: { + nextTick: () => {}, + }, + setTimeout, + }; + + const timers = new FakeTimers({ global, maxLoops: 100 }); + + timers.useFakeTimers(); + + global.process.nextTick(function infinitelyRecursingCallback() { + global.process.nextTick(infinitelyRecursingCallback); + }); + + expect(() => { + timers.runAllTicks(); + }).toThrow( + 'Aborting after running 100 timers, assuming an infinite loop!' + ); + }); + }); + + describe('runAllTimers', () => { + it('runs all timers in order', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const runOrder = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + const mock3 = jest.fn(() => runOrder.push('mock3')); + const mock4 = jest.fn(() => runOrder.push('mock4')); + const mock5 = jest.fn(() => runOrder.push('mock5')); + const mock6 = jest.fn(() => runOrder.push('mock6')); + + global.setTimeout(mock1, 100); + global.setTimeout(mock2, NaN); + global.setTimeout(mock3, 0); + const intervalHandler = global.setInterval(() => { + mock4(); + global.clearInterval(intervalHandler); + }, 200); + global.setTimeout(mock5, Infinity); + global.setTimeout(mock6, -Infinity); + + timers.runAllTimers(); + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'mock5', + 'mock6', + 'mock1', + 'mock4', + ]); + }); + + it('warns when trying to advance timers while real timers are used', () => { + const consoleWarn = console.warn; + console.warn = jest.fn(); + const timers = new FakeTimers({ + config: { + rootDir: __dirname, + }, + global, + }); + timers.runAllTimers(); + // expect( + // console.warn.mock.calls[0][0].split('\nStack Trace')[0] + // ).toMatchSnapshot(); + expect(console.warn.mock.calls[0][0].split('\nStack Trace')[0]).toMatch( + /A function to advance timers was called but the timers API is not mocked with fake timers/ + ); + console.warn = consoleWarn; + }); + + it('does nothing when no timers have been scheduled', () => { + const nativeSetTimeout = jest.fn(); + const global = { + Date, + clearTimeout, + process, + setTimeout: nativeSetTimeout, + }; + + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + timers.runAllTimers(); + }); + + it('only runs a setTimeout callback once (ever)', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const fn = jest.fn(); + global.setTimeout(fn, 0); + expect(fn).toHaveBeenCalledTimes(0); + + timers.runAllTimers(); + expect(fn).toHaveBeenCalledTimes(1); + + timers.runAllTimers(); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('runs callbacks with arguments after the interval', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const fn = jest.fn(); + global.setTimeout(fn, 0, 'mockArg1', 'mockArg2'); + + timers.runAllTimers(); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith('mockArg1', 'mockArg2'); + }); + + it("doesn't pass the callback to native setTimeout", () => { + const nativeSetTimeout = jest.fn(); + + const global = { + Date, + clearTimeout, + process, + setTimeout: nativeSetTimeout, + }; + + const timers = new FakeTimers({ global }); + // @sinonjs/fake-timers uses `setTimeout` during init to figure out if it's in Node or + // browser env. So clear its calls before we install them into the env + nativeSetTimeout.mockClear(); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.setTimeout(mock1, 0); + + timers.runAllTimers(); + expect(mock1).toHaveBeenCalledTimes(1); + expect(nativeSetTimeout).toHaveBeenCalledTimes(0); + }); + + it('throws before allowing infinite recursion', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global, maxLoops: 100 }); + timers.useFakeTimers(); + + global.setTimeout(function infinitelyRecursingCallback() { + global.setTimeout(infinitelyRecursingCallback, 0); + }, 0); + + expect(() => { + timers.runAllTimers(); + }).toThrow( + new Error( + 'Aborting after running 100 timers, assuming an infinite loop!' + ) + ); + }); + + it('also clears ticks', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const fn = jest.fn(); + global.setTimeout(() => { + process.nextTick(fn); + }, 0); + expect(fn).toHaveBeenCalledTimes(0); + + timers.runAllTimers(); + expect(fn).toHaveBeenCalledTimes(1); + }); + }); + + describe('advanceTimersByTime', () => { + it('runs timers in order', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const runOrder = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + const mock3 = jest.fn(() => runOrder.push('mock3')); + const mock4 = jest.fn(() => runOrder.push('mock4')); + + global.setTimeout(mock1, 100); + global.setTimeout(mock2, 0); + global.setTimeout(mock3, 0); + global.setInterval(() => { + mock4(); + }, 200); + + // Move forward to t=50 + timers.advanceTimersByTime(50); + expect(runOrder).toEqual(['mock2', 'mock3']); + + // Move forward to t=60 + timers.advanceTimersByTime(10); + expect(runOrder).toEqual(['mock2', 'mock3']); + + // Move forward to t=100 + timers.advanceTimersByTime(40); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + + // Move forward to t=200 + timers.advanceTimersByTime(100); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']); + + // Move forward to t=400 + timers.advanceTimersByTime(200); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4', 'mock4']); + }); + + it('does nothing when no timers have been scheduled', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + timers.advanceTimersByTime(100); + }); + }); + + describe('advanceTimersToNextTimer', () => { + it('runs timers in order', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + /** @type {string[]} */ + const runOrder = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + const mock3 = jest.fn(() => runOrder.push('mock3')); + const mock4 = jest.fn(() => runOrder.push('mock4')); + + global.setTimeout(mock1, 100); + global.setTimeout(mock2, 0); + global.setTimeout(mock3, 0); + global.setInterval(() => { + mock4(); + }, 200); + + timers.advanceTimersToNextTimer(); + // Move forward to t=0 + expect(runOrder).toEqual(['mock2', 'mock3']); + + timers.advanceTimersToNextTimer(); + // Move forward to t=100 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + + timers.advanceTimersToNextTimer(); + // Move forward to t=200 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']); + + timers.advanceTimersToNextTimer(); + // Move forward to t=400 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4', 'mock4']); + }); + + it('run correct amount of steps', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + /** @type {string[]} */ + const runOrder = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + const mock3 = jest.fn(() => runOrder.push('mock3')); + const mock4 = jest.fn(() => runOrder.push('mock4')); + + global.setTimeout(mock1, 100); + global.setTimeout(mock2, 0); + global.setTimeout(mock3, 0); + global.setInterval(() => { + mock4(); + }, 200); + + // Move forward to t=100 + timers.advanceTimersToNextTimer(2); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + + // Move forward to t=600 + timers.advanceTimersToNextTimer(3); + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'mock1', + 'mock4', + 'mock4', + 'mock4', + ]); + }); + + it('setTimeout inside setTimeout', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + /** @type {string[]} */ + const runOrder = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + const mock3 = jest.fn(() => runOrder.push('mock3')); + const mock4 = jest.fn(() => runOrder.push('mock4')); + + global.setTimeout(mock1, 0); + global.setTimeout(() => { + mock2(); + global.setTimeout(mock3, 50); + }, 25); + global.setTimeout(mock4, 100); + + // Move forward to t=75 + timers.advanceTimersToNextTimer(3); + expect(runOrder).toEqual(['mock1', 'mock2', 'mock3']); + }); + + it('does nothing when no timers have been scheduled', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + timers.advanceTimersToNextTimer(); + }); + }); + + describe('reset', () => { + it('resets all pending setTimeouts', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.setTimeout(mock1, 100); + + timers.reset(); + timers.runAllTimers(); + expect(mock1).toHaveBeenCalledTimes(0); + }); + + it('resets all pending setIntervals', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.setInterval(mock1, 200); + + timers.reset(); + timers.runAllTimers(); + expect(mock1).toHaveBeenCalledTimes(0); + }); + + it('resets all pending ticks callbacks', () => { + const global = { + Date, + clearTimeout, + process: { + nextTick: () => {}, + }, + setImmediate: () => {}, + setTimeout, + }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.process.nextTick(mock1); + global.setImmediate(mock1); + + timers.reset(); + timers.runAllTicks(); + expect(mock1).toHaveBeenCalledTimes(0); + }); + + it('resets current advanceTimersByTime time cursor', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.setTimeout(mock1, 100); + timers.advanceTimersByTime(50); + + timers.reset(); + global.setTimeout(mock1, 100); + + timers.advanceTimersByTime(50); + expect(mock1).toHaveBeenCalledTimes(0); + }); + }); + + describe('runOnlyPendingTimers', () => { + it('runs all timers in order', () => { + const nativeSetImmediate = jest.fn(); + + const global = { + Date, + clearTimeout, + process, + setImmediate: nativeSetImmediate, + setTimeout, + }; + + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const runOrder = []; + + global.setTimeout(function cb() { + runOrder.push('mock1'); + global.setTimeout(cb, 100); + }, 100); + + global.setTimeout(function cb() { + runOrder.push('mock2'); + global.setTimeout(cb, 50); + }, 0); + + global.setInterval(() => { + runOrder.push('mock3'); + }, 200); + + global.setImmediate(() => { + runOrder.push('mock4'); + }); + + global.setImmediate(function cb() { + runOrder.push('mock5'); + global.setTimeout(cb, 400); + }); + + timers.runOnlyPendingTimers(); + const firsRunOrder = [ + 'mock4', + 'mock5', + 'mock2', + 'mock2', + 'mock1', + 'mock2', + 'mock2', + 'mock3', + 'mock1', + 'mock2', + ]; + + expect(runOrder).toEqual(firsRunOrder); + + timers.runOnlyPendingTimers(); + expect(runOrder).toEqual([ + ...firsRunOrder, + 'mock2', + 'mock1', + 'mock2', + 'mock2', + 'mock3', + 'mock5', + 'mock1', + 'mock2', + ]); + }); + + it('does not run timers that were cleared in another timer', () => { + const global = { Date, clearTimeout, process, setTimeout }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + const fn = jest.fn(); + const timer = global.setTimeout(fn, 10); + global.setTimeout(() => { + global.clearTimeout(timer); + }, 0); + + timers.runOnlyPendingTimers(); + expect(fn).not.toBeCalled(); + }); + }); + + describe('useRealTimers', () => { + it('resets native timer APIs', () => { + const nativeSetTimeout = jest.fn(); + const nativeSetInterval = jest.fn(); + const nativeClearTimeout = jest.fn(); + const nativeClearInterval = jest.fn(); + + const global = { + Date, + clearInterval: nativeClearInterval, + clearTimeout: nativeClearTimeout, + process, + setInterval: nativeSetInterval, + setTimeout: nativeSetTimeout, + }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + // Ensure that timers has overridden the native timer APIs + // (because if it didn't, this test might pass when it shouldn't) + expect(global.setTimeout).not.toBe(nativeSetTimeout); + expect(global.setInterval).not.toBe(nativeSetInterval); + expect(global.clearTimeout).not.toBe(nativeClearTimeout); + expect(global.clearInterval).not.toBe(nativeClearInterval); + + timers.useRealTimers(); + + expect(global.setTimeout).toBe(nativeSetTimeout); + expect(global.setInterval).toBe(nativeSetInterval); + expect(global.clearTimeout).toBe(nativeClearTimeout); + expect(global.clearInterval).toBe(nativeClearInterval); + }); + + it('resets native process.nextTick when present', () => { + const nativeProcessNextTick = jest.fn(); + + const global = { + Date, + clearTimeout, + process: { nextTick: nativeProcessNextTick }, + setTimeout, + }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + // Ensure that timers has overridden the native timer APIs + // (because if it didn't, this test might pass when it shouldn't) + expect(global.process.nextTick).not.toBe(nativeProcessNextTick); + + timers.useRealTimers(); + + expect(global.process.nextTick).toBe(nativeProcessNextTick); + }); + + it('resets native setImmediate when present', () => { + const nativeSetImmediate = jest.fn(); + const nativeClearImmediate = jest.fn(); + + const global = { + Date, + clearImmediate: nativeClearImmediate, + clearTimeout, + process, + setImmediate: nativeSetImmediate, + setTimeout, + }; + const timers = new FakeTimers({ global }); + timers.useFakeTimers(); + + // Ensure that timers has overridden the native timer APIs + // (because if it didn't, this test might pass when it shouldn't) + expect(global.setImmediate).not.toBe(nativeSetImmediate); + expect(global.clearImmediate).not.toBe(nativeClearImmediate); + + timers.useRealTimers(); + + expect(global.setImmediate).toBe(nativeSetImmediate); + expect(global.clearImmediate).toBe(nativeClearImmediate); + }); + }); + + describe('useFakeTimers', () => { + it('resets mock timer APIs', () => { + const nativeSetTimeout = jest.fn(); + const nativeSetInterval = jest.fn(); + const nativeClearTimeout = jest.fn(); + const nativeClearInterval = jest.fn(); + + const global = { + Date, + clearInterval: nativeClearInterval, + clearTimeout: nativeClearTimeout, + process, + setInterval: nativeSetInterval, + setTimeout: nativeSetTimeout, + }; + const timers = new FakeTimers({ global }); + timers.useRealTimers(); + + // Ensure that the real timers are installed at this point + // (because if they aren't, this test might pass when it shouldn't) + expect(global.setTimeout).toBe(nativeSetTimeout); + expect(global.setInterval).toBe(nativeSetInterval); + expect(global.clearTimeout).toBe(nativeClearTimeout); + expect(global.clearInterval).toBe(nativeClearInterval); + + timers.useFakeTimers(); + + expect(global.setTimeout).not.toBe(nativeSetTimeout); + expect(global.setInterval).not.toBe(nativeSetInterval); + expect(global.clearTimeout).not.toBe(nativeClearTimeout); + expect(global.clearInterval).not.toBe(nativeClearInterval); + }); + + it('resets mock process.nextTick when present', () => { + const nativeProcessNextTick = jest.fn(); + + const global = { + Date, + clearTimeout, + process: { nextTick: nativeProcessNextTick }, + setTimeout, + }; + const timers = new FakeTimers({ global }); + timers.useRealTimers(); + + // Ensure that the real timers are installed at this point + // (because if they aren't, this test might pass when it shouldn't) + expect(global.process.nextTick).toBe(nativeProcessNextTick); + + timers.useFakeTimers(); + + expect(global.process.nextTick).not.toBe(nativeProcessNextTick); + }); + + it('resets mock setImmediate when present', () => { + const nativeSetImmediate = jest.fn(); + const nativeClearImmediate = jest.fn(); + + const global = { + Date, + clearImmediate: nativeClearImmediate, + clearTimeout, + process, + setImmediate: nativeSetImmediate, + setTimeout, + }; + const fakeTimers = new FakeTimers({ global }); + fakeTimers.useRealTimers(); + + // Ensure that the real timers are installed at this point + // (because if they aren't, this test might pass when it shouldn't) + expect(global.setImmediate).toBe(nativeSetImmediate); + expect(global.clearImmediate).toBe(nativeClearImmediate); + + fakeTimers.useFakeTimers(); + + expect(global.setImmediate).not.toBe(nativeSetImmediate); + expect(global.clearImmediate).not.toBe(nativeClearImmediate); + }); + }); + + describe('getTimerCount', () => { + it('returns the correct count', () => { + const timers = new FakeTimers({ global }); + + timers.useFakeTimers(); + + global.setTimeout(() => {}, 0); + global.setTimeout(() => {}, 0); + global.setTimeout(() => {}, 10); + + expect(timers.getTimerCount()).toEqual(3); + + timers.advanceTimersByTime(5); + + expect(timers.getTimerCount()).toEqual(1); + + timers.advanceTimersByTime(5); + + expect(timers.getTimerCount()).toEqual(0); + }); + + it('includes immediates and ticks', () => { + const timers = new FakeTimers({ global }); + + timers.useFakeTimers(); + + global.setTimeout(() => {}, 0); + global.setImmediate(() => {}); + process.nextTick(() => {}); + + expect(timers.getTimerCount()).toEqual(3); + }); + + it('not includes cancelled immediates', () => { + const timers = new FakeTimers({ global }); + + timers.useFakeTimers(); + + global.setImmediate(() => {}); + expect(timers.getTimerCount()).toEqual(1); + timers.clearAllTimers(); + + expect(timers.getTimerCount()).toEqual(0); + }); + }); +});