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/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); + }); + }); +}); 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 1c7ee3c..75ea46e 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", + "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", @@ -46,16 +48,18 @@ "@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", "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", - "expect": "^24.9.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/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...'); diff --git a/src/configure.js b/src/configure.js index d6400a2..9e02d17 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)), @@ -174,18 +176,8 @@ 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: path.resolve(__dirname, './lib/jest-globals.js'), + pattern: jestGlobalsPath, watched: false, included: true, served: true, @@ -209,6 +201,7 @@ export default async function configure(options) { ), preprocessors: { + // [jestGlobalsPath]: preprocessors, [rootFiles + '/**/*']: preprocessors, [rootFiles]: preprocessors, }, diff --git a/src/lib/jest-globals.js b/src/lib/jest-globals.js index efe9ef7..a47b2ad 100644 --- a/src/lib/jest-globals.js +++ b/src/lib/jest-globals.js @@ -1,28 +1,53 @@ +import './jest/nodeJSGlobals'; +import expect from 'expect'; +import { ModuleMocker } from 'jest-mock'; +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 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 = { addMatchers(matchers) { 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, - clearAllMocks: notImplemented, - clearAllTimers() { - // _getFakeTimers().clearAllTimers(); - notImplemented(); + clearAllMocks() { + moduleMocker.clearAllMocks(); + return this; }, + clearAllTimers: () => fakeTimers.clearAllTimers(), createMockFromModule(moduleName) { // return this._generateMock(from, moduleName); notImplemented(); @@ -32,43 +57,52 @@ global.jest = { doMock: notImplemented, dontMock: notImplemented, enableAutomock: notImplemented, - fn: jasmine.createSpy, + fn: moduleMocker.fn.bind(moduleMocker), genMockFromModule(moduleName) { // return this._generateMock(from, moduleName); notImplemented(); }, - getRealSystemTime: notImplemented, - getTimerCount() { - // return _getFakeTimers().getTimerCount(); - notImplemented(); - }, - isMockFunction(fn) { - // check if spy/mock - notImplemented(); + getRealSystemTime() { + return fakeTimers.getRealSystemTime(); }, + getTimerCount: () => fakeTimers.getTimerCount(), + isMockFunction: moduleMocker.isMockFunction, isolateModules: notImplemented, mock: jasmine.createSpy, // @todo check - requireActual: require, + // requireActual: require, requireMock: notImplemented, - resetAllMocks: notImplemented, + resetAllMocks() { + moduleMocker.resetAllMocks(); + return this; + }, resetModuleRegistry: notImplemented, resetModules: notImplemented, - restoreAllMocks: notImplemented, - retryTimes: notImplemented, - runAllImmediates() { - notImplemented(); + restoreAllMocks() { + moduleMocker.restoreAllMocks(); + return this; }, - runAllTicks: notImplemented, - runAllTimers: notImplemented, - runOnlyPendingTimers: notImplemented, - runTimersToTime: notImplemented, + retryTimes: 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: jasmine.createSpy, // @todo check + 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/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;