Testing with mock clocks can often turn into a real struggle when
dealing with situations where some work in the test is truly async and
other work is captured by the mock clock. This can happen for many
reasons, but as one example:
An asynchonrous change from a task in the mocked clock may change DOM where
a resize observer then gets triggered. This browser API is truly asynchronous
and would require the user to wait real time for it to fire. If there is
follow-up work after the resize observer fires, it may be captured by the mock
clock again. This would require the tester to write something like the
following:
```
// flush the timer
jasmine.clock().tick();
// wait for resize observer
await new Promise(resolve => setTimeout(resolve));
// flush follow-up work from the resize observer callback
jasmine.clock().tick();
```
When using mock clocks, testers are always forced to write tests with intimate
knowledge of when the mock clock needs to be ticked. Oftentimes, the
purpose of using a mock clock is to speed up the execution time of the
test when there are timeouts involved. It is not often a goal to test
the exact timeout values. This can cause tests to be riddled with
`tick`. It ideal for test code to be written in a way
that is independent of whether a mock clock is installed. For example:
```
document.getElementById('submit');
// https://testing-library.com/docs/dom-testing-library/api-async/#waitfor
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))
```
When mock clocks are involved, the above may not be possible if there is
some delay involved between the click and the request to the API.
Instead, developers would need to manually tick the clock beyond the
delay to trigger the API call.
This commit attempts to resolve these issues by adding a feature to the
clock which allows it to advance on its own with the passage of time,
just as clocks do without mocks installed. It also allows for some
breathing time so any unmocked micro and macrotasks are given space to
execute as well.
This feature would also address both #1725 and #1932. `asyncTick` can be
accomplished by enabling the auto tick feature and then waiting for a
promise with a timout to be resolved
(`await new Promise(resolve => setTimeout(resolve, 20))`) where
`setTimeout` is captured by the mock clock and flushed while the code is
waiting for the promise to resolve.
resolves #1725
resolves #1932
All credit goes to @stephenfarrar for this.
1191 lines
36 KiB
JavaScript
1191 lines
36 KiB
JavaScript
describe('Clock', function() {
|
|
const NODE_JS =
|
|
typeof process !== 'undefined' &&
|
|
process.versions &&
|
|
typeof process.versions.node === 'string';
|
|
|
|
it('does not replace setTimeout until it is installed', function() {
|
|
const fakeSetTimeout = jasmine.createSpy('global setTimeout'),
|
|
fakeGlobal = { setTimeout: fakeSetTimeout },
|
|
delayedFunctionScheduler = jasmine.createSpyObj(
|
|
'delayedFunctionScheduler',
|
|
['scheduleFunction']
|
|
),
|
|
delayedFn = jasmine.createSpy('delayedFn'),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
fakeGlobal.setTimeout(delayedFn, 0);
|
|
|
|
expect(fakeSetTimeout).toHaveBeenCalledWith(delayedFn, 0);
|
|
expect(delayedFunctionScheduler.scheduleFunction).not.toHaveBeenCalled();
|
|
|
|
fakeSetTimeout.calls.reset();
|
|
|
|
clock.install();
|
|
fakeGlobal.setTimeout(delayedFn, 0);
|
|
|
|
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalled();
|
|
expect(fakeSetTimeout).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not replace clearTimeout until it is installed', function() {
|
|
const fakeClearTimeout = jasmine.createSpy('global cleartimeout'),
|
|
fakeGlobal = { clearTimeout: fakeClearTimeout },
|
|
delayedFunctionScheduler = jasmine.createSpyObj(
|
|
'delayedFunctionScheduler',
|
|
['removeFunctionWithId']
|
|
),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
fakeGlobal.clearTimeout('foo');
|
|
|
|
expect(fakeClearTimeout).toHaveBeenCalledWith('foo');
|
|
expect(
|
|
delayedFunctionScheduler.removeFunctionWithId
|
|
).not.toHaveBeenCalled();
|
|
|
|
fakeClearTimeout.calls.reset();
|
|
|
|
clock.install();
|
|
fakeGlobal.clearTimeout('foo');
|
|
|
|
expect(delayedFunctionScheduler.removeFunctionWithId).toHaveBeenCalled();
|
|
expect(fakeClearTimeout).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not replace setInterval until it is installed', function() {
|
|
const fakeSetInterval = jasmine.createSpy('global setInterval'),
|
|
fakeGlobal = { setInterval: fakeSetInterval },
|
|
delayedFunctionScheduler = jasmine.createSpyObj(
|
|
'delayedFunctionScheduler',
|
|
['scheduleFunction']
|
|
),
|
|
delayedFn = jasmine.createSpy('delayedFn'),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
fakeGlobal.setInterval(delayedFn, 0);
|
|
|
|
expect(fakeSetInterval).toHaveBeenCalledWith(delayedFn, 0);
|
|
expect(delayedFunctionScheduler.scheduleFunction).not.toHaveBeenCalled();
|
|
|
|
fakeSetInterval.calls.reset();
|
|
|
|
clock.install();
|
|
fakeGlobal.setInterval(delayedFn, 0);
|
|
|
|
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalled();
|
|
expect(fakeSetInterval).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not replace clearInterval until it is installed', function() {
|
|
const fakeClearInterval = jasmine.createSpy('global clearinterval'),
|
|
fakeGlobal = { clearInterval: fakeClearInterval },
|
|
delayedFunctionScheduler = jasmine.createSpyObj(
|
|
'delayedFunctionScheduler',
|
|
['removeFunctionWithId']
|
|
),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
fakeGlobal.clearInterval('foo');
|
|
|
|
expect(fakeClearInterval).toHaveBeenCalledWith('foo');
|
|
expect(
|
|
delayedFunctionScheduler.removeFunctionWithId
|
|
).not.toHaveBeenCalled();
|
|
|
|
fakeClearInterval.calls.reset();
|
|
|
|
clock.install();
|
|
fakeGlobal.clearInterval('foo');
|
|
|
|
expect(delayedFunctionScheduler.removeFunctionWithId).toHaveBeenCalled();
|
|
expect(fakeClearInterval).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not install if the current setTimeout is not the original function on the global', function() {
|
|
const originalFakeSetTimeout = function() {},
|
|
replacedSetTimeout = function() {},
|
|
fakeGlobal = { setTimeout: originalFakeSetTimeout },
|
|
delayedFunctionSchedulerFactory = jasmine.createSpy(
|
|
'delayedFunctionSchedulerFactory'
|
|
),
|
|
mockDate = {},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
delayedFunctionSchedulerFactory,
|
|
mockDate
|
|
);
|
|
|
|
fakeGlobal.setTimeout = replacedSetTimeout;
|
|
|
|
expect(function() {
|
|
clock.install();
|
|
}).toThrowError(/unable to install/);
|
|
|
|
expect(delayedFunctionSchedulerFactory).not.toHaveBeenCalled();
|
|
expect(fakeGlobal.setTimeout).toBe(replacedSetTimeout);
|
|
});
|
|
|
|
it('does not install if the current clearTimeout is not the original function on the global', function() {
|
|
const originalFakeClearTimeout = function() {},
|
|
replacedClearTimeout = function() {},
|
|
fakeGlobal = { clearTimeout: originalFakeClearTimeout },
|
|
delayedFunctionSchedulerFactory = jasmine.createSpy(
|
|
'delayedFunctionSchedulerFactory'
|
|
),
|
|
mockDate = {},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
delayedFunctionSchedulerFactory,
|
|
mockDate
|
|
);
|
|
|
|
fakeGlobal.clearTimeout = replacedClearTimeout;
|
|
|
|
expect(function() {
|
|
clock.install();
|
|
}).toThrowError(/unable to install/);
|
|
|
|
expect(delayedFunctionSchedulerFactory).not.toHaveBeenCalled();
|
|
expect(fakeGlobal.clearTimeout).toBe(replacedClearTimeout);
|
|
});
|
|
|
|
it('does not install if the current setInterval is not the original function on the global', function() {
|
|
const originalFakeSetInterval = function() {},
|
|
replacedSetInterval = function() {},
|
|
fakeGlobal = { setInterval: originalFakeSetInterval },
|
|
delayedFunctionSchedulerFactory = jasmine.createSpy(
|
|
'delayedFunctionSchedulerFactory'
|
|
),
|
|
mockDate = {},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
delayedFunctionSchedulerFactory,
|
|
mockDate
|
|
);
|
|
|
|
fakeGlobal.setInterval = replacedSetInterval;
|
|
|
|
expect(function() {
|
|
clock.install();
|
|
}).toThrowError(/unable to install/);
|
|
|
|
expect(delayedFunctionSchedulerFactory).not.toHaveBeenCalled();
|
|
expect(fakeGlobal.setInterval).toBe(replacedSetInterval);
|
|
});
|
|
|
|
it('does not install if the current clearInterval is not the original function on the global', function() {
|
|
const originalFakeClearInterval = function() {},
|
|
replacedClearInterval = function() {},
|
|
fakeGlobal = { clearInterval: originalFakeClearInterval },
|
|
delayedFunctionSchedulerFactory = jasmine.createSpy(
|
|
'delayedFunctionSchedulerFactory'
|
|
),
|
|
mockDate = {},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
delayedFunctionSchedulerFactory,
|
|
mockDate
|
|
);
|
|
|
|
fakeGlobal.clearInterval = replacedClearInterval;
|
|
|
|
expect(function() {
|
|
clock.install();
|
|
}).toThrowError(/unable to install/);
|
|
|
|
expect(delayedFunctionSchedulerFactory).not.toHaveBeenCalled();
|
|
expect(fakeGlobal.clearInterval).toBe(replacedClearInterval);
|
|
});
|
|
|
|
it('replaces the global timer functions on uninstall', function() {
|
|
const fakeSetTimeout = jasmine.createSpy('global setTimeout'),
|
|
fakeClearTimeout = jasmine.createSpy('global clearTimeout'),
|
|
fakeSetInterval = jasmine.createSpy('global setInterval'),
|
|
fakeClearInterval = jasmine.createSpy('global clearInterval'),
|
|
fakeGlobal = {
|
|
setTimeout: fakeSetTimeout,
|
|
clearTimeout: fakeClearTimeout,
|
|
setInterval: fakeSetInterval,
|
|
clearInterval: fakeClearInterval
|
|
},
|
|
delayedFunctionScheduler = jasmine.createSpyObj(
|
|
'delayedFunctionScheduler',
|
|
['scheduleFunction', 'reset']
|
|
),
|
|
delayedFn = jasmine.createSpy('delayedFn'),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
clock.uninstall();
|
|
fakeGlobal.setTimeout(delayedFn, 0);
|
|
fakeGlobal.clearTimeout('foo');
|
|
fakeGlobal.setInterval(delayedFn, 10);
|
|
fakeGlobal.clearInterval('bar');
|
|
|
|
expect(fakeSetTimeout).toHaveBeenCalledWith(delayedFn, 0);
|
|
expect(fakeClearTimeout).toHaveBeenCalledWith('foo');
|
|
expect(fakeSetInterval).toHaveBeenCalledWith(delayedFn, 10);
|
|
expect(fakeClearInterval).toHaveBeenCalledWith('bar');
|
|
expect(delayedFunctionScheduler.scheduleFunction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('can be installed for the duration of a passed in function and uninstalled when done', function() {
|
|
const fakeSetTimeout = jasmine.createSpy('global setTimeout'),
|
|
fakeClearTimeout = jasmine.createSpy('global clearTimeout'),
|
|
fakeSetInterval = jasmine.createSpy('global setInterval'),
|
|
fakeClearInterval = jasmine.createSpy('global clearInterval'),
|
|
fakeGlobal = {
|
|
setTimeout: fakeSetTimeout,
|
|
clearTimeout: fakeClearTimeout,
|
|
setInterval: fakeSetInterval,
|
|
clearInterval: fakeClearInterval
|
|
},
|
|
delayedFunctionScheduler = jasmine.createSpyObj(
|
|
'delayedFunctionScheduler',
|
|
['scheduleFunction', 'reset', 'removeFunctionWithId']
|
|
),
|
|
delayedFn = jasmine.createSpy('delayedFn'),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
let passedFunctionCalled = false;
|
|
|
|
clock.withMock(function() {
|
|
fakeGlobal.setTimeout(delayedFn, 0);
|
|
fakeGlobal.clearTimeout('foo');
|
|
fakeGlobal.setInterval(delayedFn, 10);
|
|
fakeGlobal.clearInterval('bar');
|
|
passedFunctionCalled = true;
|
|
});
|
|
|
|
expect(passedFunctionCalled).toBe(true);
|
|
|
|
expect(fakeSetTimeout).not.toHaveBeenCalled();
|
|
expect(fakeClearTimeout).not.toHaveBeenCalled();
|
|
expect(fakeSetInterval).not.toHaveBeenCalled();
|
|
expect(fakeClearInterval).not.toHaveBeenCalled();
|
|
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalled();
|
|
|
|
delayedFunctionScheduler.scheduleFunction.calls.reset();
|
|
|
|
fakeGlobal.setTimeout(delayedFn, 0);
|
|
fakeGlobal.clearTimeout('foo');
|
|
fakeGlobal.setInterval(delayedFn, 10);
|
|
fakeGlobal.clearInterval('bar');
|
|
|
|
expect(fakeSetTimeout).toHaveBeenCalledWith(delayedFn, 0);
|
|
expect(fakeClearTimeout).toHaveBeenCalledWith('foo');
|
|
expect(fakeSetInterval).toHaveBeenCalledWith(delayedFn, 10);
|
|
expect(fakeClearInterval).toHaveBeenCalledWith('bar');
|
|
expect(delayedFunctionScheduler.scheduleFunction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('can be installed for the duration of a passed in function and uninstalled if an error is thrown', function() {
|
|
const fakeSetTimeout = jasmine.createSpy('global setTimeout'),
|
|
fakeClearTimeout = jasmine.createSpy('global clearTimeout'),
|
|
fakeSetInterval = jasmine.createSpy('global setInterval'),
|
|
fakeClearInterval = jasmine.createSpy('global clearInterval'),
|
|
fakeGlobal = {
|
|
setTimeout: fakeSetTimeout,
|
|
clearTimeout: fakeClearTimeout,
|
|
setInterval: fakeSetInterval,
|
|
clearInterval: fakeClearInterval
|
|
},
|
|
delayedFunctionScheduler = jasmine.createSpyObj(
|
|
'delayedFunctionScheduler',
|
|
['scheduleFunction', 'reset', 'removeFunctionWithId']
|
|
),
|
|
delayedFn = jasmine.createSpy('delayedFn'),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
let passedFunctionCalled = false;
|
|
|
|
expect(function() {
|
|
clock.withMock(function() {
|
|
fakeGlobal.setTimeout(delayedFn, 0);
|
|
fakeGlobal.clearTimeout('foo');
|
|
fakeGlobal.setInterval(delayedFn, 10);
|
|
fakeGlobal.clearInterval('bar');
|
|
passedFunctionCalled = true;
|
|
throw 'oops';
|
|
});
|
|
}).toThrow('oops');
|
|
|
|
expect(passedFunctionCalled).toBe(true);
|
|
|
|
expect(fakeSetTimeout).not.toHaveBeenCalled();
|
|
expect(fakeClearTimeout).not.toHaveBeenCalled();
|
|
expect(fakeSetInterval).not.toHaveBeenCalled();
|
|
expect(fakeClearInterval).not.toHaveBeenCalled();
|
|
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalled();
|
|
|
|
delayedFunctionScheduler.scheduleFunction.calls.reset();
|
|
|
|
fakeGlobal.setTimeout(delayedFn, 0);
|
|
fakeGlobal.clearTimeout('foo');
|
|
fakeGlobal.setInterval(delayedFn, 10);
|
|
fakeGlobal.clearInterval('bar');
|
|
|
|
expect(fakeSetTimeout).toHaveBeenCalledWith(delayedFn, 0);
|
|
expect(fakeClearTimeout).toHaveBeenCalledWith('foo');
|
|
expect(fakeSetInterval).toHaveBeenCalledWith(delayedFn, 10);
|
|
expect(fakeClearInterval).toHaveBeenCalledWith('bar');
|
|
expect(delayedFunctionScheduler.scheduleFunction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('schedules the delayed function (via setTimeout) with the fake timer', function() {
|
|
const fakeSetTimeout = jasmine.createSpy('setTimeout'),
|
|
scheduleFunction = jasmine.createSpy('scheduleFunction'),
|
|
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
|
|
fakeGlobal = { setTimeout: fakeSetTimeout },
|
|
delayedFn = jasmine.createSpy('delayedFn'),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
),
|
|
timeout = new clock.FakeTimeout();
|
|
|
|
clock.install();
|
|
clock.setTimeout(delayedFn, 0, 'a', 'b');
|
|
|
|
expect(fakeSetTimeout).not.toHaveBeenCalled();
|
|
|
|
if (!NODE_JS) {
|
|
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
|
|
delayedFn,
|
|
0,
|
|
['a', 'b']
|
|
);
|
|
} else {
|
|
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
|
|
delayedFn,
|
|
0,
|
|
['a', 'b'],
|
|
false,
|
|
timeout
|
|
);
|
|
}
|
|
});
|
|
|
|
it('returns an id for the delayed function', function() {
|
|
const fakeSetTimeout = jasmine.createSpy('setTimeout'),
|
|
scheduleId = 123,
|
|
scheduleFunction = jasmine
|
|
.createSpy('scheduleFunction')
|
|
.and.returnValue(scheduleId),
|
|
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
|
|
fakeGlobal = { setTimeout: fakeSetTimeout },
|
|
delayedFn = jasmine.createSpy('delayedFn'),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
const timeout = clock.setTimeout(delayedFn, 0);
|
|
|
|
if (!NODE_JS) {
|
|
expect(timeout).toEqual(123);
|
|
} else {
|
|
expect(timeout.constructor.name).toEqual('FakeTimeout');
|
|
}
|
|
});
|
|
|
|
it('clears the scheduled function with the scheduler', function() {
|
|
const fakeClearTimeout = jasmine.createSpy('clearTimeout'),
|
|
delayedFunctionScheduler = jasmine.createSpyObj(
|
|
'delayedFunctionScheduler',
|
|
['removeFunctionWithId']
|
|
),
|
|
fakeGlobal = { setTimeout: fakeClearTimeout },
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
clock.clearTimeout(123);
|
|
|
|
expect(fakeClearTimeout).not.toHaveBeenCalled();
|
|
expect(delayedFunctionScheduler.removeFunctionWithId).toHaveBeenCalledWith(
|
|
123
|
|
);
|
|
});
|
|
|
|
it('schedules the delayed function with the fake timer', function() {
|
|
const fakeSetInterval = jasmine.createSpy('setInterval'),
|
|
scheduleFunction = jasmine.createSpy('scheduleFunction'),
|
|
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
|
|
fakeGlobal = { setInterval: fakeSetInterval },
|
|
delayedFn = jasmine.createSpy('delayedFn'),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
),
|
|
timeout = new clock.FakeTimeout();
|
|
|
|
clock.install();
|
|
clock.setInterval(delayedFn, 0, 'a', 'b');
|
|
|
|
expect(fakeSetInterval).not.toHaveBeenCalled();
|
|
|
|
if (!NODE_JS) {
|
|
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
|
|
delayedFn,
|
|
0,
|
|
['a', 'b'],
|
|
true
|
|
);
|
|
} else {
|
|
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
|
|
delayedFn,
|
|
0,
|
|
['a', 'b'],
|
|
true,
|
|
timeout
|
|
);
|
|
}
|
|
});
|
|
|
|
it('returns an id for the delayed function', function() {
|
|
const fakeSetInterval = jasmine.createSpy('setInterval'),
|
|
scheduleId = 123,
|
|
scheduleFunction = jasmine
|
|
.createSpy('scheduleFunction')
|
|
.and.returnValue(scheduleId),
|
|
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
|
|
fakeGlobal = { setInterval: fakeSetInterval },
|
|
delayedFn = jasmine.createSpy('delayedFn'),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
const interval = clock.setInterval(delayedFn, 0);
|
|
|
|
if (!NODE_JS) {
|
|
expect(interval).toEqual(123);
|
|
} else {
|
|
expect(interval.constructor.name).toEqual('FakeTimeout');
|
|
}
|
|
});
|
|
|
|
it('clears the scheduled function with the scheduler', function() {
|
|
const clearInterval = jasmine.createSpy('clearInterval'),
|
|
delayedFunctionScheduler = jasmine.createSpyObj(
|
|
'delayedFunctionScheduler',
|
|
['removeFunctionWithId']
|
|
),
|
|
fakeGlobal = { setInterval: clearInterval },
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
fakeGlobal,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
clock.clearInterval(123);
|
|
|
|
expect(clearInterval).not.toHaveBeenCalled();
|
|
expect(delayedFunctionScheduler.removeFunctionWithId).toHaveBeenCalledWith(
|
|
123
|
|
);
|
|
});
|
|
|
|
it('gives you a friendly reminder if the Clock is not installed and you tick', function() {
|
|
const clock = new jasmineUnderTest.Clock(
|
|
{},
|
|
jasmine.createSpyObj('delayedFunctionScheduler', ['tick'])
|
|
);
|
|
expect(function() {
|
|
clock.tick(50);
|
|
}).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Clock (acceptance)', function() {
|
|
it('can run setTimeouts/setIntervals synchronously', function() {
|
|
const delayedFn1 = jasmine.createSpy('delayedFn1'),
|
|
delayedFn2 = jasmine.createSpy('delayedFn2'),
|
|
delayedFn3 = jasmine.createSpy('delayedFn3'),
|
|
recurring1 = jasmine.createSpy('recurring1'),
|
|
delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: setTimeout },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
|
|
clock.setTimeout(delayedFn1, 0);
|
|
const intervalId = clock.setInterval(recurring1, 50);
|
|
clock.setTimeout(delayedFn2, 100);
|
|
clock.setTimeout(delayedFn3, 200);
|
|
|
|
expect(delayedFn1).not.toHaveBeenCalled();
|
|
expect(delayedFn2).not.toHaveBeenCalled();
|
|
expect(delayedFn3).not.toHaveBeenCalled();
|
|
|
|
clock.tick(0);
|
|
|
|
expect(delayedFn1).toHaveBeenCalled();
|
|
expect(delayedFn2).not.toHaveBeenCalled();
|
|
expect(delayedFn3).not.toHaveBeenCalled();
|
|
|
|
clock.tick(50);
|
|
|
|
expect(recurring1).toHaveBeenCalled();
|
|
expect(recurring1.calls.count()).toBe(1);
|
|
expect(delayedFn2).not.toHaveBeenCalled();
|
|
expect(delayedFn3).not.toHaveBeenCalled();
|
|
|
|
clock.tick(50);
|
|
|
|
expect(recurring1.calls.count()).toBe(2);
|
|
expect(delayedFn2).toHaveBeenCalled();
|
|
expect(delayedFn3).not.toHaveBeenCalled();
|
|
|
|
clock.tick(100);
|
|
|
|
expect(recurring1.calls.count()).toBe(4);
|
|
expect(delayedFn3).toHaveBeenCalled();
|
|
|
|
clock.clearInterval(intervalId);
|
|
clock.tick(50);
|
|
|
|
expect(recurring1.calls.count()).toBe(4);
|
|
});
|
|
|
|
describe('auto tick mode', () => {
|
|
let delayedFunctionScheduler;
|
|
let mockDate;
|
|
let clock;
|
|
|
|
beforeEach(() => {
|
|
delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler();
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
};
|
|
clock = new jasmineUnderTest.Clock(
|
|
// We use the real window for global or firefox is displeased when we try to call a real setTimeout on an object "that doesn't implement window".
|
|
typeof window !== 'undefined' ? window : { setTimeout: setTimeout },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
clock.install();
|
|
clock.autoTick();
|
|
});
|
|
|
|
afterEach(() => {
|
|
clock.uninstall();
|
|
});
|
|
|
|
it('can run setTimeouts/setIntervals asynchronously', function() {
|
|
const recurring = jasmine.createSpy('recurring'),
|
|
fn1 = jasmine.createSpy('fn1'),
|
|
fn2 = jasmine.createSpy('fn2'),
|
|
fn3 = jasmine.createSpy('fn3');
|
|
|
|
const intervalId = clock.setInterval(recurring, 50);
|
|
// In a microtask, add some timeouts.
|
|
Promise.resolve()
|
|
.then(function() {
|
|
return new Promise(function(resolve) {
|
|
clock.setTimeout(resolve, 25);
|
|
});
|
|
})
|
|
.then(function() {
|
|
fn1();
|
|
return new Promise(function(resolve) {
|
|
clock.setTimeout(resolve, 200);
|
|
});
|
|
})
|
|
.then(function() {
|
|
fn2();
|
|
return new Promise(function(resolve) {
|
|
clock.setTimeout(resolve, 100);
|
|
});
|
|
})
|
|
.then(function() {
|
|
fn3();
|
|
});
|
|
|
|
expect(recurring).not.toHaveBeenCalled();
|
|
expect(fn1).not.toHaveBeenCalled();
|
|
expect(fn2).not.toHaveBeenCalled();
|
|
expect(fn3).not.toHaveBeenCalled();
|
|
|
|
return new Promise(resolve => clock.setTimeout(resolve, 50))
|
|
.then(function() {
|
|
expect(recurring).toHaveBeenCalledTimes(1);
|
|
expect(fn1).toHaveBeenCalled();
|
|
expect(fn2).not.toHaveBeenCalled();
|
|
expect(fn3).not.toHaveBeenCalled();
|
|
|
|
return new Promise(resolve => clock.setTimeout(resolve, 175));
|
|
})
|
|
.then(function() {
|
|
expect(recurring).toHaveBeenCalledTimes(4);
|
|
expect(fn1).toHaveBeenCalled();
|
|
expect(fn2).toHaveBeenCalled();
|
|
expect(fn3).not.toHaveBeenCalled();
|
|
|
|
clock.clearInterval(intervalId);
|
|
return new Promise(resolve => clock.setTimeout(resolve, 100));
|
|
})
|
|
.then(function() {
|
|
expect(recurring).toHaveBeenCalledTimes(4);
|
|
expect(fn1).toHaveBeenCalled();
|
|
expect(fn2).toHaveBeenCalled();
|
|
expect(fn3).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('speeds up the execution of the timers in all browsers', async () => {
|
|
const startTimeMs = performance.now() / 1000;
|
|
await new Promise(resolve => clock.setTimeout(resolve, 5000));
|
|
await new Promise(resolve => clock.setTimeout(resolve, 5000));
|
|
await new Promise(resolve => clock.setTimeout(resolve, 5000));
|
|
await new Promise(resolve => clock.setTimeout(resolve, 5000));
|
|
const endTimeMs = performance.now() / 1000;
|
|
// Ensure we didn't take 20s to complete the awaits above and, in fact, can do it in a fraction of a second
|
|
expect(endTimeMs - startTimeMs).toBeLessThan(100);
|
|
});
|
|
|
|
it('avoids throttling in browsers other than Safari', async () => {
|
|
if (
|
|
typeof navigator !== 'undefined' &&
|
|
/^((?!chrome|android|firefox).)*safari/i.test(navigator.userAgent)
|
|
) {
|
|
return;
|
|
}
|
|
// This test ensures the setTimeout loop isn't getting throttled by browsers
|
|
const promises = [];
|
|
// 2000 timers at ~4ms throttling = 8_000ms would time out if we weren't
|
|
// preventing the throttle with the MessageChannel trick.
|
|
for (let i = 0; i < 2000; i++) {
|
|
promises.push(new Promise(resolve => clock.setTimeout(resolve)));
|
|
}
|
|
const startTimeMs = performance.now() / 1000;
|
|
await Promise.all(promises);
|
|
const endTimeMs = performance.now() / 1000;
|
|
expect(endTimeMs - startTimeMs).toBeLessThan(1000);
|
|
});
|
|
|
|
it('is easy to test async functions with interleaved timers and microtasks', async () => {
|
|
async function blackBoxWithLotsOfAsyncStuff() {
|
|
await new Promise(r => clock.setTimeout(r, 10));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await new Promise(r => clock.setTimeout(r, 20));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
return 'done';
|
|
}
|
|
const result = await blackBoxWithLotsOfAsyncStuff();
|
|
expect(result).toBe('done');
|
|
});
|
|
});
|
|
|
|
it('can clear a previously set timeout', function() {
|
|
const clearedFn = jasmine.createSpy('clearedFn'),
|
|
delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: function() {} },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
|
|
const timeoutId = clock.setTimeout(clearedFn, 100);
|
|
expect(clearedFn).not.toHaveBeenCalled();
|
|
|
|
clock.clearTimeout(timeoutId);
|
|
clock.tick(100);
|
|
|
|
expect(clearedFn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("can clear a previously set interval using that interval's handler", function() {
|
|
const spy = jasmine.createSpy('spy'),
|
|
delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setInterval: function() {} },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
|
|
const intervalId = clock.setInterval(function() {
|
|
spy();
|
|
clock.clearInterval(intervalId);
|
|
}, 100);
|
|
clock.tick(200);
|
|
|
|
expect(spy.calls.count()).toEqual(1);
|
|
});
|
|
|
|
it('correctly schedules functions after the Clock has advanced', function() {
|
|
const delayedFn1 = jasmine.createSpy('delayedFn1'),
|
|
delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: function() {} },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
|
|
clock.tick(100);
|
|
clock.setTimeout(delayedFn1, 10, ['some', 'arg']);
|
|
clock.tick(5);
|
|
expect(delayedFn1).not.toHaveBeenCalled();
|
|
clock.tick(5);
|
|
expect(delayedFn1).toHaveBeenCalled();
|
|
});
|
|
|
|
it('correctly schedules functions while the Clock is advancing', function() {
|
|
const delayedFn1 = jasmine.createSpy('delayedFn1'),
|
|
delayedFn2 = jasmine.createSpy('delayedFn2'),
|
|
delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: function() {} },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
delayedFn1.and.callFake(function() {
|
|
clock.setTimeout(delayedFn2, 0);
|
|
});
|
|
clock.install();
|
|
clock.setTimeout(delayedFn1, 5);
|
|
|
|
clock.tick(5);
|
|
expect(delayedFn1).toHaveBeenCalled();
|
|
expect(delayedFn2).not.toHaveBeenCalled();
|
|
|
|
clock.tick();
|
|
expect(delayedFn2).toHaveBeenCalled();
|
|
});
|
|
|
|
it('correctly calls functions scheduled while the Clock is advancing', function() {
|
|
const delayedFn1 = jasmine.createSpy('delayedFn1'),
|
|
delayedFn2 = jasmine.createSpy('delayedFn2'),
|
|
delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: function() {} },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
delayedFn1.and.callFake(function() {
|
|
clock.setTimeout(delayedFn2, 1);
|
|
});
|
|
clock.install();
|
|
clock.setTimeout(delayedFn1, 5);
|
|
|
|
clock.tick(6);
|
|
expect(delayedFn1).toHaveBeenCalled();
|
|
expect(delayedFn2).toHaveBeenCalled();
|
|
});
|
|
|
|
it('correctly schedules functions scheduled while the Clock is advancing but after the Clock is uninstalled', function() {
|
|
const delayedFn1 = jasmine.createSpy('delayedFn1'),
|
|
delayedFn2 = jasmine.createSpy('delayedFn2'),
|
|
delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
mockDate = {
|
|
install: function() {},
|
|
tick: function() {},
|
|
uninstall: function() {}
|
|
},
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: function() {} },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
delayedFn1.and.callFake(function() {
|
|
clock.uninstall();
|
|
clock.install();
|
|
clock.setTimeout(delayedFn2, 0);
|
|
});
|
|
|
|
clock.install();
|
|
clock.setTimeout(delayedFn1, 1);
|
|
|
|
clock.tick(1);
|
|
expect(delayedFn1).toHaveBeenCalled();
|
|
expect(delayedFn2).not.toHaveBeenCalled();
|
|
|
|
clock.tick(1);
|
|
expect(delayedFn2).toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not mock the Date object by default', function() {
|
|
const delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
global = { Date: Date },
|
|
mockDate = new jasmineUnderTest.MockDate(global),
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: setTimeout },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
|
|
expect(global.Date).toEqual(Date);
|
|
|
|
const now = new global.Date().getTime();
|
|
|
|
clock.tick(50);
|
|
|
|
expect(new global.Date().getTime() - now).not.toEqual(50);
|
|
});
|
|
|
|
it('mocks the Date object and sets it to current time', function() {
|
|
const delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
global = { Date: Date },
|
|
mockDate = new jasmineUnderTest.MockDate(global),
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: setTimeout },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install().mockDate();
|
|
|
|
const now = new global.Date().getTime();
|
|
|
|
clock.tick(50);
|
|
|
|
expect(new global.Date().getTime() - now).toEqual(50);
|
|
|
|
let timeoutDate = 0;
|
|
clock.setTimeout(function() {
|
|
timeoutDate = new global.Date().getTime();
|
|
}, 100);
|
|
|
|
clock.tick(100);
|
|
|
|
expect(timeoutDate - now).toEqual(150);
|
|
});
|
|
|
|
it('mocks the Date object and sets it to a given time', function() {
|
|
const delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
global = { Date: Date },
|
|
mockDate = new jasmineUnderTest.MockDate(global),
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: setTimeout },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
),
|
|
baseTime = new Date(2013, 9, 23);
|
|
|
|
clock.install().mockDate(baseTime);
|
|
|
|
const now = new global.Date().getTime();
|
|
|
|
expect(now).toEqual(baseTime.getTime());
|
|
|
|
clock.tick(50);
|
|
|
|
expect(new global.Date().getTime()).toEqual(baseTime.getTime() + 50);
|
|
|
|
let timeoutDate = 0;
|
|
clock.setTimeout(function() {
|
|
timeoutDate = new global.Date().getTime();
|
|
}, 100);
|
|
|
|
clock.tick(100);
|
|
|
|
expect(timeoutDate).toEqual(baseTime.getTime() + 150);
|
|
});
|
|
|
|
it('throws mockDate is called with a non-Date', function() {
|
|
const delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
global = { Date: Date },
|
|
mockDate = new jasmineUnderTest.MockDate(global),
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: setTimeout },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
expect(() => clock.mockDate(12345)).toThrowError(
|
|
'The argument to jasmine.clock().mockDate(), if specified, should be ' +
|
|
'a Date instance.'
|
|
);
|
|
});
|
|
|
|
it('mocks the Date object and updates the date per delayed function', function() {
|
|
const delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
global = { Date: Date },
|
|
mockDate = new jasmineUnderTest.MockDate(global),
|
|
clock = new jasmineUnderTest.Clock(
|
|
{ setTimeout: setTimeout },
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
),
|
|
baseTime = new Date();
|
|
|
|
clock.install().mockDate(baseTime);
|
|
|
|
const actualTimes = [];
|
|
const pushCurrentTime = function() {
|
|
actualTimes.push(global.Date().getTime());
|
|
};
|
|
delayedFunctionScheduler.scheduleFunction(pushCurrentTime);
|
|
delayedFunctionScheduler.scheduleFunction(pushCurrentTime, 1);
|
|
delayedFunctionScheduler.scheduleFunction(pushCurrentTime, 3);
|
|
|
|
clock.tick(1);
|
|
expect(global.Date().getTime()).toEqual(baseTime.getTime() + 1);
|
|
|
|
clock.tick(3);
|
|
expect(global.Date().getTime()).toEqual(baseTime.getTime() + 4);
|
|
|
|
clock.tick(1);
|
|
expect(global.Date().getTime()).toEqual(baseTime.getTime() + 5);
|
|
|
|
expect(actualTimes).toEqual([
|
|
baseTime.getTime(),
|
|
baseTime.getTime() + 1,
|
|
baseTime.getTime() + 3
|
|
]);
|
|
});
|
|
|
|
it('correctly clears a scheduled timeout while the Clock is advancing', function() {
|
|
const delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
global = { Date: Date, setTimeout: undefined },
|
|
mockDate = new jasmineUnderTest.MockDate(global),
|
|
clock = new jasmineUnderTest.Clock(
|
|
global,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
|
|
let timerId2;
|
|
|
|
global.setTimeout(function() {
|
|
global.clearTimeout(timerId2);
|
|
}, 100);
|
|
|
|
timerId2 = global.setTimeout(fail, 100);
|
|
|
|
clock.tick(100);
|
|
});
|
|
|
|
it('correctly clears a scheduled interval while the Clock is advancing', function() {
|
|
const delayedFunctionScheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
|
|
global = { Date: Date, setTimeout: undefined },
|
|
mockDate = new jasmineUnderTest.MockDate(global),
|
|
clock = new jasmineUnderTest.Clock(
|
|
global,
|
|
function() {
|
|
return delayedFunctionScheduler;
|
|
},
|
|
mockDate
|
|
);
|
|
|
|
clock.install();
|
|
|
|
let timerId2;
|
|
global.setInterval(function() {
|
|
global.clearInterval(timerId2);
|
|
}, 100);
|
|
|
|
timerId2 = global.setInterval(fail, 100);
|
|
|
|
clock.tick(400);
|
|
});
|
|
});
|