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<void>>}
  */
-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;