Merge branch 'atscott-autoTick'

* Merges #2042 from @atscott
* Fixes #1932
* Fixes #1725
This commit is contained in:
Steve Gravrock
2025-03-21 09:19:56 -07:00
5 changed files with 426 additions and 0 deletions

View File

@@ -687,6 +687,142 @@ describe('Clock (acceptance)', function() {
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(),

View File

@@ -264,6 +264,42 @@ describe('DelayedFunctionScheduler', function() {
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');