Files
jasmine/spec/core/DelayedFunctionSchedulerSpec.js
Andrew Scott d31a431d1f fix(clock): Avoid generating timers with IDs that conflict with native
This commit attempts to ensure that the timers created by jasmine mock
clock do not conflict with the native timers. This also retains
pre-existing behavior whereby a native scheduled function cannot be
cleared if it was created prior to the mock clock being installed
(unless the mock clock is uninstalled first).

Prior to this commit, attempting to clear a native timer would result in
clearing a mocked scheduled function instead, in some scenarios where
the IDs conflicted.

fixes #2068
2025-07-14 16:55:05 -07:00

393 lines
12 KiB
JavaScript

describe('DelayedFunctionScheduler', function() {
it('schedules a function for later execution', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
fn = jasmine.createSpy('fn');
scheduler.scheduleFunction(fn, 0);
expect(fn).not.toHaveBeenCalled();
scheduler.tick(0);
expect(fn).toHaveBeenCalled();
});
it('schedules a string for later execution', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
strfn = 'horrible = true;';
scheduler.scheduleFunction(strfn, 0);
scheduler.tick(0);
expect(horrible).toEqual(true);
});
it('#tick defaults to 0', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
fn = jasmine.createSpy('fn');
scheduler.scheduleFunction(fn, 0);
expect(fn).not.toHaveBeenCalled();
scheduler.tick();
expect(fn).toHaveBeenCalled();
});
it('defaults delay to 0', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
fn = jasmine.createSpy('fn');
scheduler.scheduleFunction(fn);
expect(fn).not.toHaveBeenCalled();
scheduler.tick(0);
expect(fn).toHaveBeenCalled();
});
it('optionally passes params to scheduled functions', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
fn = jasmine.createSpy('fn');
scheduler.scheduleFunction(fn, 0, ['foo', 'bar']);
expect(fn).not.toHaveBeenCalled();
scheduler.tick(0);
expect(fn).toHaveBeenCalledWith('foo', 'bar');
});
it('scheduled fns can optionally reoccur', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
fn = jasmine.createSpy('fn');
scheduler.scheduleFunction(fn, 20, [], true);
expect(fn).not.toHaveBeenCalled();
scheduler.tick(20);
expect(fn.calls.count()).toBe(1);
scheduler.tick(40);
expect(fn.calls.count()).toBe(3);
scheduler.tick(21);
expect(fn.calls.count()).toBe(4);
});
it('increments scheduled fns ids unless one is passed', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler();
const initial = scheduler.scheduleFunction(function() {}, 0);
expect(scheduler.scheduleFunction(function() {}, 0)).toBe(initial + 1);
expect(scheduler.scheduleFunction(function() {}, 0)).toBe(initial + 2);
expect(scheduler.scheduleFunction(function() {}, 0, [], false, 123)).toBe(
123
);
expect(scheduler.scheduleFunction(function() {}, 0)).toBe(initial + 3);
});
it('#removeFunctionWithId removes a previously scheduled function with a given id', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
fn = jasmine.createSpy('fn'),
timeoutKey = scheduler.scheduleFunction(fn, 0);
expect(fn).not.toHaveBeenCalled();
scheduler.removeFunctionWithId(timeoutKey);
scheduler.tick(0);
expect(fn).not.toHaveBeenCalled();
});
it('executes recurring functions interleaved with regular functions in the correct order', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler();
const fn = jasmine.createSpy('fn');
let recurringCallCount = 0;
const recurring = jasmine.createSpy('recurring').and.callFake(function() {
recurringCallCount++;
if (recurringCallCount < 5) {
expect(fn).not.toHaveBeenCalled();
}
});
scheduler.scheduleFunction(recurring, 10, [], true);
scheduler.scheduleFunction(fn, 50);
scheduler.tick(60);
expect(recurring).toHaveBeenCalled();
expect(recurring.calls.count()).toBe(6);
expect(fn).toHaveBeenCalled();
});
it('schedules a function for later execution during a tick', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
fn = jasmine.createSpy('fn'),
fnDelay = 10;
scheduler.scheduleFunction(function() {
scheduler.scheduleFunction(fn, fnDelay);
}, 0);
expect(fn).not.toHaveBeenCalled();
scheduler.tick(fnDelay);
expect(fn).toHaveBeenCalled();
});
it('#removeFunctionWithId removes a previously scheduled function with a given id during a tick', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
fn = jasmine.createSpy('fn'),
fnDelay = 10;
let timeoutKey;
scheduler.scheduleFunction(function() {
scheduler.removeFunctionWithId(timeoutKey);
}, 0);
timeoutKey = scheduler.scheduleFunction(fn, fnDelay);
expect(fn).not.toHaveBeenCalled();
scheduler.tick(fnDelay);
expect(fn).not.toHaveBeenCalled();
});
it('executes recurring functions interleaved with regular functions and functions scheduled during a tick in the correct order', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler();
const fn = jasmine.createSpy('fn');
let recurringCallCount = 0;
const recurring = jasmine.createSpy('recurring').and.callFake(function() {
recurringCallCount++;
if (recurringCallCount < 5) {
expect(fn).not.toHaveBeenCalled();
}
});
const innerFn = jasmine.createSpy('innerFn').and.callFake(function() {
expect(recurring.calls.count()).toBe(4);
expect(fn).not.toHaveBeenCalled();
});
const scheduling = jasmine.createSpy('scheduling').and.callFake(function() {
expect(recurring.calls.count()).toBe(3);
expect(fn).not.toHaveBeenCalled();
scheduler.scheduleFunction(innerFn, 10); // 41ms absolute
});
scheduler.scheduleFunction(recurring, 10, [], true);
scheduler.scheduleFunction(fn, 50);
scheduler.scheduleFunction(scheduling, 31);
scheduler.tick(60);
expect(recurring).toHaveBeenCalled();
expect(recurring.calls.count()).toBe(6);
expect(fn).toHaveBeenCalled();
expect(scheduling).toHaveBeenCalled();
expect(innerFn).toHaveBeenCalled();
});
it('executes recurring functions after rescheduling them', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
recurring = function() {
expect(scheduler.scheduleFunction).toHaveBeenCalled();
};
scheduler.scheduleFunction(recurring, 10, [], true);
spyOn(scheduler, 'scheduleFunction');
scheduler.tick(10);
});
it('removes functions during a tick that runs the function', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
spy = jasmine.createSpy('fn'),
spyAndRemove = jasmine.createSpy('fn'),
fnDelay = 10;
let timeoutKey;
spyAndRemove.and.callFake(function() {
scheduler.removeFunctionWithId(timeoutKey);
});
scheduler.scheduleFunction(spyAndRemove, fnDelay);
timeoutKey = scheduler.scheduleFunction(spy, fnDelay, [], true);
scheduler.tick(2 * fnDelay);
expect(spy).not.toHaveBeenCalled();
expect(spyAndRemove).toHaveBeenCalled();
});
it('removes functions during the first tick that runs the function', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
fn = jasmine.createSpy('fn'),
fnDelay = 10;
let timeoutKey;
timeoutKey = scheduler.scheduleFunction(fn, fnDelay, [], true);
scheduler.scheduleFunction(function() {
scheduler.removeFunctionWithId(timeoutKey);
}, fnDelay);
expect(fn).not.toHaveBeenCalled();
scheduler.tick(3 * fnDelay);
expect(fn).toHaveBeenCalled();
expect(fn.calls.count()).toBe(1);
});
it("does not remove a function that hasn't been added yet", function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
fn = jasmine.createSpy('fn'),
fnDelay = 10;
scheduler.removeFunctionWithId('foo');
scheduler.scheduleFunction(fn, fnDelay, [], false, 'foo');
expect(fn).not.toHaveBeenCalled();
scheduler.tick(fnDelay + 1);
expect(fn).toHaveBeenCalled();
});
it('runs the next scheduled funtion', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler();
const fn = jasmine.createSpy('fn');
const tickSpy = jasmine.createSpy('tick');
scheduler.scheduleFunction(fn, 10, [], false, 'foo');
expect(fn).not.toHaveBeenCalled();
scheduler.runNextQueuedFunction(tickSpy);
expect(fn).toHaveBeenCalled();
expect(tickSpy).toHaveBeenCalledWith(10);
});
it('runs the only a single scheduled funtion in a time slot', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler();
const fn1 = jasmine.createSpy('fn');
const fn2 = jasmine.createSpy('fn2');
const tickSpy = jasmine.createSpy('tick');
scheduler.scheduleFunction(fn1, 10, [], false, 'foo1');
scheduler.scheduleFunction(fn2, 10, [], false, 'foo2');
scheduler.runNextQueuedFunction(tickSpy);
expect(fn1).toHaveBeenCalled();
expect(fn2).not.toHaveBeenCalled();
expect(tickSpy).toHaveBeenCalledWith(10);
tickSpy.calls.reset();
scheduler.runNextQueuedFunction(tickSpy);
expect(fn2).toHaveBeenCalled();
expect(tickSpy).toHaveBeenCalledWith(0);
});
it('updates the mockDate per scheduled time', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler(),
tickDate = jasmine.createSpy('tickDate');
scheduler.scheduleFunction(function() {});
scheduler.scheduleFunction(function() {}, 1);
scheduler.tick(1, tickDate);
expect(tickDate).toHaveBeenCalledWith(0);
expect(tickDate).toHaveBeenCalledWith(1);
});
it('does not conflict with native timer IDs', function() {
const NODE_JS =
typeof process !== 'undefined' &&
process.versions &&
typeof process.versions.node === 'string';
if (NODE_JS) {
pending('numeric timer ID conflicts only relevant for browsers.');
}
const nativeTimeoutId = setTimeout(function() {}, 100);
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler();
const fn = jasmine.createSpy('fn');
for (let i = 0; i < nativeTimeoutId; i++) {
scheduler.scheduleFunction(fn, 0, [], false);
}
scheduler.removeFunctionWithId(nativeTimeoutId);
scheduler.tick(1);
expect(fn).toHaveBeenCalledTimes(nativeTimeoutId);
});
describe('ticking inside a scheduled function', function() {
let clock;
// Runner function calls the callback until it returns false
function runWork(workCallback) {
while (workCallback()) {}
}
// Make a worker that takes a little time and tracks when it finished
function mockWork(times) {
return () => {
clock.tick(1);
const now = new Date().getTime();
expect(lastWork)
.withContext('Previous function calls should always be in the past')
.toBeLessThan(now);
lastWork = now;
times--;
return times > 0;
};
}
let lastWork = 0;
beforeEach(() => {
clock = jasmineUnderTest.getEnv().clock;
clock.install();
clock.mockDate(new Date(1));
});
afterEach(function() {
jasmineUnderTest.getEnv().clock.uninstall();
});
it('preserves monotonically-increasing current time', () => {
const work1 = mockWork(3);
setTimeout(() => {
runWork(work1);
}, 1);
clock.tick(1);
expect(lastWork)
.withContext('tick should advance past last-scheduled function')
.toBeLessThanOrEqual(new Date().getTime());
const work2 = mockWork(3);
setTimeout(() => {
runWork(work2);
}, 1);
clock.tick(1);
expect(lastWork)
.withContext('tick should advance past last-scheduled function')
.toBeLessThanOrEqual(new Date().getTime());
});
});
});