feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2018, Sinon.JS
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+16
View File
@@ -0,0 +1,16 @@
# commons
[![CircleCI](https://circleci.com/gh/sinonjs/commons.svg?style=svg)](https://circleci.com/gh/sinonjs/commons)
[![codecov](https://codecov.io/gh/sinonjs/commons/branch/master/graph/badge.svg)](https://codecov.io/gh/sinonjs/commons)
<a href="CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Contributor Covenant" /></a>
Simple functions shared among the sinon end user libraries
## Rules
- Follows the [Sinon.JS compatibility](https://github.com/sinonjs/sinon/blob/master/CONTRIBUTING.md#compatibility)
- 100% test coverage
- Code formatted using [Prettier](https://prettier.io)
- No side effects welcome! (only pure functions)
- No platform specific functions
- One export per file (any bundler can do tree shaking)
+55
View File
@@ -0,0 +1,55 @@
"use strict";
var every = require("./prototypes/array").every;
/**
* @private
*/
function hasCallsLeft(callMap, spy) {
if (callMap[spy.id] === undefined) {
callMap[spy.id] = 0;
}
return callMap[spy.id] < spy.callCount;
}
/**
* @private
*/
function checkAdjacentCalls(callMap, spy, index, spies) {
var calledBeforeNext = true;
if (index !== spies.length - 1) {
calledBeforeNext = spy.calledBefore(spies[index + 1]);
}
if (hasCallsLeft(callMap, spy) && calledBeforeNext) {
callMap[spy.id] += 1;
return true;
}
return false;
}
/**
* A Sinon proxy object (fake, spy, stub)
* @typedef {object} SinonProxy
* @property {Function} calledBefore - A method that determines if this proxy was called before another one
* @property {string} id - Some id
* @property {number} callCount - Number of times this proxy has been called
*/
/**
* Returns true when the spies have been called in the order they were supplied in
* @param {SinonProxy[] | SinonProxy} spies An array of proxies, or several proxies as arguments
* @returns {boolean} true when spies are called in order, false otherwise
*/
function calledInOrder(spies) {
var callMap = {};
// eslint-disable-next-line no-underscore-dangle
var _spies = arguments.length > 1 ? arguments : spies;
return every(_spies, checkAdjacentCalls.bind(null, callMap));
}
module.exports = calledInOrder;
+121
View File
@@ -0,0 +1,121 @@
"use strict";
var assert = require("@sinonjs/referee-sinon").assert;
var calledInOrder = require("./called-in-order");
var sinon = require("@sinonjs/referee-sinon").sinon;
var testObject1 = {
someFunction: function () {
return;
},
};
var testObject2 = {
otherFunction: function () {
return;
},
};
var testObject3 = {
thirdFunction: function () {
return;
},
};
function testMethod() {
testObject1.someFunction();
testObject2.otherFunction();
testObject2.otherFunction();
testObject2.otherFunction();
testObject3.thirdFunction();
}
describe("calledInOrder", function () {
beforeEach(function () {
sinon.stub(testObject1, "someFunction");
sinon.stub(testObject2, "otherFunction");
sinon.stub(testObject3, "thirdFunction");
testMethod();
});
afterEach(function () {
testObject1.someFunction.restore();
testObject2.otherFunction.restore();
testObject3.thirdFunction.restore();
});
describe("given single array argument", function () {
describe("when stubs were called in expected order", function () {
it("returns true", function () {
assert.isTrue(
calledInOrder([
testObject1.someFunction,
testObject2.otherFunction,
])
);
assert.isTrue(
calledInOrder([
testObject1.someFunction,
testObject2.otherFunction,
testObject2.otherFunction,
testObject3.thirdFunction,
])
);
});
});
describe("when stubs were called in unexpected order", function () {
it("returns false", function () {
assert.isFalse(
calledInOrder([
testObject2.otherFunction,
testObject1.someFunction,
])
);
assert.isFalse(
calledInOrder([
testObject2.otherFunction,
testObject1.someFunction,
testObject1.someFunction,
testObject3.thirdFunction,
])
);
});
});
});
describe("given multiple arguments", function () {
describe("when stubs were called in expected order", function () {
it("returns true", function () {
assert.isTrue(
calledInOrder(
testObject1.someFunction,
testObject2.otherFunction
)
);
assert.isTrue(
calledInOrder(
testObject1.someFunction,
testObject2.otherFunction,
testObject3.thirdFunction
)
);
});
});
describe("when stubs were called in unexpected order", function () {
it("returns false", function () {
assert.isFalse(
calledInOrder(
testObject2.otherFunction,
testObject1.someFunction
)
);
assert.isFalse(
calledInOrder(
testObject2.otherFunction,
testObject1.someFunction,
testObject3.thirdFunction
)
);
});
});
});
});
+13
View File
@@ -0,0 +1,13 @@
"use strict";
/**
* Returns a display name for a value from a constructor
* @param {object} value A value to examine
* @returns {(string|null)} A string or null
*/
function className(value) {
const name = value.constructor && value.constructor.name;
return name || null;
}
module.exports = className;
+37
View File
@@ -0,0 +1,37 @@
"use strict";
/* eslint-disable no-empty-function */
var assert = require("@sinonjs/referee").assert;
var className = require("./class-name");
describe("className", function () {
it("returns the class name of an instance", function () {
// Because eslint-config-sinon disables es6, we can't
// use a class definition here
// https://github.com/sinonjs/eslint-config-sinon/blob/master/index.js
// var instance = new (class TestClass {})();
var instance = new (function TestClass() {})();
var name = className(instance);
assert.equals(name, "TestClass");
});
it("returns 'Object' for {}", function () {
var name = className({});
assert.equals(name, "Object");
});
it("returns null for an object that has no prototype", function () {
var obj = Object.create(null);
var name = className(obj);
assert.equals(name, null);
});
it("returns null for an object whose prototype was mangled", function () {
// This is what Node v6 and v7 do for objects returned by querystring.parse()
function MangledObject() {}
MangledObject.prototype = Object.create(null);
var obj = new MangledObject();
var name = className(obj);
assert.equals(name, null);
});
});
+48
View File
@@ -0,0 +1,48 @@
/* eslint-disable no-console */
"use strict";
/**
* Returns a function that will invoke the supplied function and print a
* deprecation warning to the console each time it is called.
* @param {Function} func
* @param {string} msg
* @returns {Function}
*/
exports.wrap = function (func, msg) {
var wrapped = function () {
exports.printWarning(msg);
return func.apply(this, arguments);
};
if (func.prototype) {
wrapped.prototype = func.prototype;
}
return wrapped;
};
/**
* Returns a string which can be supplied to `wrap()` to notify the user that a
* particular part of the sinon API has been deprecated.
* @param {string} packageName
* @param {string} funcName
* @returns {string}
*/
exports.defaultMsg = function (packageName, funcName) {
return `${packageName}.${funcName} is deprecated and will be removed from the public API in a future version of ${packageName}.`;
};
/**
* Prints a warning on the console, when it exists
* @param {string} msg
* @returns {undefined}
*/
exports.printWarning = function (msg) {
/* istanbul ignore next */
if (typeof process === "object" && process.emitWarning) {
// Emit Warnings in Node
process.emitWarning(msg);
} else if (console.info) {
console.info(msg);
} else {
console.log(msg);
}
};
+101
View File
@@ -0,0 +1,101 @@
/* eslint-disable no-console */
"use strict";
var assert = require("@sinonjs/referee-sinon").assert;
var sinon = require("@sinonjs/referee-sinon").sinon;
var deprecated = require("./deprecated");
var msg = "test";
describe("deprecated", function () {
describe("defaultMsg", function () {
it("should return a string", function () {
assert.equals(
deprecated.defaultMsg("sinon", "someFunc"),
"sinon.someFunc is deprecated and will be removed from the public API in a future version of sinon."
);
});
});
describe("printWarning", function () {
beforeEach(function () {
sinon.replace(process, "emitWarning", sinon.fake());
});
afterEach(sinon.restore);
describe("when `process.emitWarning` is defined", function () {
it("should call process.emitWarning with a msg", function () {
deprecated.printWarning(msg);
assert.calledOnceWith(process.emitWarning, msg);
});
});
describe("when `process.emitWarning` is undefined", function () {
beforeEach(function () {
sinon.replace(console, "info", sinon.fake());
sinon.replace(console, "log", sinon.fake());
process.emitWarning = undefined;
});
afterEach(sinon.restore);
describe("when `console.info` is defined", function () {
it("should call `console.info` with a message", function () {
deprecated.printWarning(msg);
assert.calledOnceWith(console.info, msg);
});
});
describe("when `console.info` is undefined", function () {
it("should call `console.log` with a message", function () {
console.info = undefined;
deprecated.printWarning(msg);
assert.calledOnceWith(console.log, msg);
});
});
});
});
describe("wrap", function () {
// eslint-disable-next-line mocha/no-setup-in-describe
var method = sinon.fake();
var wrapped;
beforeEach(function () {
wrapped = deprecated.wrap(method, msg);
});
it("should return a wrapper function", function () {
assert.match(wrapped, sinon.match.func);
});
it("should assign the prototype of the passed method", function () {
assert.equals(method.prototype, wrapped.prototype);
});
context("when the passed method has falsy prototype", function () {
it("should not be assigned to the wrapped method", function () {
method.prototype = null;
wrapped = deprecated.wrap(method, msg);
assert.match(wrapped.prototype, sinon.match.object);
});
});
context("when invoking the wrapped function", function () {
before(function () {
sinon.replace(deprecated, "printWarning", sinon.fake());
wrapped({});
});
it("should call `printWarning` before invoking", function () {
assert.calledOnceWith(deprecated.printWarning, msg);
});
it("should invoke the passed method with the given arguments", function () {
assert.calledOnceWith(method, {});
});
});
});
});
+26
View File
@@ -0,0 +1,26 @@
"use strict";
/**
* Returns true when fn returns true for all members of obj.
* This is an every implementation that works for all iterables
* @param {object} obj
* @param {Function} fn
* @returns {boolean}
*/
module.exports = function every(obj, fn) {
var pass = true;
try {
// eslint-disable-next-line @sinonjs/no-prototype-methods/no-prototype-methods
obj.forEach(function () {
if (!fn.apply(this, arguments)) {
// Throwing an error is the only way to break `forEach`
throw new Error();
}
});
} catch (e) {
pass = false;
}
return pass;
};
+41
View File
@@ -0,0 +1,41 @@
"use strict";
var assert = require("@sinonjs/referee-sinon").assert;
var sinon = require("@sinonjs/referee-sinon").sinon;
var every = require("./every");
describe("util/core/every", function () {
it("returns true when the callback function returns true for every element in an iterable", function () {
var obj = [true, true, true, true];
var allTrue = every(obj, function (val) {
return val;
});
assert(allTrue);
});
it("returns false when the callback function returns false for any element in an iterable", function () {
var obj = [true, true, true, false];
var result = every(obj, function (val) {
return val;
});
assert.isFalse(result);
});
it("calls the given callback once for each item in an iterable until it returns false", function () {
var iterableOne = [true, true, true, true];
var iterableTwo = [true, true, false, true];
var callback = sinon.spy(function (val) {
return val;
});
every(iterableOne, callback);
assert.equals(callback.callCount, 4);
callback.resetHistory();
every(iterableTwo, callback);
assert.equals(callback.callCount, 3);
});
});
+28
View File
@@ -0,0 +1,28 @@
"use strict";
/**
* Returns a display name for a function
* @param {Function} func
* @returns {string}
*/
module.exports = function functionName(func) {
if (!func) {
return "";
}
try {
return (
func.displayName ||
func.name ||
// Use function decomposition as a last resort to get function
// name. Does not rely on function decomposition to work - if it
// doesn't debugging will be slightly less informative
// (i.e. toString will say 'spy' rather than 'myFunc').
(String(func).match(/function ([^\s(]+)/) || [])[1]
);
} catch (e) {
// Stringify may fail and we might get an exception, as a last-last
// resort fall back to empty string.
return "";
}
};
+76
View File
@@ -0,0 +1,76 @@
"use strict";
var jsc = require("jsverify");
var refute = require("@sinonjs/referee-sinon").refute;
var functionName = require("./function-name");
describe("function-name", function () {
it("should return empty string if func is falsy", function () {
jsc.assertForall("falsy", function (fn) {
return functionName(fn) === "";
});
});
it("should use displayName by default", function () {
jsc.assertForall("nestring", function (displayName) {
var fn = { displayName: displayName };
return functionName(fn) === fn.displayName;
});
});
it("should use name if displayName is not available", function () {
jsc.assertForall("nestring", function (name) {
var fn = { name: name };
return functionName(fn) === fn.name;
});
});
it("should fallback to string parsing", function () {
jsc.assertForall("nat", function (naturalNumber) {
var name = `fn${naturalNumber}`;
var fn = {
toString: function () {
return `\nfunction ${name}`;
},
};
return functionName(fn) === name;
});
});
it("should not fail when a name cannot be found", function () {
refute.exception(function () {
var fn = {
toString: function () {
return "\nfunction (";
},
};
functionName(fn);
});
});
it("should not fail when toString is undefined", function () {
refute.exception(function () {
functionName(Object.create(null));
});
});
it("should not fail when toString throws", function () {
refute.exception(function () {
var fn;
try {
// eslint-disable-next-line no-eval
fn = eval("(function*() {})")().constructor;
} catch (e) {
// env doesn't support generators
return;
}
functionName(fn);
});
});
});
+21
View File
@@ -0,0 +1,21 @@
"use strict";
/**
* A reference to the global object
* @type {object} globalObject
*/
var globalObject;
/* istanbul ignore else */
if (typeof global !== "undefined") {
// Node
globalObject = global;
} else if (typeof window !== "undefined") {
// Browser
globalObject = window;
} else {
// WebWorker
globalObject = self;
}
module.exports = globalObject;
+16
View File
@@ -0,0 +1,16 @@
"use strict";
var assert = require("@sinonjs/referee-sinon").assert;
var globalObject = require("./global");
describe("global", function () {
before(function () {
if (typeof global === "undefined") {
this.skip();
}
});
it("is same as global", function () {
assert.same(globalObject, global);
});
});
+14
View File
@@ -0,0 +1,14 @@
"use strict";
module.exports = {
global: require("./global"),
calledInOrder: require("./called-in-order"),
className: require("./class-name"),
deprecated: require("./deprecated"),
every: require("./every"),
functionName: require("./function-name"),
orderByFirstCall: require("./order-by-first-call"),
prototypes: require("./prototypes"),
typeOf: require("./type-of"),
valueToString: require("./value-to-string"),
};
+31
View File
@@ -0,0 +1,31 @@
"use strict";
var assert = require("@sinonjs/referee-sinon").assert;
var index = require("./index");
var expectedMethods = [
"calledInOrder",
"className",
"every",
"functionName",
"orderByFirstCall",
"typeOf",
"valueToString",
];
var expectedObjectProperties = ["deprecated", "prototypes"];
describe("package", function () {
// eslint-disable-next-line mocha/no-setup-in-describe
expectedMethods.forEach(function (name) {
it(`should export a method named ${name}`, function () {
assert.isFunction(index[name]);
});
});
// eslint-disable-next-line mocha/no-setup-in-describe
expectedObjectProperties.forEach(function (name) {
it(`should export an object property named ${name}`, function () {
assert.isObject(index[name]);
});
});
});
+34
View File
@@ -0,0 +1,34 @@
"use strict";
var sort = require("./prototypes/array").sort;
var slice = require("./prototypes/array").slice;
/**
* @private
*/
function comparator(a, b) {
// uuid, won't ever be equal
var aCall = a.getCall(0);
var bCall = b.getCall(0);
var aId = (aCall && aCall.callId) || -1;
var bId = (bCall && bCall.callId) || -1;
return aId < bId ? -1 : 1;
}
/**
* A Sinon proxy object (fake, spy, stub)
* @typedef {object} SinonProxy
* @property {Function} getCall - A method that can return the first call
*/
/**
* Sorts an array of SinonProxy instances (fake, spy, stub) by their first call
* @param {SinonProxy[] | SinonProxy} spies
* @returns {SinonProxy[]}
*/
function orderByFirstCall(spies) {
return sort(slice(spies), comparator);
}
module.exports = orderByFirstCall;
+52
View File
@@ -0,0 +1,52 @@
"use strict";
var assert = require("@sinonjs/referee-sinon").assert;
var knuthShuffle = require("knuth-shuffle").knuthShuffle;
var sinon = require("@sinonjs/referee-sinon").sinon;
var orderByFirstCall = require("./order-by-first-call");
describe("orderByFirstCall", function () {
it("should order an Array of spies by the callId of the first call, ascending", function () {
// create an array of spies
var spies = [
sinon.spy(),
sinon.spy(),
sinon.spy(),
sinon.spy(),
sinon.spy(),
sinon.spy(),
];
// call all the spies
spies.forEach(function (spy) {
spy();
});
// add a few uncalled spies
spies.push(sinon.spy());
spies.push(sinon.spy());
// randomise the order of the spies
knuthShuffle(spies);
var sortedSpies = orderByFirstCall(spies);
assert.equals(sortedSpies.length, spies.length);
var orderedByFirstCall = sortedSpies.every(function (spy, index) {
if (index + 1 === sortedSpies.length) {
return true;
}
var nextSpy = sortedSpies[index + 1];
// uncalled spies should be ordered first
if (!spy.called) {
return true;
}
return spy.calledImmediatelyBefore(nextSpy);
});
assert.isTrue(orderedByFirstCall);
});
});
+43
View File
@@ -0,0 +1,43 @@
# Prototypes
The functions in this folder are to be use for keeping cached references to the built-in prototypes, so that people can't inadvertently break the library by making mistakes in userland.
See https://github.com/sinonjs/sinon/pull/1523
## Without cached references
```js
// in userland, the library user needs to replace the filter method on
// Array.prototype
var array = [1, 2, 3];
sinon.replace(array, "filter", sinon.fake.returns(2));
// in a sinon module, the library author needs to use the filter method
var someArray = ["a", "b", 42, "c"];
var answer = filter(someArray, function (v) {
return v === 42;
});
console.log(answer);
// => 2
```
## With cached references
```js
// in userland, the library user needs to replace the filter method on
// Array.prototype
var array = [1, 2, 3];
sinon.replace(array, "filter", sinon.fake.returns(2));
// in a sinon module, the library author needs to use the filter method
// get a reference to the original Array.prototype.filter
var filter = require("@sinonjs/commons").prototypes.array.filter;
var someArray = ["a", "b", 42, "c"];
var answer = filter(someArray, function (v) {
return v === 42;
});
console.log(answer);
// => 42
```
+5
View File
@@ -0,0 +1,5 @@
"use strict";
var copyPrototype = require("./copy-prototype-methods");
module.exports = copyPrototype(Array.prototype);
@@ -0,0 +1,40 @@
"use strict";
var call = Function.call;
var throwsOnProto = require("./throws-on-proto");
var disallowedProperties = [
// ignore size because it throws from Map
"size",
"caller",
"callee",
"arguments",
];
// This branch is covered when tests are run with `--disable-proto=throw`,
// however we can test both branches at the same time, so this is ignored
/* istanbul ignore next */
if (throwsOnProto) {
disallowedProperties.push("__proto__");
}
module.exports = function copyPrototypeMethods(prototype) {
// eslint-disable-next-line @sinonjs/no-prototype-methods/no-prototype-methods
return Object.getOwnPropertyNames(prototype).reduce(function (
result,
name
) {
if (disallowedProperties.includes(name)) {
return result;
}
if (typeof prototype[name] !== "function") {
return result;
}
result[name] = call.bind(prototype[name]);
return result;
},
Object.create(null));
};
@@ -0,0 +1,12 @@
"use strict";
var refute = require("@sinonjs/referee-sinon").refute;
var copyPrototypeMethods = require("./copy-prototype-methods");
describe("copyPrototypeMethods", function () {
it("does not throw for Map", function () {
refute.exception(function () {
copyPrototypeMethods(Map.prototype);
});
});
});
+5
View File
@@ -0,0 +1,5 @@
"use strict";
var copyPrototype = require("./copy-prototype-methods");
module.exports = copyPrototype(Function.prototype);
+10
View File
@@ -0,0 +1,10 @@
"use strict";
module.exports = {
array: require("./array"),
function: require("./function"),
map: require("./map"),
object: require("./object"),
set: require("./set"),
string: require("./string"),
};
+61
View File
@@ -0,0 +1,61 @@
"use strict";
var assert = require("@sinonjs/referee-sinon").assert;
var arrayProto = require("./index").array;
var functionProto = require("./index").function;
var mapProto = require("./index").map;
var objectProto = require("./index").object;
var setProto = require("./index").set;
var stringProto = require("./index").string;
var throwsOnProto = require("./throws-on-proto");
describe("prototypes", function () {
describe(".array", function () {
// eslint-disable-next-line mocha/no-setup-in-describe
verifyProperties(arrayProto, Array);
});
describe(".function", function () {
// eslint-disable-next-line mocha/no-setup-in-describe
verifyProperties(functionProto, Function);
});
describe(".map", function () {
// eslint-disable-next-line mocha/no-setup-in-describe
verifyProperties(mapProto, Map);
});
describe(".object", function () {
// eslint-disable-next-line mocha/no-setup-in-describe
verifyProperties(objectProto, Object);
});
describe(".set", function () {
// eslint-disable-next-line mocha/no-setup-in-describe
verifyProperties(setProto, Set);
});
describe(".string", function () {
// eslint-disable-next-line mocha/no-setup-in-describe
verifyProperties(stringProto, String);
});
});
function verifyProperties(p, origin) {
var disallowedProperties = ["size", "caller", "callee", "arguments"];
if (throwsOnProto) {
disallowedProperties.push("__proto__");
}
it("should have all the methods of the origin prototype", function () {
var methodNames = Object.getOwnPropertyNames(origin.prototype).filter(
function (name) {
if (disallowedProperties.includes(name)) {
return false;
}
return typeof origin.prototype[name] === "function";
}
);
methodNames.forEach(function (name) {
assert.isTrue(Object.prototype.hasOwnProperty.call(p, name), name);
});
});
}
+5
View File
@@ -0,0 +1,5 @@
"use strict";
var copyPrototype = require("./copy-prototype-methods");
module.exports = copyPrototype(Map.prototype);
+5
View File
@@ -0,0 +1,5 @@
"use strict";
var copyPrototype = require("./copy-prototype-methods");
module.exports = copyPrototype(Object.prototype);
+5
View File
@@ -0,0 +1,5 @@
"use strict";
var copyPrototype = require("./copy-prototype-methods");
module.exports = copyPrototype(Set.prototype);
+5
View File
@@ -0,0 +1,5 @@
"use strict";
var copyPrototype = require("./copy-prototype-methods");
module.exports = copyPrototype(String.prototype);
@@ -0,0 +1,24 @@
"use strict";
/**
* Is true when the environment causes an error to be thrown for accessing the
* __proto__ property.
* This is necessary in order to support `node --disable-proto=throw`.
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto
* @type {boolean}
*/
let throwsOnProto;
try {
const object = {};
// eslint-disable-next-line no-proto, no-unused-expressions
object.__proto__;
throwsOnProto = false;
} catch (_) {
// This branch is covered when tests are run with `--disable-proto=throw`,
// however we can test both branches at the same time, so this is ignored
/* istanbul ignore next */
throwsOnProto = true;
}
module.exports = throwsOnProto;
+12
View File
@@ -0,0 +1,12 @@
"use strict";
var type = require("type-detect");
/**
* Returns the lower-case result of running type from type-detect on the value
* @param {*} value
* @returns {string}
*/
module.exports = function typeOf(value) {
return type(value).toLowerCase();
};
+51
View File
@@ -0,0 +1,51 @@
"use strict";
var assert = require("@sinonjs/referee-sinon").assert;
var typeOf = require("./type-of");
describe("typeOf", function () {
it("returns boolean", function () {
assert.equals(typeOf(false), "boolean");
});
it("returns string", function () {
assert.equals(typeOf("Sinon.JS"), "string");
});
it("returns number", function () {
assert.equals(typeOf(123), "number");
});
it("returns object", function () {
assert.equals(typeOf({}), "object");
});
it("returns function", function () {
assert.equals(
typeOf(function () {
return undefined;
}),
"function"
);
});
it("returns undefined", function () {
assert.equals(typeOf(undefined), "undefined");
});
it("returns null", function () {
assert.equals(typeOf(null), "null");
});
it("returns array", function () {
assert.equals(typeOf([]), "array");
});
it("returns regexp", function () {
assert.equals(typeOf(/.*/), "regexp");
});
it("returns date", function () {
assert.equals(typeOf(new Date()), "date");
});
});
+16
View File
@@ -0,0 +1,16 @@
"use strict";
/**
* Returns a string representation of the value
* @param {*} value
* @returns {string}
*/
function valueToString(value) {
if (value && value.toString) {
// eslint-disable-next-line @sinonjs/no-prototype-methods/no-prototype-methods
return value.toString();
}
return String(value);
}
module.exports = valueToString;
+20
View File
@@ -0,0 +1,20 @@
"use strict";
var assert = require("@sinonjs/referee-sinon").assert;
var valueToString = require("./value-to-string");
describe("util/core/valueToString", function () {
it("returns string representation of an object", function () {
var obj = {};
assert.equals(valueToString(obj), obj.toString());
});
it("returns 'null' for literal null'", function () {
assert.equals(valueToString(null), "null");
});
it("returns 'undefined' for literal undefined", function () {
assert.equals(valueToString(undefined), "undefined");
});
});
+57
View File
@@ -0,0 +1,57 @@
{
"name": "@sinonjs/commons",
"version": "3.0.1",
"description": "Simple functions shared among the sinon end user libraries",
"main": "lib/index.js",
"types": "./types/index.d.ts",
"scripts": {
"build": "rm -rf types && tsc",
"lint": "eslint .",
"precommit": "lint-staged",
"test": "mocha --recursive -R dot \"lib/**/*.test.js\"",
"test-check-coverage": "npm run test-coverage && nyc check-coverage --branches 100 --functions 100 --lines 100",
"test-coverage": "nyc --reporter text --reporter html --reporter lcovonly npm run test",
"prepublishOnly": "npm run build",
"prettier:check": "prettier --check '**/*.{js,css,md}'",
"prettier:write": "prettier --write '**/*.{js,css,md}'",
"preversion": "npm run test-check-coverage",
"version": "changes --commits --footer",
"postversion": "git push --follow-tags && npm publish",
"prepare": "husky install"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sinonjs/commons.git"
},
"files": [
"lib",
"types"
],
"author": "",
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/sinonjs/commons/issues"
},
"homepage": "https://github.com/sinonjs/commons#readme",
"lint-staged": {
"*.{js,css,md}": "prettier --check",
"*.js": "eslint"
},
"devDependencies": {
"@sinonjs/eslint-config": "^4.0.6",
"@sinonjs/eslint-plugin-no-prototype-methods": "^0.1.0",
"@sinonjs/referee-sinon": "^10.1.0",
"@studio/changes": "^2.2.0",
"husky": "^6.0.0",
"jsverify": "0.8.4",
"knuth-shuffle": "^1.0.8",
"lint-staged": "^13.0.3",
"mocha": "^10.1.0",
"nyc": "^15.1.0",
"prettier": "^2.7.1",
"typescript": "^4.8.4"
},
"dependencies": {
"type-detect": "4.0.8"
}
}
+34
View File
@@ -0,0 +1,34 @@
export = calledInOrder;
/**
* A Sinon proxy object (fake, spy, stub)
* @typedef {object} SinonProxy
* @property {Function} calledBefore - A method that determines if this proxy was called before another one
* @property {string} id - Some id
* @property {number} callCount - Number of times this proxy has been called
*/
/**
* Returns true when the spies have been called in the order they were supplied in
* @param {SinonProxy[] | SinonProxy} spies An array of proxies, or several proxies as arguments
* @returns {boolean} true when spies are called in order, false otherwise
*/
declare function calledInOrder(spies: SinonProxy[] | SinonProxy, ...args: any[]): boolean;
declare namespace calledInOrder {
export { SinonProxy };
}
/**
* A Sinon proxy object (fake, spy, stub)
*/
type SinonProxy = {
/**
* - A method that determines if this proxy was called before another one
*/
calledBefore: Function;
/**
* - Some id
*/
id: string;
/**
* - Number of times this proxy has been called
*/
callCount: number;
};
+7
View File
@@ -0,0 +1,7 @@
export = className;
/**
* Returns a display name for a value from a constructor
* @param {object} value A value to examine
* @returns {(string|null)} A string or null
*/
declare function className(value: object): (string | null);
+3
View File
@@ -0,0 +1,3 @@
export function wrap(func: Function, msg: string): Function;
export function defaultMsg(packageName: string, funcName: string): string;
export function printWarning(msg: string): undefined;
+2
View File
@@ -0,0 +1,2 @@
declare function _exports(obj: object, fn: Function): boolean;
export = _exports;
+2
View File
@@ -0,0 +1,2 @@
declare function _exports(func: Function): string;
export = _exports;
+6
View File
@@ -0,0 +1,6 @@
export = globalObject;
/**
* A reference to the global object
* @type {object} globalObject
*/
declare var globalObject: object;
+17
View File
@@ -0,0 +1,17 @@
export const global: any;
export const calledInOrder: typeof import("./called-in-order");
export const className: typeof import("./class-name");
export const deprecated: typeof import("./deprecated");
export const every: (obj: any, fn: Function) => boolean;
export const functionName: (func: Function) => string;
export const orderByFirstCall: typeof import("./order-by-first-call");
export const prototypes: {
array: any;
function: any;
map: any;
object: any;
set: any;
string: any;
};
export const typeOf: (value: any) => string;
export const valueToString: typeof import("./value-to-string");
+24
View File
@@ -0,0 +1,24 @@
export = orderByFirstCall;
/**
* A Sinon proxy object (fake, spy, stub)
* @typedef {object} SinonProxy
* @property {Function} getCall - A method that can return the first call
*/
/**
* Sorts an array of SinonProxy instances (fake, spy, stub) by their first call
* @param {SinonProxy[] | SinonProxy} spies
* @returns {SinonProxy[]}
*/
declare function orderByFirstCall(spies: SinonProxy[] | SinonProxy): SinonProxy[];
declare namespace orderByFirstCall {
export { SinonProxy };
}
/**
* A Sinon proxy object (fake, spy, stub)
*/
type SinonProxy = {
/**
* - A method that can return the first call
*/
getCall: Function;
};
+2
View File
@@ -0,0 +1,2 @@
declare const _exports: any;
export = _exports;
@@ -0,0 +1,2 @@
declare function _exports(prototype: any): any;
export = _exports;
+2
View File
@@ -0,0 +1,2 @@
declare const _exports: any;
export = _exports;
+7
View File
@@ -0,0 +1,7 @@
export declare const array: any;
declare const _function: any;
export { _function as function };
export declare const map: any;
export declare const object: any;
export declare const set: any;
export declare const string: any;
+2
View File
@@ -0,0 +1,2 @@
declare const _exports: any;
export = _exports;
+2
View File
@@ -0,0 +1,2 @@
declare const _exports: any;
export = _exports;
+2
View File
@@ -0,0 +1,2 @@
declare const _exports: any;
export = _exports;
+2
View File
@@ -0,0 +1,2 @@
declare const _exports: any;
export = _exports;
@@ -0,0 +1,10 @@
export = throwsOnProto;
/**
* Is true when the environment causes an error to be thrown for accessing the
* __proto__ property.
* This is necessary in order to support `node --disable-proto=throw`.
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto
* @type {boolean}
*/
declare let throwsOnProto: boolean;
+2
View File
@@ -0,0 +1,2 @@
declare function _exports(value: any): string;
export = _exports;
+7
View File
@@ -0,0 +1,7 @@
export = valueToString;
/**
* Returns a string representation of the value
* @param {*} value
* @returns {string}
*/
declare function valueToString(value: any): string;
+11
View File
@@ -0,0 +1,11 @@
Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+430
View File
@@ -0,0 +1,430 @@
# `@sinonjs/fake-timers`
[![codecov](https://codecov.io/gh/sinonjs/fake-timers/branch/main/graph/badge.svg)](https://codecov.io/gh/sinonjs/fake-timers)
<a href="CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Contributor Covenant" /></a>
JavaScript implementation of the timer
APIs; `setTimeout`, `clearTimeout`, `setImmediate`, `clearImmediate`, `setInterval`, `clearInterval`, `requestAnimationFrame`, `cancelAnimationFrame`, `requestIdleCallback`,
and `cancelIdleCallback`, along with a clock instance that controls the flow of time. FakeTimers also provides a `Date`
implementation that gets its time from the clock.
In addition in browser environment `@sinonjs/fake-timers` provides a `performance` implementation that gets its time
from the clock. In Node environments FakeTimers provides a `nextTick` implementation that is synchronized with the
clock - and a `process.hrtime` shim that works with the clock.
`@sinonjs/fake-timers` can be used to simulate passing time in automated tests and other
situations where you want the scheduling semantics, but don't want to actually
wait.
`@sinonjs/fake-timers` is an integral part of [Sinon.JS](https://github.com/sinonjs/sinon.js) and targets
the [same runtimes](https://sinonjs.org/releases/latest#compatibility-and-supported-runtimes).
## Autocomplete, IntelliSense and TypeScript definitions
`@sinonjs/fake-timers` ships with built-in type definitions generated from JSDoc. This provides autocomplete and type suggestions in supporting IDEs and TypeScript projects without requiring external `@types` packages.
## Installation
`@sinonjs/fake-timers` can be used in both Node and browser environments. Installation is as easy as
```sh
npm install @sinonjs/fake-timers
```
If you want to use `@sinonjs/fake-timers` in a browser you can either build your own bundle or
use [Skypack](https://www.skypack.dev).
## Usage
To use `@sinonjs/fake-timers`, create a new clock, schedule events on it using the timer
functions and pass time using the `tick` method.
```js
// In the browser distribution, a global `FakeTimers` is already available
var FakeTimers = require("@sinonjs/fake-timers");
var clock = FakeTimers.createClock();
clock.setTimeout(function () {
console.log(
"The poblano is a mild chili pepper originating in the state of Puebla, Mexico.",
);
}, 15);
// ...
clock.tick(15);
```
Upon executing the last line, an interesting fact about the
[Poblano](https://en.wikipedia.org/wiki/Poblano) will be printed synchronously to
the screen. If you want to simulate asynchronous behavior, please see the `async` function variants (
eg `clock.tick(time)` vs `await clock.tickAsync(time)`).
The `next`, `runAll`, `runToFrame`, and `runToLast` methods are available to advance the clock. See the
API Reference for more details.
### Faking the native timers
When using `@sinonjs/fake-timers` to test timers, you will most likely want to replace the native
timers such that calling `setTimeout` actually schedules a callback with your
clock instance, not the browser's internals.
Calling `install` with no arguments achieves this. You can call `uninstall`
later to restore things as they were again.
Note that in NodeJS the [timers](https://nodejs.org/api/timers.html)
and [timers/promises](https://nodejs.org/api/timers.html#timers-promises-api) modules will also receive fake timers when
using global scope.
```js
// In the browser distribution, a global `FakeTimers` is already available
var FakeTimers = require("@sinonjs/fake-timers");
var clock = FakeTimers.install();
// Equivalent to
// var clock = FakeTimers.install(typeof global !== "undefined" ? global : window);
setTimeout(fn, 15); // Schedules with clock.setTimeout
clock.uninstall();
// setTimeout is restored to the native implementation
```
To hijack timers in another context pass it to the `install` method.
```js
var FakeTimers = require("@sinonjs/fake-timers");
var context = {
setTimeout: setTimeout, // By default context.setTimeout uses the global setTimeout
};
var clock = FakeTimers.withGlobal(context).install();
context.setTimeout(fn, 15); // Schedules with clock.setTimeout
clock.uninstall();
// context.setTimeout is restored to the original implementation
```
Usually you want to install the timers onto the global object, so call `install`
without arguments.
#### Automatically incrementing mocked time
FakeTimers supports the possibility to attach the faked timers to any change
in the real system time. This means that there is no need to `tick()` the
clock in a situation where you won't know **when** to call `tick()`.
Please note that this is achieved using the original setImmediate() API at a certain
configurable interval `config.advanceTimeDelta` (default: 20ms). Meaning time would
be incremented every 20ms, not in real time.
An example would be:
```js
var FakeTimers = require("@sinonjs/fake-timers");
var clock = FakeTimers.install({
shouldAdvanceTime: true,
advanceTimeDelta: 40,
});
setTimeout(() => {
console.log("this just timed out"); //executed after 40ms
}, 30);
setImmediate(() => {
console.log("not so immediate"); //executed after 40ms
});
setTimeout(() => {
console.log("this timed out after"); //executed after 80ms
clock.uninstall();
}, 50);
```
In addition to the above, mocked time can be configured to advance more quickly
using `clock.setTickMode({ mode: "nextAsync" });`. With this mode, the clock
advances to the first scheduled timer and fires it, in a loop. Between each timer,
it will also break the event loop, allowing any scheduled promise
callbacks to execute _before_ running the next one.
## API Reference
### `var clock = FakeTimers.createClock([now[, loopLimit]])`
Creates a clock. The default
[epoch](https://en.wikipedia.org/wiki/Epoch_%28reference_date%29) is `0`.
The `now` argument may be a number (in milliseconds), a `Date` object, a `Temporal.Instant`, or a `Temporal.ZonedDateTime`.
The `loopLimit` argument sets the maximum number of timers that will be run when calling `runAll()` before assuming that
we have an infinite loop and throwing an error. The default is `1000`.
### `var clock = FakeTimers.install([config])`
Installs FakeTimers using the specified config (otherwise with epoch `0` on the global scope).
Note that in NodeJS the [timers](https://nodejs.org/api/timers.html)
and [timers/promises](https://nodejs.org/api/timers.html#timers-promises-api) modules will also receive fake timers when
using global scope.
The following configuration options are available
| Parameter | Type | Default | Description |
| -------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `config.now` | Number/Date/Temporal.Instant/Temporal.ZonedDateTime | 0 | installs FakeTimers with the specified unix epoch |
| `config.toFake` | String[] | ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "Date"] plus, when natively available: "setImmediate", "clearImmediate", "hrtime", "nextTick", "performance", "requestAnimationFrame", "queueMicrotask", "cancelAnimationFrame", "requestIdleCallback", "cancelIdleCallback", "Intl", "Temporal" | an array with explicit function names (or objects, in the case of "performance") to hijack. When not set, FakeTimers will automatically fake all methods e.g., `FakeTimers.install({ toFake: ["setTimeout","nextTick"]})` will fake only `setTimeout` and `nextTick`. Cannot be combined with `config.toNotFake`. `"Temporal"` fakes all `Temporal.Now.*` methods (requires Node 26+ or an environment with native Temporal support). |
| `config.toNotFake` | String[] | [] | an array with explicit function names that should remain native. When set, FakeTimers will fake every other supported method e.g., `FakeTimers.install({ toNotFake: ["Date"] })` fakes all supported methods except `Date`. Cannot be combined with `config.toFake`. |
| `config.loopLimit` | Number | 1000 | the maximum number of timers that will be run when calling runAll() |
| `config.shouldAdvanceTime` | Boolean | false | tells FakeTimers to increment mocked time automatically based on the real system time shift (e.g. the mocked time will be incremented by 20ms for every 20ms change in the real system time) |
| `config.advanceTimeDelta` | Number | 20 | relevant only when using with `shouldAdvanceTime: true`. increment mocked time by `advanceTimeDelta` ms every `advanceTimeDelta` ms change in the real system time. |
| `config.shouldClearNativeTimers` | Boolean | false | tells FakeTimers to clear 'native' (i.e. not fake) timers by delegating to their respective handlers. These are not cleared by default, leading to potentially unexpected behavior if timers existed prior to installing FakeTimers. |
| `config.ignoreMissingTimers` | Boolean | false | tells FakeTimers to ignore missing timers that might not exist in the given environment |
### `clock.setTickMode(mode)`
Allows configuring how the clock advances time, automatically or manually.
There are 3 different types of modes for advancing timers:
- `{mode: 'manual'}`: Timers do not advance without explicit, manual calls to the tick
APIs (`clock.nextAsync`, `clock.runAllAsync`, etc). This mode is equivalent to `false`.
- `{mode: 'nextAsync'}`: The clock will continuously break the event loop, then run the next timer until the mode changes.
As a result, tests can be written in a way that is independent from whether fake timers are installed.
Tests can always be written to wait for timers to resolve, even when using fake timers.
- `{mode: 'interval', delta?: <number>}`: This is the same as specifying `shouldAdvanceTime: true` with an `advanceTimeDelta`. If the delta is
not specified, 20 will be used by default.
The 'nextAsync' mode differs from `interval` in two key ways:
1. The microtask queue is allowed to empty between each timer execution,
as would be the case without fake timers installed.
1. It advances as quickly and as far as necessary. If the next timer in
the queue is at 1000ms, it will advance 1000ms immediately whereas interval,
without manually advancing time in the test, would take `1000 / advanceTimeDelta`
real time to reach and execute the timer.
### `var id = clock.setTimeout(callback, timeout)`
Schedules the callback to be fired once `timeout` milliseconds have ticked by.
In Node.js `setTimeout` returns a timer object. FakeTimers will do the same, however
its `ref()` and `unref()` methods have no effect.
In browsers a timer ID is returned.
### `clock.clearTimeout(id)`
Clears the timer given the ID or timer object, as long as it was created using
`setTimeout`.
### `var id = clock.setInterval(callback, timeout)`
Schedules the callback to be fired every time `timeout` milliseconds have ticked
by.
In Node.js `setInterval` returns a timer object. FakeTimers will do the same, however
its `ref()` and `unref()` methods have no effect.
In browsers a timer ID is returned.
### `clock.clearInterval(id)`
Clears the timer given the ID or timer object, as long as it was created using
`setInterval`.
### `var id = clock.setImmediate(callback)`
Schedules the callback to be fired once `0` milliseconds have ticked by. Note
that you'll still have to call `clock.tick()` for the callback to fire. If
called during a tick the callback won't fire until `1` millisecond has ticked
by.
In Node.js `setImmediate` returns a timer object. FakeTimers will do the same,
however its `ref()` and `unref()` methods have no effect.
In browsers a timer ID is returned.
### `clock.clearImmediate(id)`
Clears the timer given the ID or timer object, as long as it was created using
`setImmediate`.
### `clock.requestAnimationFrame(callback)`
Schedules the callback to be fired on the next animation frame, which runs every
16 ticks. Returns an `id` which can be used to cancel the callback. This is
available in both browser & node environments.
### `clock.cancelAnimationFrame(id)`
Cancels the callback scheduled by the provided id.
### `clock.requestIdleCallback(callback[, timeout])`
Queued the callback to be fired during idle periods to perform background and low priority work on the main event loop.
Callbacks which have a timeout option will be fired no later than time in milliseconds. Returns an `id` which can be
used to cancel the callback.
### `clock.cancelIdleCallback(id)`
Cancels the callback scheduled by the provided id.
### `clock.countTimers()`
Returns the number of waiting timers. This can be used to assert that a test
finishes without leaking any timers.
### `clock.hrtime(prevTime?)`
Only available in Node.js, mimicks process.hrtime().
### `clock.nextTick(callback)`
Only available in Node.js, mimics `process.nextTick` to enable completely synchronous testing flows.
### `clock.performance.now()`
Only available in browser environments, mimicks performance.now().
### `clock.tick(time)` / `await clock.tickAsync(time)`
Advance the clock, firing callbacks if necessary. `time` may be the number of
milliseconds to advance the clock by, a human-readable string, or a
`Temporal.Duration` (requires Node 26+ or an environment with native Temporal
support). Valid string formats are `"08"` for eight seconds, `"01:00"` for one
minute and `"02:34:10"` for two hours, 34 minutes and ten seconds.
The `tickAsync()` will also break the event loop, allowing any scheduled promise
callbacks to execute _before_ running the timers.
### `clock.next()` / `await clock.nextAsync()`
Advances the clock to the the moment of the first scheduled timer, firing it.
The `nextAsync()` will also break the event loop, allowing any scheduled promise
callbacks to execute _before_ running the timers.
### `clock.jump(time)`
Advance the clock by jumping forward in time, firing callbacks at most once.
`time` takes the same formats as [`clock.tick`](#clockticktime--await-clocktickasynctime).
This can be used to simulate the JS engine (such as a browser) being put to sleep and resumed later, skipping
intermediary timers.
### `clock.reset()`
Removes all timers and ticks without firing them, and sets `now` to `config.now`
that was provided to `FakeTimers.install` or to `0` if `config.now` was not provided.
Useful to reset the state of the clock without having to `uninstall` and `install` it.
### `clock.runAll()` / `await clock.runAllAsync()`
This runs all pending timers until there are none remaining. If new timers are added while it is executing they will be
run as well.
This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, or
the delays in those timers.
It runs a maximum of `loopLimit` times after which it assumes there is an infinite loop of timers and throws an error.
The `runAllAsync()` will also break the event loop, allowing any scheduled promise
callbacks to execute _before_ running the timers.
### `clock.runMicrotasks()`
This runs all pending microtasks scheduled with `nextTick` but none of the timers and is mostly useful for libraries
using FakeTimers underneath and for running `nextTick` items without any timers.
### `clock.runToFrame()`
Advances the clock to the next frame, firing all scheduled animation frame callbacks,
if any, for that frame as well as any other timers scheduled along the way.
### `clock.runToLast()` / `await clock.runToLastAsync()`
This takes note of the last scheduled timer when it is run, and advances the
clock to that time firing callbacks as necessary.
If new timers are added while it is executing they will be run only if they
would occur before this time.
This is useful when you want to run a test to completion, but the test recursively
sets timers that would cause `runAll` to trigger an infinite loop warning.
The `runToLastAsync()` will also break the event loop, allowing any scheduled promise
callbacks to execute _before_ running the timers.
### `clock.setSystemTime([now])`
This simulates a user changing the system clock while your program is running.
It affects the current time but it does not in itself cause e.g. timers to fire;
they will fire exactly as they would have done without the call to
setSystemTime(). The `now` argument may be a number (in milliseconds), a `Date`
object, a `Temporal.Instant`, or a `Temporal.ZonedDateTime`.
### `clock.uninstall()`
Restores the original methods of the native timers or the methods on the object
that was passed to `FakeTimers.withGlobal`
### `Date`
Implements the `Date` object but using the clock to provide the correct time.
### `Performance`
Implements the `now` method of the [`Performance`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now)
object but using the clock to provide the correct time. Only available in environments that support the Performance
object (browsers mostly).
### `FakeTimers.withGlobal`
In order to support creating clocks based on separate or sandboxed environments (such as JSDOM), FakeTimers exports a
factory method which takes single argument `global`, which it inspects to figure out what to mock and what features to
support. When invoking this function with a global, you will get back an object with `timers`, `createClock`
and `install` - same as the regular FakeTimers exports only based on the passed in global instead of the global
environment.
## Promises and fake time
If you use a Promise library like Bluebird, note that you should either call `clock.runMicrotasks()` or make sure to
_not_ mock `nextTick`.
## Running tests
FakeTimers has a comprehensive test suite. If you're thinking of contributing bug
fixes or suggesting new features, you need to make sure you have not broken any
tests. You are also expected to add tests for any new behavior.
### On node:
```sh
npm test
```
Or, if you prefer more verbose output:
```
$(npm bin)/mocha ./test/fake-timers-test.js
```
### In the browser
[Mochify](https://github.com/mochify-js) is used to run the tests in headless
Chrome.
```sh
npm test-headless
```
## License
BSD 3-clause "New" or "Revised" License (see LICENSE file)
## Contributing
`@sinonjs/fake-timers` uses JSDoc in `src/fake-timers-src.js` as the source of truth for its public types.
TypeScript declarations are automatically generated from this file. When contributing changes:
- Update JSDoc annotations in `src/fake-timers-src.js` if the public API changes.
- Run `npm run types:build` to regenerate the declarations in `types/`.
- Ensure `npm run types:smoke` still passes to validate the generated types.
- `tsgo` is used as a parallel validation lane to ensure compatibility with future TypeScript versions.
+94
View File
@@ -0,0 +1,94 @@
{
"name": "@sinonjs/fake-timers",
"description": "Fake JavaScript timers",
"version": "15.4.0",
"homepage": "https://github.com/sinonjs/fake-timers",
"author": "Christian Johansen",
"repository": {
"type": "git",
"url": "git+https://github.com/sinonjs/fake-timers.git"
},
"bugs": {
"mail": "christian@cjohansen.no",
"url": "https://github.com/sinonjs/fake-timers/issues"
},
"license": "BSD-3-Clause",
"scripts": {
"lint": "eslint .",
"test-node": "mocha --timeout 200 test/ integration-test/ -R dot --check-leaks",
"test-headless": "mochify --driver puppeteer",
"test-check-coverage": "npm run test-coverage && nyc check-coverage",
"test-cloud": "npm run test-edge && npm run test-firefox && npm run test-safari",
"test-edge": "BROWSER_NAME=MicrosoftEdge mochify --config mochify.webdriver.js",
"test-firefox": "BROWSER_NAME=firefox mochify --config mochify.webdriver.js",
"test-safari": "BROWSER_NAME=safari mochify --config mochify.webdriver.js",
"test-coverage": "nyc -x mochify.webdriver.js -x coverage --all --reporter text --reporter html --reporter lcovonly npm run test-node",
"test": "npm run test-node && npm run test-headless",
"posttest": "npm run types:build",
"prettier:check": "prettier --check '**/*.{js,css,md}'",
"prettier:write": "prettier --write '**/*.{js,css,md}'",
"preversion": "./scripts/preversion.sh",
"version": "./scripts/version.sh",
"postversion": "./scripts/postversion.sh",
"prepare": "husky",
"types:build": "tsgo -p tsconfig.types.json",
"types:check": "tsgo -p tsconfig.types.json --noEmit",
"types:smoke": "tsgo -p test/typescript-consumer/tsconfig.json --noEmit && tsgo -p test/typescript-no-dom-consumer/tsconfig.json --noEmit"
},
"types": "./types/fake-timers-src.d.ts",
"lint-staged": {
"*.{js,css,md}": "prettier --check",
"*.js": "eslint"
},
"mochify": {
"reporter": "dot",
"timeout": 10000,
"bundle": "esbuild --bundle --sourcemap=inline --define:process.env.NODE_DEBUG=\"\"",
"bundle_stdin": "require",
"spec": "test/**/*-test.js"
},
"files": [
"src/",
"types/"
],
"devDependencies": {
"@mochify/cli": "^1.0.0",
"@mochify/driver-puppeteer": "^1.0.1",
"@mochify/driver-webdriver": "^1.0.0",
"@sinonjs/eslint-config": "^7.0.3",
"@sinonjs/referee-sinon": "12.0.1",
"@types/node": "^25.6.0",
"@typescript/native-preview": "^7.0.0-dev.20260505.1",
"esbuild": "^0.28.0",
"eslint": "^10.3.0",
"husky": "^9.1.7",
"jsdom": "29.1.1",
"lint-staged": "16.4.0",
"mocha": "11.7.5",
"nyc": "18.0.0",
"prettier": "3.8.3"
},
"main": "./src/fake-timers-src.js",
"dependencies": {
"@sinonjs/commons": "^3.0.1"
},
"nyc": {
"branches": 85,
"lines": 92,
"functions": 92,
"statements": 92,
"exclude": [
"**/*-test.js",
"coverage/**",
"types/**",
"fake-timers.js"
]
},
"browser": {
"vm": false,
"node:timers": false,
"node:timers/promises": false,
"timers": false,
"timers/promises": false
}
}
+3114
View File
@@ -0,0 +1,3114 @@
"use strict";
const globalObject = require("@sinonjs/commons").global;
let timersModule, timersPromisesModule;
if (typeof require === "function" && typeof module === "object") {
try {
timersModule = require("timers");
} catch {
// ignored
}
try {
timersPromisesModule = require("timers/promises");
} catch {
// ignored
}
}
/**
* @typedef {"nextAsync" | "manual" | "interval"} TickMode
*/
/**
* @typedef {object} NextAsyncTickMode
* @property {"nextAsync"} mode - runs timers one macrotask at a time
*/
/**
* @typedef {object} ManualTickMode
* @property {"manual"} mode - advances only when the caller explicitly ticks
*/
/**
* @typedef {object} IntervalTickMode
* @property {"interval"} mode - advances automatically on a native interval
* @property {number} [delta] - interval duration in milliseconds
*/
/**
* @typedef {IntervalTickMode | NextAsyncTickMode | ManualTickMode} TimerTickMode
*/
/**
* @callback FakeTimersFunction
* @param {...unknown[]} args
* @returns {unknown}
*/
/**
* @callback VoidVarArgsFunc
* @param {...unknown[]} args - optional arguments to call the callback with
* @returns {void}
*/
/**
* @callback NextTick
* @param {VoidVarArgsFunc} callback - the callback to run
* @param {...unknown[]} args - optional arguments to call the callback with
* @returns {void}
*/
/**
* @callback SetImmediate
* @param {VoidVarArgsFunc} callback - the callback to run
* @param {...unknown[]} args - optional arguments to call the callback with
* @returns {NodeImmediate}
*/
/**
* @callback SetTimeout
* @param {VoidVarArgsFunc} callback - the callback to run
* @param {number} [delay] - optional delay in milliseconds
* @param {...unknown[]} args - optional arguments to call the callback with
* @returns {TimerId} - the timeout identifier
*/
/**
* @callback ClearTimeout
* @param {TimerId} [id] - the timeout identifier to clear
* @returns {void}
*/
/**
* @callback SetInterval
* @param {VoidVarArgsFunc} callback - the callback to run
* @param {number} [delay] - optional delay in milliseconds
* @param {...unknown[]} args - optional arguments to call the callback with
* @returns {TimerId} - the interval identifier
*/
/**
* @callback ClearInterval
* @param {TimerId} [id] - the interval identifier to clear
* @returns {void}
*/
/**
* @callback QueueMicrotask
* @param {VoidVarArgsFunc} callback - the callback to run
* @returns {void}
*/
/**
* @callback TimeRemaining
* @returns {number}
*/
/**
* @typedef {object} IdleDeadline
* @property {boolean} didTimeout - whether or not the callback was called before reaching the optional timeout
* @property {TimeRemaining} timeRemaining - a floating-point value providing an estimate of the number of milliseconds remaining in the current idle period
*/
/**
* @callback RequestIdleCallbackCallback
* @param {IdleDeadline} deadline
*/
/**
* Queues a function to be called during a browser's idle periods
* @callback RequestIdleCallback
* @param {RequestIdleCallbackCallback} callback
* @param {{timeout?: number}} [options] - an options object
* @returns {number} the id
*/
/**
* @callback AnimationFrameCallback
* @param {number} timestamp
*/
/**
* @callback RequestAnimationFrame
* @param {AnimationFrameCallback} callback
* @returns {TimerId} - the request id
*/
/**
* @callback CancelAnimationFrame
* @param {TimerId} id - cancels a frame callback
* @returns {void}
*/
/**
* @callback CancelIdleCallback
* @param {TimerId} id - cancels a scheduled idle callback
* @returns {void}
*/
/**
* @callback ClearImmediate
* @param {NodeImmediate} id - faked `clearImmediate`
* @returns {void}
*/
/**
* @callback CountTimers
* @returns {number}
*/
/**
* @callback RunMicrotasks
* @returns {void}
*/
/**
* @typedef {object} TemporalDuration
* @property {number} years - years component
* @property {number} months - months component
* @property {number} weeks - weeks component
* @property {number} days - days component
* @property {number} hours - hours component
* @property {number} minutes - minutes component
* @property {number} seconds - seconds component
* @property {number} milliseconds - milliseconds component
* @property {number} microseconds - microseconds component
* @property {number} nanoseconds - nanoseconds component
* @property {(options: {unit: string, relativeTo?: unknown}) => number} total - converts to a single unit
*/
/**
* @typedef {object} TemporalTimelike
* @property {number} epochMilliseconds - milliseconds since the Unix epoch (present on Temporal.Instant and Temporal.ZonedDateTime)
*/
/**
* @callback Tick
* @param {number|string|TemporalDuration} tickValue milliseconds, a string parseable by parseTime, or a Temporal.Duration
* @returns {number} will return the new `now` value
*/
/**
* @callback TickAsync
* @param {number|string|TemporalDuration} tickValue milliseconds, a string parseable by parseTime, or a Temporal.Duration
* @returns {Promise<number>}
*/
/**
* @callback Next
* @returns {number}
*/
/**
* @callback NextAsync
* @returns {Promise<number>}
*/
/**
* @callback RunAll
* @returns {number}
*/
/**
* @callback RunToFrame
* @returns {number}
*/
/**
* @callback RunAllAsync
* @returns {Promise<number>}
*/
/**
* @callback RunToLast
* @returns {number}
*/
/**
* @callback RunToLastAsync
* @returns {Promise<number>}
*/
/**
* @callback Reset
* @returns {void}
*/
/**
* @callback SetSystemTime
* @param {number|Date|TemporalTimelike} [now] initial mocked time, as milliseconds since epoch, a Date, a Temporal.Instant, or a Temporal.ZonedDateTime
* @returns {void}
*/
/**
* @callback Jump
* @param {number|string|TemporalDuration} tickValue milliseconds, a human-readable value like "01:11:15", or a Temporal.Duration
* @returns {number}
*/
/**
* @callback Uninstall
* @returns {void}
*/
/**
* @callback SetTickMode
* @param {SetTickModeConfig} tickModeConfig - The new configuration for how the clock should tick.
* @returns {void}
*/
/**
* @callback Hrtime
* @param {Array<number>} [prev]
* @returns {Array<number>}
*/
/**
* @callback WithGlobal
* @param {object} _global Namespace to mock (e.g. `window`)
* @returns {FakeTimers}
*/
/**
* @typedef {"setTimeout" | "clearTimeout" | "setImmediate" | "clearImmediate" | "setInterval" | "clearInterval" | "Date" | "nextTick" | "hrtime" | "requestAnimationFrame" | "cancelAnimationFrame" | "requestIdleCallback" | "cancelIdleCallback" | "performance" | "queueMicrotask" | "Intl" | "Temporal"} FakeMethod
*/
/**
* @typedef {number | NodeImmediate | Timer} TimerId
*/
/* eslint-disable jsdoc/reject-any-type */
/**
* @typedef {Record<string, any> & {
* setTimeout?: SetTimeout,
* clearTimeout?: ClearTimeout,
* setInterval?: SetInterval,
* clearInterval?: ClearInterval,
* setImmediate?: SetImmediate,
* clearImmediate?: ClearImmediate,
* queueMicrotask?: QueueMicrotask,
* requestAnimationFrame?: RequestAnimationFrame,
* cancelAnimationFrame?: CancelAnimationFrame,
* requestIdleCallback?: RequestIdleCallback,
* cancelIdleCallback?: CancelIdleCallback,
* process?: any,
* performance?: any,
* Performance?: any,
* Intl?: any,
* Temporal?: any,
* Promise?: typeof Promise,
* Date: typeof Date & { isFake?: boolean, toSource?: () => string, clock?: any }
* }} GlobalObject
*/
/**
* @typedef {object} TimerHeap
* @property {Timer[]} timers - the heap-ordered timers
* @property {() => Timer | undefined} peek - returns the next timer without removing it
* @property {(timer: Timer) => void} push - adds a timer to the heap
* @property {() => Timer | undefined} pop - removes and returns the next timer
* @property {(timer: Timer) => void} remove - removes a specific timer
*/
/**
* @typedef {object} ClockTickMode
* @property {TickMode} mode - active tick mode
* @property {number} counter - increments whenever the mode changes
* @property {number} [delta] - interval length in milliseconds
*/
/**
* @typedef {object} SetTickModeConfig
* @property {TickMode} mode - desired tick mode
* @property {number} [delta] - interval length in milliseconds
*/
/**
* @typedef {Record<string, any> & { clock: Clock }} IntlWithClock
*/
/**
* @typedef {Record<string, any> & { now: () => number }} PerformanceLike
*/
/**
* @typedef {object} Timers
* @property {SetTimeout} setTimeout - native `setTimeout`
* @property {ClearTimeout} clearTimeout - native `clearTimeout`
* @property {SetInterval} setInterval - native `setInterval`
* @property {ClearInterval} clearInterval - native `clearInterval`
* @property {typeof Date} Date - native `Date`
* @property {typeof Intl} [Intl] - native `Intl`
* @property {any} [Temporal] - native `Temporal`
* @property {SetImmediate} [setImmediate] - native `setImmediate`, if available
* @property {ClearImmediate} [clearImmediate] - native `clearImmediate`, if available
* @property {Hrtime} [hrtime] - native `process.hrtime`, if available
* @property {NextTick} [nextTick] - native `process.nextTick`, if available
* @property {PerformanceLike} [performance] - native `performance`, if available
* @property {RequestAnimationFrame} [requestAnimationFrame] - native `requestAnimationFrame`, if available
* @property {QueueMicrotask} [queueMicrotask] - whether `queueMicrotask` exists
* @property {CancelAnimationFrame} [cancelAnimationFrame] - native `cancelAnimationFrame`, if available
* @property {RequestIdleCallback} [requestIdleCallback] - native `requestIdleCallback`, if available
* @property {CancelIdleCallback} [cancelIdleCallback] - native `cancelIdleCallback`, if available
*/
/**
* @typedef {object} ClockState
* @property {number} tickFrom - lower bound of the current tick range
* @property {number} tickTo - upper bound of the current tick range
* @property {number} [previous] - previous timer time used during ticking
* @property {number | null} [oldNow] - previous value of `now`
* @property {Timer} [timer] - timer currently being processed
* @property {unknown} [firstException] - first exception raised while processing timers
* @property {number} [nanosTotal] - accumulated nanoseconds from fractional ticks
* @property {number} [msFloat] - accumulated fractional milliseconds
* @property {number} [ms] - accumulated whole milliseconds
*/
/**
* @typedef {object} TimerInitialProps
* @property {VoidVarArgsFunc} func - callback or string to execute
* @property {unknown[]} [args] - arguments passed to the callback
* @property {'Timeout' | 'Interval' | 'Immediate' | 'AnimationFrame' | 'IdleCallback'} [type] - timer kind
* @property {number} [delay] - requested delay in milliseconds
* @property {number} [callAt] - scheduled execution time
* @property {number} [createdAt] - time at which the timer was created
* @property {boolean} [immediate] - whether this timer should run before non-immediate timers at the same time
* @property {number} [id] - unique timer identifier
* @property {Error} [error] - captured stack for loop diagnostics
* @property {number} [interval] - interval for repeated timers
* @property {boolean} [animation] - whether this is an animation frame timer
* @property {boolean} [requestIdleCallback] - whether this is an idle callback timer
* @property {number} [order] - execution order for timers at the same time
* @property {number} [heapIndex] - index in the timer heap
*/
/**
* @callback CreateClockCallback
* @param {number|Date|TemporalTimelike} [start] initial mocked time, as milliseconds since epoch, a Date, a Temporal.Instant, or a Temporal.ZonedDateTime
* @param {number} [loopLimit] maximum number of timers run before aborting with an infinite-loop error
* @returns {Clock}
*/
/**
* @callback InstallCallback
* @param {Config} [config] Optional config
* @returns {Clock}
*/
/**
* @typedef {object} FakeTimers
* @property {Timers} timers - the native timer APIs saved for later restoration
* @property {CreateClockCallback} createClock - creates a new fake clock
* @property {InstallCallback} install - installs the fake timers onto the default global object
* @property {WithGlobal} withGlobal - creates a fake-timers instance for a provided global object
*/
/**
* @typedef {object} Clock
* @property {number} now - current mocked time in milliseconds
* @property {typeof Date & {clock?: Clock, isFake?: boolean, toSource?: () => string}} Date - fake Date constructor bound to this clock
* @property {number} loopLimit - maximum number of timers before assuming an infinite loop
* @property {RequestIdleCallback} requestIdleCallback - schedules an idle callback
* @property {CancelIdleCallback} cancelIdleCallback - cancels a scheduled idle callback
* @property {SetTimeout} setTimeout - faked `setTimeout`
* @property {ClearTimeout} clearTimeout - faked `clearTimeout`
* @property {NextTick} nextTick - faked `process.nextTick`
* @property {QueueMicrotask} queueMicrotask - faked `queueMicrotask`
* @property {SetInterval} setInterval - faked `setInterval`
* @property {ClearInterval} clearInterval - faked `clearInterval`
* @property {SetImmediate} setImmediate - faked `setImmediate`
* @property {ClearImmediate} clearImmediate - faked `clearImmediate`
* @property {CountTimers} countTimers - counts scheduled timers
* @property {RequestAnimationFrame} requestAnimationFrame - schedules a frame callback
* @property {CancelAnimationFrame} cancelAnimationFrame - cancels a frame callback
* @property {RunMicrotasks} runMicrotasks - drains microtasks
* @property {Tick} tick - advances fake time synchronously
* @property {TickAsync} tickAsync - advances fake time asynchronously
* @property {Next} next - runs the next scheduled timer
* @property {NextAsync} nextAsync - runs the next scheduled timer asynchronously
* @property {RunAll} runAll - runs all scheduled timers
* @property {RunToFrame} runToFrame - runs timers up to the next animation frame
* @property {RunAllAsync} runAllAsync - runs all scheduled timers asynchronously
* @property {RunToLast} runToLast - runs timers up to the last scheduled timer
* @property {RunToLastAsync} runToLastAsync - runs timers up to the last scheduled timer asynchronously
* @property {Reset} reset - clears all timers and resets the clock
* @property {SetSystemTime} setSystemTime - sets the clock to a specific wall-clock time
* @property {Jump} jump - advances time and returns the new `now`
* @property {any} performance - fake performance object
* @property {Hrtime} hrtime - faked `process.hrtime`
* @property {Uninstall} uninstall - restores native timers
* @property {string[]} methods - names of faked methods
* @property {boolean} [shouldClearNativeTimers] - inherited from config
* @property {{methodName:string, original:unknown}[] | undefined} timersModuleMethods - saved Node timers module methods
* @property {{methodName:string, original:unknown}[] | undefined} timersPromisesModuleMethods - saved Node timers/promises methods
* @property {Map<VoidVarArgsFunc, AbortSignal>} abortListenerMap - active abort listeners
* @property {SetTickMode} setTickMode - switches the auto-tick mode
* @property {Map<number, Timer>} [timers] - internal timer storage
* @property {TimerHeap} [timerHeap] - internal timer heap
* @property {boolean} [duringTick] - internal flag
* @property {boolean} isNearInfiniteLimit - internal flag indicating the loop limit is nearly reached
* @property {TimerId} [attachedInterval] - internal flag
* @property {ClockTickMode} [tickMode] - internal flag
* @property {Timer[]} [jobs] - internal flag
* @property {IntlWithClock} [Intl] - fake Intl object
* @property {any} [Temporal] - fake Temporal object
*/
/* eslint-enable jsdoc/reject-any-type */
/**
* Configuration object for the `install` method.
* @typedef {object} Config
* @property {number|Date|TemporalTimelike} [now] initial mocked time, as milliseconds since epoch, a Date, a Temporal.Instant, or a Temporal.ZonedDateTime
* @property {FakeMethod[]} [toFake] method names that should be faked
* @property {FakeMethod[]} [toNotFake] method names that should remain native
* @property {number} [loopLimit] maximum number of timers run before aborting with an infinite-loop error
* @property {boolean} [shouldAdvanceTime] automatically increments mocked time while the clock is installed
* @property {number} [advanceTimeDelta] interval in milliseconds used when `shouldAdvanceTime` is enabled
* @property {boolean} [shouldClearNativeTimers] forwards clear calls to native methods when the timer is not fake
* @property {boolean} [ignoreMissingTimers] suppresses errors when a requested timer is missing from the global object
* @property {GlobalObject} [target] global object to install onto
*/
/**
* The internal structure to describe a scheduled fake timer
* @typedef {TimerInitialProps} Timer
* @property {unknown[]} args - arguments passed to the callback
* @property {number} callAt - scheduled execution time
* @property {number} createdAt - time at which the timer was created
* @property {number} id - unique timer identifier
* @property {'Timeout' | 'Interval' | 'Immediate' | 'AnimationFrame' | 'IdleCallback'} type - timer kind
*/
/**
* @callback NodeImmediateHasRef
* @returns {boolean}
*/
/**
* @callback NodeImmediateRef
* @returns {NodeImmediate}
*/
/**
* @callback NodeImmediateUnref
* @returns {NodeImmediate}
*/
/**
* A Node timer
* @typedef {object} NodeImmediate
* @property {NodeImmediateHasRef} hasRef - reports whether the timer keeps the event loop alive
* @property {NodeImmediateRef} ref - marks the timer as referenced
* @property {NodeImmediateUnref} unref - marks the timer as unreferenced
*/
/* eslint-disable complexity */
/**
* Mocks available features in the specified global namespace.
* @param {GlobalObject} _global Namespace to mock (e.g. `window`)
* @returns {FakeTimers}
*/
function withGlobal(_global) {
const maxTimeout = Math.pow(2, 31) - 1; //see https://heycam.github.io/webidl/#abstract-opdef-converttoint
const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs
const NOOP = function () {
return undefined;
};
const NOOP_ARRAY = function () {
return [];
};
const isPresent = {};
let timeoutResult,
addTimerReturnsObject = false;
if (_global.setTimeout) {
isPresent.setTimeout = true;
timeoutResult = _global.setTimeout(NOOP, 0);
addTimerReturnsObject = typeof timeoutResult === "object";
}
isPresent.clearTimeout = Boolean(_global.clearTimeout);
isPresent.setInterval = Boolean(_global.setInterval);
isPresent.clearInterval = Boolean(_global.clearInterval);
isPresent.hrtime =
_global.process && typeof _global.process.hrtime === "function";
isPresent.hrtimeBigint =
isPresent.hrtime && typeof _global.process.hrtime.bigint === "function";
isPresent.nextTick =
_global.process && typeof _global.process.nextTick === "function";
const utilPromisify = _global.process && require("util").promisify;
isPresent.performance =
_global.performance && typeof _global.performance.now === "function";
const hasPerformancePrototype =
_global.Performance &&
(typeof _global.Performance).match(/^(function|object)$/);
const hasPerformanceConstructorPrototype =
_global.performance &&
_global.performance.constructor &&
_global.performance.constructor.prototype;
isPresent.queueMicrotask = Object.prototype.hasOwnProperty.call(
_global,
"queueMicrotask",
);
isPresent.requestAnimationFrame =
_global.requestAnimationFrame &&
typeof _global.requestAnimationFrame === "function";
isPresent.cancelAnimationFrame =
_global.cancelAnimationFrame &&
typeof _global.cancelAnimationFrame === "function";
isPresent.requestIdleCallback =
_global.requestIdleCallback &&
typeof _global.requestIdleCallback === "function";
isPresent.cancelIdleCallback =
_global.cancelIdleCallback &&
typeof _global.cancelIdleCallback === "function";
isPresent.setImmediate =
_global.setImmediate && typeof _global.setImmediate === "function";
isPresent.clearImmediate =
_global.clearImmediate && typeof _global.clearImmediate === "function";
isPresent.Intl = _global.Intl && typeof _global.Intl === "object";
isPresent.Temporal =
_global.Temporal !== null &&
typeof _global.Temporal === "object" &&
typeof _global.Temporal.Now !== "undefined" &&
typeof _global.Temporal.Instant !== "undefined";
if (_global.clearTimeout) {
_global.clearTimeout(timeoutResult);
}
const NativeDate = _global.Date;
const NativeIntl = isPresent.Intl
? Object.defineProperties(
Object.create(null),
Object.getOwnPropertyDescriptors(_global.Intl),
)
: undefined;
const NativeTemporal = isPresent.Temporal ? _global.Temporal : undefined;
let uniqueTimerId = idCounterStart;
/** @type {number} */
let uniqueTimerOrder = 0;
if (NativeDate === undefined) {
throw new Error(
"The global scope doesn't have a `Date` object" +
" (see https://github.com/sinonjs/sinon/issues/1852#issuecomment-419622780)",
);
}
isPresent.Date = true;
/**
* The PerformanceEntry object encapsulates a single performance metric
* that is part of the browser's performance timeline.
*
* This is an object returned by the `mark` and `measure` methods on the Performance prototype
*/
class FakePerformanceEntry {
constructor(name, entryType, startTime, duration) {
this.name = name;
this.entryType = entryType;
this.startTime = startTime;
this.duration = duration;
}
toJSON() {
return JSON.stringify({ ...this });
}
}
/**
* @param {number} num
* @returns {boolean}
*/
function isNumberFinite(num) {
if (Number.isFinite) {
return Number.isFinite(num);
}
return isFinite(num);
}
/**
* @param {Clock} clock
* @param {number} i
*/
function checkIsNearInfiniteLimit(clock, i) {
if (clock.loopLimit && i === clock.loopLimit - 1) {
clock.isNearInfiniteLimit = true;
}
}
/**
* @param {Clock} clock
*/
function resetIsNearInfiniteLimit(clock) {
if (clock) {
clock.isNearInfiniteLimit = false;
}
}
/**
* Parse strings like "01:10:00" (meaning 1 hour, 10 minutes, 0 seconds) into
* number of milliseconds. This is used to support human-readable strings passed
* to clock.tick()
* @param {string} str
* @returns {number}
*/
function parseTime(str) {
if (!str) {
return 0;
}
const strings = str.split(":");
const l = strings.length;
let i = l;
let ms = 0;
let parsed;
if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
throw new Error(
"tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits",
);
}
while (i--) {
parsed = parseInt(strings[i], 10);
if (parsed >= 60) {
throw new Error(`Invalid time ${str}`);
}
ms += parsed * Math.pow(60, l - i - 1);
}
return ms * 1000;
}
/**
* Get the decimal part of the millisecond value as nanoseconds
* @param {number} msFloat the number of milliseconds
* @returns {number} an integer number of nanoseconds in the range [0,1e6)
*
* Example: nanoRemainer(123.456789) -> 456789
*/
function nanoRemainder(msFloat) {
const modulo = 1e6;
const remainder = (msFloat * 1e6) % modulo;
const positiveRemainder =
remainder < 0 ? remainder + modulo : remainder;
return Math.floor(positiveRemainder);
}
/**
* Used to grok the `now` parameter to createClock.
* @param {Date|number|TemporalTimelike} epoch the system time
* @returns {number}
*/
function getEpoch(epoch) {
if (!epoch) {
return 0;
}
if (typeof epoch === "number") {
return epoch;
}
if (typeof (/** @type {Date} */ (epoch).getTime) === "function") {
return /** @type {Date} */ (epoch).getTime();
}
if (
typeof (
/** @type {TemporalTimelike} */ (epoch).epochMilliseconds
) === "number"
) {
// Temporal.Instant and Temporal.ZonedDateTime both have epochMilliseconds
return /** @type {TemporalTimelike} */ (epoch).epochMilliseconds;
}
throw new TypeError("now should be milliseconds since UNIX epoch");
}
/**
* @param {number} from
* @param {number} to
* @param {Timer} timer
* @returns {boolean}
*/
function inRange(from, to, timer) {
return timer && timer.callAt >= from && timer.callAt <= to;
}
/**
* @param {Clock} clock
* @param {Timer} job
* @returns {Error}
*/
function getInfiniteLoopError(clock, job) {
const infiniteLoopError = new Error(
`Aborting after running ${clock.loopLimit} timers, assuming an infinite loop!`,
);
if (!job.error) {
return infiniteLoopError;
}
// pattern never matched in Node
const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/;
let clockMethodPattern = new RegExp(
String(Object.keys(clock).join("|")),
);
if (addTimerReturnsObject) {
// node.js environment
clockMethodPattern = new RegExp(
`\\s+at (Object\\.)?(?:${Object.keys(clock).join("|")})\\s+`,
);
}
let matchedLineIndex = -1;
job.error.stack.split("\n").some(function (line, i) {
// If we've matched a computed target line (e.g. setTimeout) then we
// don't need to look any further. Return true to stop iterating.
const matchedComputedTarget = line.match(computedTargetPattern);
/* istanbul ignore if */
if (matchedComputedTarget) {
matchedLineIndex = i;
return true;
}
// If we've matched a clock method line, then there may still be
// others further down the trace. Return false to keep iterating.
const matchedClockMethod = line.match(clockMethodPattern);
if (matchedClockMethod) {
matchedLineIndex = i;
return false;
}
// If we haven't matched anything on this line, but we matched
// previously and set the matched line index, then we can stop.
// If we haven't matched previously, then we should keep iterating.
return matchedLineIndex >= 0;
});
const stack = `${infiniteLoopError}\n${job.type || "Microtask"} - ${
job.func.name || "anonymous"
}\n${job.error.stack
.split("\n")
.slice(matchedLineIndex + 1)
.join("\n")}`;
try {
Object.defineProperty(infiniteLoopError, "stack", {
value: stack,
});
} catch {
// noop
}
return infiniteLoopError;
}
/**
* @returns {typeof Date & { clock: Clock }}
*/
function createDate() {
class ClockDate extends NativeDate {
/** @type {Clock} */
static clock;
constructor(...args) {
// Preserve fake time when Date is called without arguments.
if (args.length === 0) {
super(ClockDate.clock.now);
} else {
// The subclass is intentionally thin for explicit args.
// @ts-expect-error Date constructor overloads are intentionally dynamic.
super(...args);
}
// ensures identity checks using the constructor prop still works
// this should have no other functional effect
Object.defineProperty(this, "constructor", {
value: NativeDate,
enumerable: false,
});
}
static [Symbol.hasInstance](instance) {
return instance instanceof NativeDate;
}
}
ClockDate.isFake = true;
if (NativeDate.now) {
ClockDate.now = function now() {
return ClockDate.clock.now;
};
}
const NativeDateWithToSource =
/** @type {typeof Date & { toSource?: () => string }} */ (
NativeDate
);
if (NativeDateWithToSource.toSource) {
ClockDate.toSource = function toSource() {
return NativeDateWithToSource.toSource();
};
}
ClockDate.toString = function toString() {
return NativeDateWithToSource.toString();
};
// noinspection UnnecessaryLocalVariableJS
/**
* A normal Class constructor cannot be called without `new`, but Date can, so we need
* to wrap it in a Proxy in order to ensure this functionality of Date is kept intact
* @type {typeof ClockDate}
*/
const ClockDateProxy = new Proxy(ClockDate, {
// handler for [[Call]] invocations (i.e. not using `new`)
apply() {
// the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2.
// This remains so in the 10th edition of 2019 as well.
if (this instanceof ClockDate) {
throw new TypeError(
"A Proxy should only capture `new` calls with the `construct` handler. This is not supposed to be possible, so check the logic.",
);
}
return new NativeDate(ClockDate.clock.now).toString();
},
});
return /** @type {typeof Date & { clock: Clock }} */ (
/** @type {unknown} */ (ClockDateProxy)
);
}
/**
* Mirror Intl by default on our fake implementation
*
* Most of the properties are the original native ones,
* but we need to take control of those that have a
* dependency on the current clock.
* @param {Clock} clock
* @returns {IntlWithClock} the partly fake Intl implementation
*/
function createIntl(clock) {
/** @type {IntlWithClock} */
const IntlWithClock = { clock: clock };
/*
* All properties of Intl are non-enumerable, so we need
* to do a bit of work to get them out.
*/
Object.getOwnPropertyNames(NativeIntl).forEach(
(property) => (IntlWithClock[property] = NativeIntl[property]),
);
IntlWithClock.DateTimeFormat = function (...args) {
const realFormatter = new NativeIntl.DateTimeFormat(...args);
const formatter = {};
["formatRange", "formatRangeToParts", "resolvedOptions"].forEach(
(method) => {
formatter[method] =
realFormatter[method].bind(realFormatter);
},
);
["format", "formatToParts"].forEach((method) => {
formatter[method] = function (date) {
return realFormatter[method](
date || IntlWithClock.clock.now,
);
};
});
return formatter;
};
IntlWithClock.DateTimeFormat.prototype = Object.create(
NativeIntl.DateTimeFormat.prototype,
);
IntlWithClock.DateTimeFormat.supportedLocalesOf =
NativeIntl.DateTimeFormat.supportedLocalesOf;
return IntlWithClock;
}
//eslint-disable-next-line jsdoc/require-jsdoc
function createTemporal(clock, getNanos) {
const fakeNow = {
instant() {
return NativeTemporal.Instant.fromEpochNanoseconds(
BigInt(clock.now) * 1_000_000n + BigInt(getNanos()),
);
},
timeZoneId() {
return NativeTemporal.Now.timeZoneId();
},
zonedDateTimeISO(timeZone) {
const tz = timeZone ?? NativeTemporal.Now.timeZoneId();
return fakeNow.instant().toZonedDateTimeISO(tz);
},
plainDateTimeISO(timeZone) {
return fakeNow.zonedDateTimeISO(timeZone).toPlainDateTime();
},
plainDateISO(timeZone) {
return fakeNow.zonedDateTimeISO(timeZone).toPlainDate();
},
plainTimeISO(timeZone) {
return fakeNow.zonedDateTimeISO(timeZone).toPlainTime();
},
};
const TemporalWithClock = Object.create(
Object.getPrototypeOf(NativeTemporal),
);
[
...Object.getOwnPropertyNames(NativeTemporal),
...Object.getOwnPropertySymbols(NativeTemporal),
].forEach((prop) => {
Object.defineProperty(
TemporalWithClock,
prop,
Object.getOwnPropertyDescriptor(NativeTemporal, prop),
);
});
// Temporal.Now is writable:false in the spec so we must use defineProperty
Object.defineProperty(TemporalWithClock, "Now", {
value: fakeNow,
writable: true,
enumerable: false,
configurable: true,
});
return TemporalWithClock;
}
//eslint-disable-next-line jsdoc/require-jsdoc
function enqueueJob(clock, job) {
// enqueues a microtick-deferred task - ecma262/#sec-enqueuejob
if (!clock.jobs) {
clock.jobs = [];
}
clock.jobs.push(job);
}
//eslint-disable-next-line jsdoc/require-jsdoc
function runJobs(clock) {
// runs all microtick-deferred tasks - ecma262/#sec-runjobs
if (!clock.jobs) {
return;
}
const wasNearLimit = clock.isNearInfiniteLimit;
for (let i = 0; i < clock.jobs.length; i++) {
const job = clock.jobs[i];
job.func.apply(null, job.args);
checkIsNearInfiniteLimit(clock, i);
if (clock.loopLimit && i > clock.loopLimit) {
throw getInfiniteLoopError(clock, job);
}
}
if (!wasNearLimit) {
resetIsNearInfiniteLimit(clock);
}
clock.jobs = [];
}
/**
* A compact "soonest timer first" container.
*
* Think of this as a waiting room for scheduled callbacks where the next
* callback to run is always kept at the front of the list. The internal
* array is arranged so we can find, add, remove, and reorder timers
* efficiently without sorting the whole list every time something changes.
*
* The important idea is not the data structure name, but the behavior:
* the timer that should run next stays near the front, and when one timer
* moves, the rest are shifted just enough to keep that promise true.
*/
class TimerHeap {
constructor() {
this.timers = [];
}
/**
* Look at the next timer without removing it.
* This is the timer the clock would run first if time advanced now.
* @returns {Timer}
*/
peek() {
return this.timers[0];
}
/**
* Add a timer to the waiting room, then move it upward until it is in
* the right place relative to the timers it should run before and after.
* @param {Timer} timer
*/
push(timer) {
this.timers.push(timer);
this.bubbleUp(this.timers.length - 1);
}
/**
* Remove and return the next timer to run.
*
* We pull the front timer out, move the last timer into the empty spot,
* and then shift that replacement down until the ordering is correct
* again. That avoids rebuilding the whole list from scratch.
* @returns {Timer|undefined}
*/
pop() {
if (this.timers.length === 0) {
return undefined;
}
const first = this.timers[0];
const last = this.timers.pop();
if (this.timers.length > 0) {
this.timers[0] = last;
last.heapIndex = 0;
this.bubbleDown(0);
}
delete first.heapIndex;
return first;
}
/**
* Remove a specific timer from the waiting room.
*
* The heap stores timers in a shape that lets us jump directly to the
* timer's current position, replace it with the last timer, and then
* move that replacement up or down until the ordering is correct again.
* @param {Timer} timer
* @returns {boolean}
*/
remove(timer) {
const index = timer.heapIndex;
if (index === undefined || this.timers[index] !== timer) {
return false;
}
const last = this.timers.pop();
if (timer !== last) {
this.timers[index] = last;
last.heapIndex = index;
if (compareTimers(last, timer) < 0) {
this.bubbleUp(index);
} else {
this.bubbleDown(index);
}
}
delete timer.heapIndex;
return true;
}
/**
* Move a timer toward the front until it is no longer "earlier" than
* the timer above it.
*
* Conceptually, this is what happens when something newly scheduled
* turns out to belong ahead of its parent in the waiting room. We keep
* swapping it upward until it is no longer out of place.
* @param {number} index
*/
bubbleUp(index) {
const timer = this.timers[index];
let currentIndex = index;
while (currentIndex > 0) {
const parentIndex = Math.floor((currentIndex - 1) / 2);
const parent = this.timers[parentIndex];
if (compareTimers(timer, parent) < 0) {
this.timers[currentIndex] = parent;
parent.heapIndex = currentIndex;
currentIndex = parentIndex;
} else {
break;
}
}
this.timers[currentIndex] = timer;
timer.heapIndex = currentIndex;
}
/**
* Move a timer away from the front until the timer below it is no
* longer supposed to run after it.
*
* This is the opposite of `bubbleUp`: when a timer at the front is
* removed or moved, the replacement may be too far ahead, so we
* repeatedly swap it downward with the best child until the waiting
* room is ordered again.
* @param {number} index
*/
bubbleDown(index) {
const timer = this.timers[index];
let currentIndex = index;
const halfLength = Math.floor(this.timers.length / 2);
while (currentIndex < halfLength) {
const leftIndex = currentIndex * 2 + 1;
const rightIndex = leftIndex + 1;
let bestChildIndex = leftIndex;
let bestChild = this.timers[leftIndex];
if (
rightIndex < this.timers.length &&
compareTimers(this.timers[rightIndex], bestChild) < 0
) {
bestChildIndex = rightIndex;
bestChild = this.timers[rightIndex];
}
if (compareTimers(bestChild, timer) < 0) {
this.timers[currentIndex] = bestChild;
bestChild.heapIndex = currentIndex;
currentIndex = bestChildIndex;
} else {
break;
}
}
this.timers[currentIndex] = timer;
timer.heapIndex = currentIndex;
}
}
/**
* Ensure timer storage and heap stay in sync even if a clear path touches
* timer state before anything has been scheduled.
*
* Why do we need two data structures to keep tabs on timers?
* 1. Fast ID Lookup (clock.timers): This is a Map from timer IDs to their respective timer objects. It allows clearTimeout(id) and
* clearInterval(id) to be $O(1)$ operations. Without this map, finding a specific timer in the heap to remove it would require a linear
* $O(n)$ search, which would significantly degrade performance as the number of active timers grows.
* 2. Efficient Scheduling (clock.timerHeap): This is a priority queue (min-heap) that keeps timers ordered by their execution time (callAt). It
* allows the library to instantly find the next timer to run (peek() in $O(1)$) and efficiently update the schedule when timers are added or
* removed ($O(\log n)$).
*
* In short: clock.timers provides fast access by ID, while clock.timerHeap provides fast access by Time. Removing either one would make common
* operations (like clearing or finding the next timer) much slower.
* @param {Clock} clock
*/
function ensureTimerState(clock) {
if (!clock.timers) {
clock.timers = new Map();
clock.timerHeap = new TimerHeap();
}
}
/**
* @param {Clock} clock
* @param {number} id
* @returns {boolean}
*/
function hasTimer(clock, id) {
return clock.timers ? clock.timers.has(id) : false;
}
/**
* @param {Clock} clock
* @param {number} id
* @returns {Timer}
*/
function getTimer(clock, id) {
return clock.timers ? clock.timers.get(id) : undefined;
}
/**
* @param {Clock} clock
* @param {Timer} timer
*/
function setTimer(clock, timer) {
ensureTimerState(clock);
clock.timers.set(timer.id, timer);
}
/**
* @param {Clock} clock
* @param {number} id
* @returns {boolean}
*/
function deleteTimer(clock, id) {
return clock.timers ? clock.timers.delete(id) : false;
}
/**
* @param {Clock} clock
* @param {(timer: Timer) => void} callback
*/
function forEachActiveTimer(clock, callback) {
if (!clock.timers) {
return;
}
for (const timer of clock.timers.values()) {
callback(timer);
}
}
/**
* @param {Clock} clock
*/
function rebuildTimerHeap(clock) {
clock.timerHeap = new TimerHeap();
forEachActiveTimer(clock, (timer) => {
clock.timerHeap.push(timer);
});
}
/**
* @param {Clock} clock
* @param {TimerInitialProps} timer
* @returns {TimerId} id of the created timer
*/
function addTimer(clock, timer) {
if (timer.func === undefined) {
throw new Error("Callback must be provided to timer calls");
}
if (typeof timer.func !== "function") {
throw new TypeError(
`[ERR_INVALID_CALLBACK]: Callback must be a function. Received ${
timer.func
} of type ${typeof timer.func}`,
);
}
if (clock.isNearInfiniteLimit) {
timer.error = new Error();
}
timer.type = timer.immediate ? "Immediate" : "Timeout";
if (Object.prototype.hasOwnProperty.call(timer, "delay")) {
if (typeof timer.delay !== "number") {
timer.delay = parseInt(timer.delay, 10);
}
if (!isNumberFinite(timer.delay)) {
timer.delay = 0;
}
timer.delay = timer.delay > maxTimeout ? 1 : timer.delay;
timer.delay = Math.max(0, timer.delay);
}
if (Object.prototype.hasOwnProperty.call(timer, "interval")) {
timer.type = "Interval";
timer.interval = timer.interval > maxTimeout ? 1 : timer.interval;
}
if (Object.prototype.hasOwnProperty.call(timer, "animation")) {
timer.type = "AnimationFrame";
timer.animation = true;
}
if (
Object.prototype.hasOwnProperty.call(timer, "requestIdleCallback")
) {
// mark timer as IdleCallback type if it has no delay, otherwise it'd be of type timeout
// this way we are able to sort such that the timer only gets called when there's truly no pending task to run
if (!timer.delay) {
timer.type = "IdleCallback";
}
timer.requestIdleCallback = true;
}
ensureTimerState(clock);
while (hasTimer(clock, uniqueTimerId)) {
uniqueTimerId++;
if (uniqueTimerId >= Number.MAX_SAFE_INTEGER) {
uniqueTimerId = idCounterStart;
}
}
timer.id = uniqueTimerId++;
if (uniqueTimerId >= Number.MAX_SAFE_INTEGER) {
uniqueTimerId = idCounterStart;
}
timer.order = uniqueTimerOrder++;
timer.createdAt = clock.now;
timer.callAt =
clock.now +
(parseInt(String(timer.delay)) || (clock.duringTick ? 1 : 0));
setTimer(clock, timer);
clock.timerHeap.push(timer);
if (addTimerReturnsObject) {
const res = {
refed: true,
ref: function () {
this.refed = true;
return this;
},
unref: function () {
this.refed = false;
return this;
},
hasRef: function () {
return this.refed;
},
refresh: function () {
timer.callAt =
clock.now +
(parseInt(String(timer.delay)) ||
(clock.duringTick ? 1 : 0));
clock.timerHeap.remove(timer);
timer.order = uniqueTimerOrder++;
setTimer(clock, timer);
clock.timerHeap.push(timer);
return this;
},
[Symbol.toPrimitive]: function () {
return timer.id;
},
};
return res;
}
return timer.id;
}
/* eslint consistent-return: "off" */
/**
* Timer comparator
* @param {Timer} a
* @param {Timer} b
* @returns {number}
*/
function compareTimers(a, b) {
// Sort IdleCallback timers to the bottom when scheduled for the same time
if (a.type === "IdleCallback" && b.type !== "IdleCallback") {
return 1;
}
if (a.type !== "IdleCallback" && b.type === "IdleCallback") {
return -1;
}
// Sort first by absolute timing
if (a.callAt < b.callAt) {
return -1;
}
if (a.callAt > b.callAt) {
return 1;
}
// Sort next by immediate, immediate timers take precedence
if (a.immediate && !b.immediate) {
return -1;
}
if (!a.immediate && b.immediate) {
return 1;
}
if (a.order < b.order) {
return -1;
}
if (a.order > b.order) {
return 1;
}
// Sort next by creation time, earlier-created timers take precedence
if (a.createdAt < b.createdAt) {
return -1;
}
if (a.createdAt > b.createdAt) {
return 1;
}
// Sort next by id, lower-id timers take precedence
if (a.id < b.id) {
return -1;
}
if (a.id > b.id) {
return 1;
}
// As timer ids are unique, no fallback `0` is necessary
return 0;
}
/**
* @param {Clock} clock
* @param {number} from
* @param {number} to
* @returns {Timer}
*/
function firstTimerInRange(clock, from, to) {
if (!clock.timerHeap) {
return null;
}
const timers = clock.timerHeap.timers;
if (timers.length === 1 && timers[0].requestIdleCallback) {
return timers[0];
}
const first = clock.timerHeap.peek();
if (first && inRange(from, to, first)) {
return first;
}
/**
* @type {?Timer}
*/
let timer = null;
for (let i = 0; i < timers.length; i++) {
if (
inRange(from, to, timers[i]) &&
(!timer || compareTimers(timer, timers[i]) === 1)
) {
timer = timers[i];
}
}
return timer;
}
/**
* @param {Clock} clock
* @returns {Timer}
*/
function firstTimer(clock) {
if (!clock.timerHeap) {
return null;
}
return clock.timerHeap.peek() || null;
}
/**
* @param {Clock} clock
* @returns {Timer}
*/
function lastTimer(clock) {
if (!clock.timerHeap) {
return null;
}
const timers = clock.timerHeap.timers;
let timer = null;
for (let i = 0; i < timers.length; i++) {
if (!timer || compareTimers(timer, timers[i]) === -1) {
timer = timers[i];
}
}
return timer;
}
/**
* @param {Clock} clock
* @param {Timer} timer
*/
function callTimer(clock, timer) {
if (typeof timer.interval === "number") {
clock.timerHeap.remove(timer);
timer.callAt += timer.interval;
timer.order = uniqueTimerOrder++;
if (clock.isNearInfiniteLimit) {
timer.error = new Error();
}
clock.timerHeap.push(timer);
} else {
deleteTimer(clock, timer.id);
clock.timerHeap.remove(timer);
}
if (typeof timer.func === "function") {
timer.func.apply(null, timer.args);
}
}
/**
* Gets clear handler name for a given timer type
* @param {string} ttype
* @returns {string}
*/
function getClearHandler(ttype) {
if (ttype === "IdleCallback" || ttype === "AnimationFrame") {
return `cancel${ttype}`;
}
return `clear${ttype}`;
}
/**
* Gets schedule handler name for a given timer type
* @param {string} ttype
* @returns {string}
*/
function getScheduleHandler(ttype) {
if (ttype === "IdleCallback" || ttype === "AnimationFrame") {
return `request${ttype}`;
}
return `set${ttype}`;
}
/**
* Creates an anonymous function to warn only once
* @returns {(msg: string) => void}
*/
function createWarnOnce() {
let calls = 0;
return function (msg) {
// eslint-disable-next-line
!calls++ && console.warn(msg);
};
}
const warnOnce = createWarnOnce();
/**
* @param {Clock} clock
* @param {TimerId} timerId
* @param {string} ttype
* @returns {void}
*/
function clearTimer(clock, timerId, ttype) {
if (!timerId) {
// null appears to be allowed in most browsers, and appears to be
// relied upon by some libraries, like Bootstrap carousel
return;
}
// in Node, the ID is stored as the primitive value for `Timeout` objects
// for `Immediate` objects, no ID exists, so it gets coerced to NaN
const id = Number(timerId);
if (Number.isNaN(id) || id < idCounterStart) {
const handlerName = getClearHandler(ttype);
if (clock.shouldClearNativeTimers === true) {
const nativeHandler = clock[`_${handlerName}`];
return typeof nativeHandler === "function"
? nativeHandler(timerId)
: undefined;
}
// Include the stacktrace, excluding the 'error' line
const stackTrace = new Error().stack
.split("\n")
.slice(1)
.join("\n");
warnOnce(
`FakeTimers: ${handlerName} was invoked to clear a native timer instead of one created by this library.` +
"\nTo automatically clean-up native timers, use `shouldClearNativeTimers`." +
`\n${stackTrace}`,
);
}
if (hasTimer(clock, id)) {
// check that the ID matches a timer of the correct type
const timer = getTimer(clock, id);
if (
timer.type === ttype ||
(timer.type === "Timeout" && ttype === "Interval") ||
(timer.type === "Interval" && ttype === "Timeout")
) {
deleteTimer(clock, id);
clock.timerHeap.remove(timer);
} else {
const clear = getClearHandler(ttype);
const schedule = getScheduleHandler(timer.type);
throw new Error(
`Cannot clear timer: timer created with ${schedule}() but cleared with ${clear}()`,
);
}
}
}
/**
* @param {object} target the target containing the method to replace
* @param {string} method the keyname of the method on the target
* @param {Clock} clock
*/
function hijackMethod(target, method, clock) {
clock[method].hasOwnProperty = Object.prototype.hasOwnProperty.call(
target,
method,
);
clock[`_${method}`] = target[method];
if (method === "Date") {
target[method] = clock[method];
} else if (method === "Intl") {
target[method] = clock[method];
} else if (method === "Temporal") {
target[method] = clock[method];
} else if (method === "performance") {
const originalPerfDescriptor = Object.getOwnPropertyDescriptor(
target,
method,
);
// JSDOM has a read only performance field so we have to save/copy it differently
if (
originalPerfDescriptor &&
originalPerfDescriptor.get &&
!originalPerfDescriptor.set
) {
Object.defineProperty(
clock,
`_${method}`,
originalPerfDescriptor,
);
const perfDescriptor = Object.getOwnPropertyDescriptor(
clock,
method,
);
Object.defineProperty(target, method, perfDescriptor);
} else {
target[method] = clock[method];
}
} else {
target[method] = function () {
return clock[method].apply(clock, arguments);
};
Object.defineProperties(
target[method],
Object.getOwnPropertyDescriptors(clock[method]),
);
}
target[method].clock = clock;
}
/**
* @param {Clock} clock
* @param {number} advanceTimeDelta
*/
function doIntervalTick(clock, advanceTimeDelta) {
clock.tick(advanceTimeDelta);
}
/** @type {Timers} */
const timers = {
setTimeout: _global.setTimeout,
clearTimeout: _global.clearTimeout,
setInterval: _global.setInterval,
clearInterval: _global.clearInterval,
Date: _global.Date,
};
if (isPresent.setImmediate) {
timers.setImmediate = _global.setImmediate;
}
if (isPresent.clearImmediate) {
timers.clearImmediate = _global.clearImmediate;
}
if (isPresent.hrtime) {
timers.hrtime = _global.process.hrtime;
}
if (isPresent.nextTick) {
timers.nextTick = _global.process.nextTick;
}
if (isPresent.performance) {
timers.performance = _global.performance;
}
if (isPresent.requestAnimationFrame) {
timers.requestAnimationFrame = _global.requestAnimationFrame;
}
if (isPresent.queueMicrotask) {
timers.queueMicrotask = _global.queueMicrotask;
}
if (isPresent.cancelAnimationFrame) {
timers.cancelAnimationFrame = _global.cancelAnimationFrame;
}
if (isPresent.requestIdleCallback) {
timers.requestIdleCallback = _global.requestIdleCallback;
}
if (isPresent.cancelIdleCallback) {
timers.cancelIdleCallback = _global.cancelIdleCallback;
}
if (isPresent.Intl) {
timers.Intl = NativeIntl;
}
if (isPresent.Temporal) {
timers.Temporal = NativeTemporal;
}
const originalSetTimeout = _global.setImmediate || _global.setTimeout;
const originalClearInterval = _global.clearInterval;
const originalSetInterval = _global.setInterval;
/**
* @param {Date|number|TemporalTimelike} [start] the system time - non-integer values are floored
* @param {number} [loopLimit] maximum number of timers that will be run when calling runAll()
* @returns {Clock}
*/
function createClock(start, loopLimit) {
/** @type {number} */
// eslint-disable-next-line no-param-reassign
start = Math.floor(getEpoch(start));
const startTimestamp = start;
// eslint-disable-next-line no-param-reassign
loopLimit = loopLimit || 1000;
/** @type {number} */
let nanos = 0;
let uninstalled = false;
/** @type {number[]} */
const adjustedSystemTime = [0, 0]; // [millis, nanoremainder]
/** @type {Clock} */
const clock = /** @type {Clock} */ ({
now: start,
Date: createDate(),
loopLimit: loopLimit,
isNearInfiniteLimit: false,
tickMode: { mode: "manual", counter: 0, delta: undefined },
});
clock.Date.clock = clock;
//eslint-disable-next-line jsdoc/require-jsdoc
function getTimeToNextFrame() {
return 16 - ((clock.now - startTimestamp) % 16);
}
//eslint-disable-next-line jsdoc/require-jsdoc
function hrtime(prev) {
const millisSinceStart =
clock.now - adjustedSystemTime[0] - startTimestamp;
const secsSinceStart = Math.floor(millisSinceStart / 1000);
const remainderInNanos =
(millisSinceStart - secsSinceStart * 1e3) * 1e6 +
nanos -
adjustedSystemTime[1];
if (Array.isArray(prev)) {
if (prev[1] > 1e9) {
throw new TypeError(
"Number of nanoseconds can't exceed a billion",
);
}
const oldSecs = prev[0];
let nanoDiff = remainderInNanos - prev[1];
let secDiff = secsSinceStart - oldSecs;
if (nanoDiff < 0) {
nanoDiff += 1e9;
secDiff -= 1;
}
return [secDiff, nanoDiff];
}
return [secsSinceStart, remainderInNanos];
}
/**
* A high resolution timestamp in milliseconds.
* @typedef {number} DOMHighResTimeStamp
*/
/**
* performance.now()
* @returns {DOMHighResTimeStamp}
*/
function fakePerformanceNow() {
const hrt = hrtime();
const millis = hrt[0] * 1000 + hrt[1] / 1e6;
return millis;
}
if (isPresent.hrtimeBigint) {
hrtime.bigint = function () {
const parts = hrtime();
return BigInt(parts[0]) * BigInt(1e9) + BigInt(parts[1]);
};
}
if (isPresent.Intl) {
clock.Intl = createIntl(clock);
clock.Intl.clock = clock;
}
if (isPresent.Temporal) {
clock.Temporal = createTemporal(clock, () => nanos);
}
/**
* @param {SetTickModeConfig} tickModeConfig - The new configuration for how the clock should tick.
*/
clock.setTickMode = function (tickModeConfig) {
const { mode: newMode, delta: newDelta } =
/** @type {SetTickModeConfig} */ (tickModeConfig);
const { mode: oldMode, delta: oldDelta } = clock.tickMode;
if (newMode === oldMode && newDelta === oldDelta) {
return;
}
if (oldMode === "interval") {
originalClearInterval(clock.attachedInterval);
}
clock.tickMode = {
counter: clock.tickMode.counter + 1,
mode: newMode,
delta: newDelta,
};
if (newMode === "nextAsync") {
advanceUntilModeChanges();
} else if (newMode === "interval") {
createIntervalTick(clock, newDelta || 20);
}
};
/**
* Keeps advancing the native event loop until the tick mode changes.
* @returns {Promise<void>}
*/
async function advanceUntilModeChanges() {
/**
* Waits for one native macrotask and then one microtask turn.
* @returns {Promise<void>}
*/
async function newMacrotask() {
// MessageChannel ensures that setTimeout is not throttled to 4ms.
// https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
// https://stackblitz.com/edit/stackblitz-starters-qtlpcc
const channel = new MessageChannel();
await new Promise((resolve) => {
channel.port1.onmessage = () => {
resolve(undefined);
channel.port1.close();
};
channel.port2.postMessage(undefined);
});
channel.port1.close();
channel.port2.close();
// setTimeout ensures microtask queue is emptied
await new Promise((resolve) => {
originalSetTimeout(resolve);
});
}
const { counter } = clock.tickMode;
while (clock.tickMode.counter === counter) {
await newMacrotask();
if (clock.tickMode.counter !== counter) {
return;
}
clock.next();
}
}
/**
* Temporarily pauses nextAsync auto-ticking while an async operation runs.
* @param {Promise<unknown>} promise
* @returns {Promise<unknown>}
*/
function pauseAutoTickUntilFinished(promise) {
if (clock.tickMode.mode !== "nextAsync") {
return promise;
}
clock.setTickMode({ mode: "manual" });
return promise.finally(() => {
if (!uninstalled) {
clock.setTickMode({ mode: "nextAsync" });
}
});
}
/**
* Returns the remaining time in the current idle window.
* @returns {number}
*/
function getTimeToNextIdlePeriod() {
let timeToNextIdlePeriod = 0;
if (clock.countTimers() > 0) {
timeToNextIdlePeriod = 50; // const for now
}
return timeToNextIdlePeriod;
}
clock.requestIdleCallback = function requestIdleCallback(
func,
{ timeout } = /** @type {{ timeout?: number }} */ ({}),
) {
/**
* @type {IdleDeadline}
*/
const idleDeadline = {
didTimeout: true,
timeRemaining: getTimeToNextIdlePeriod,
};
const result = addTimer(clock, {
func: func,
args: [idleDeadline],
delay: timeout,
requestIdleCallback: true,
});
return Number(result);
};
clock.cancelIdleCallback = function cancelIdleCallback(timerId) {
return clearTimer(clock, timerId, "IdleCallback");
};
clock.setTimeout = function setTimeout(func, timeout) {
return addTimer(clock, {
func: func,
args: Array.prototype.slice.call(arguments, 2),
delay: timeout,
});
};
if (typeof _global.Promise !== "undefined" && utilPromisify) {
clock.setTimeout[utilPromisify.custom] =
function promisifiedSetTimeout(timeout, arg) {
return new _global.Promise(function setTimeoutExecutor(
resolve,
) {
addTimer(clock, {
func: resolve,
args: [arg],
delay: timeout,
});
});
};
}
clock.clearTimeout = function clearTimeout(timerId) {
return clearTimer(clock, timerId, "Timeout");
};
clock.nextTick = function nextTick(func) {
return enqueueJob(clock, {
func: func,
args: Array.prototype.slice.call(arguments, 1),
error: clock.isNearInfiniteLimit ? new Error() : null,
});
};
clock.queueMicrotask = function queueMicrotask(func) {
return clock.nextTick(func); // explicitly drop additional arguments
};
clock.setInterval = function setInterval(func, timeout) {
// eslint-disable-next-line no-param-reassign
timeout = parseInt(String(timeout), 10);
return addTimer(clock, {
func: func,
args: Array.prototype.slice.call(arguments, 2),
delay: timeout,
interval: timeout,
});
};
clock.clearInterval = function clearInterval(timerId) {
return clearTimer(clock, timerId, "Interval");
};
if (isPresent.setImmediate) {
clock.setImmediate = /** @type {SetImmediate} */ (
function setImmediate(func) {
return addTimer(clock, {
func: func,
args: Array.prototype.slice.call(arguments, 1),
immediate: true,
});
}
);
if (typeof _global.Promise !== "undefined" && utilPromisify) {
clock.setImmediate[utilPromisify.custom] =
function promisifiedSetImmediate(arg) {
return new _global.Promise(
function setImmediateExecutor(resolve) {
addTimer(clock, {
func: resolve,
args: [arg],
immediate: true,
});
},
);
};
}
clock.clearImmediate = function clearImmediate(timerId) {
return clearTimer(clock, timerId, "Immediate");
};
}
clock.countTimers = function countTimers() {
return (
(clock.timerHeap ? clock.timerHeap.timers.length : 0) +
(clock.jobs || []).length
);
};
clock.requestAnimationFrame = function requestAnimationFrame(func) {
const result = addTimer(clock, {
func: func,
delay: getTimeToNextFrame(),
get args() {
return [fakePerformanceNow()];
},
animation: true,
});
return Number(result);
};
clock.cancelAnimationFrame = function cancelAnimationFrame(timerId) {
return clearTimer(clock, timerId, "AnimationFrame");
};
clock.runMicrotasks = function runMicrotasks() {
runJobs(clock);
};
//eslint-disable-next-line jsdoc/require-jsdoc
function durationToMs(duration) {
// relativeTo uses the real system timezone — fake-timers fakes time, not place.
// Calendar-unit durations (months, years) will resolve DST/length using the host tz.
const relativeTo = NativeTemporal.Instant.fromEpochMilliseconds(
clock.now,
).toZonedDateTimeISO(NativeTemporal.Now.timeZoneId());
return duration.total({ unit: "millisecond", relativeTo });
}
/**
* @param {number|string|TemporalDuration} tickValue
* @returns {number} milliseconds as a float
*/
function tickValueToMs(tickValue) {
if (typeof tickValue === "number") {
return tickValue;
}
if (
isPresent.Temporal &&
tickValue !== null &&
typeof tickValue === "object" &&
typeof (/** @type {TemporalDuration} */ (tickValue).total) ===
"function"
) {
return durationToMs(
/** @type {TemporalDuration} */ (tickValue),
);
}
return parseTime(/** @type {string} */ (tickValue));
}
/**
* @param {number|string|TemporalDuration} tickValue milliseconds, a string parseable by parseTime, or a Temporal.Duration
* @returns {ClockState} a mutable state object for the tick execution
*/
function createTickState(tickValue) {
const msFloat = tickValueToMs(tickValue);
const ms = Math.floor(msFloat);
const remainder = nanoRemainder(msFloat);
let nanosTotal = nanos + remainder;
let tickTo = clock.now + ms;
if (msFloat < 0) {
throw new TypeError("Negative ticks are not supported");
}
// adjust for positive overflow
if (nanosTotal >= 1e6) {
tickTo += 1;
nanosTotal -= 1e6;
}
return /** @type {ClockState} */ ({
msFloat: msFloat,
ms: ms,
nanosTotal: nanosTotal,
tickFrom: clock.now,
tickTo: tickTo,
previous: clock.now,
timer: null,
firstException: null,
oldNow: null,
});
}
/**
* @param {ClockState} state mutable tick state
* @param {number} oldNow the clock.now before some action
* @param {object} [options] compensation options
* @param {boolean} [options.includePrevious] whether to also update state.previous
*/
function applyClockChangeCompensation(state, oldNow, options) {
if (oldNow !== clock.now) {
const difference = clock.now - oldNow;
state.tickFrom += difference;
state.tickTo += difference;
if (options && options.includePrevious) {
state.previous += difference;
}
}
}
/**
* @param {ClockState} state mutable tick state
*/
function runInitialJobs(state) {
state.oldNow = clock.now;
runJobs(clock);
applyClockChangeCompensation(state, state.oldNow);
}
/**
* @param {ClockState} state mutable tick state
*/
function runPostLoopJobs(state) {
state.oldNow = clock.now;
runJobs(clock);
applyClockChangeCompensation(state, state.oldNow);
}
/**
* @param {ClockState} state mutable tick state
*/
function selectNextTimerInRange(state) {
state.timer = firstTimerInRange(
clock,
state.previous,
state.tickTo,
);
state.previous = state.tickFrom;
}
/**
* @param {ClockState} state mutable tick state
* @param {boolean} isAsync whether this is an async tick
* @param {FakeTimersFunction} nextPromiseTick callback for async promise settlement
* @param {FakeTimersFunction} compensationCheck callback for clock change compensation
* @returns {boolean} whether an early return was triggered (async mode)
*/
function runTimersInRange(
state,
isAsync,
nextPromiseTick,
compensationCheck,
) {
state.timer = firstTimerInRange(
clock,
state.tickFrom,
state.tickTo,
);
while (state.timer && state.tickFrom <= state.tickTo) {
if (hasTimer(clock, state.timer.id)) {
state.tickFrom = state.timer.callAt;
clock.now = state.timer.callAt;
state.oldNow = clock.now;
try {
runJobs(clock);
callTimer(clock, state.timer);
} catch (e) {
state.firstException = state.firstException || e;
}
if (isAsync) {
// finish up after native setImmediate callback to allow
// all native es6 promises to process their callbacks after
// each timer fires.
originalSetTimeout(nextPromiseTick);
return true;
}
compensationCheck();
}
selectNextTimerInRange(state);
}
return false;
}
/**
* @param {ClockState} state mutable tick state
* @param {boolean} isAsync whether this is an async tick
* @param {FakeTimersFunction} resolve promise resolve function
* @returns {number|undefined} the new clock.now or nothing for async
*/
function finalizeTick(state, isAsync, resolve) {
// corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo]
state.timer = firstTimerInRange(
clock,
state.tickFrom,
state.tickTo,
);
if (state.timer) {
try {
clock.tick(state.tickTo - clock.now); // do it all again - for the remainder of the requested range
} catch (e) {
state.firstException = state.firstException || e;
}
} else {
// no timers remaining in the requested range: move the clock all the way to the end
clock.now = state.tickTo;
// update nanos
nanos = state.nanosTotal;
}
if (state.firstException) {
throw state.firstException;
}
if (isAsync) {
resolve(clock.now);
} else {
return clock.now;
}
}
/**
* @param {number|string|TemporalDuration} tickValue milliseconds or a string parseable by parseTime
* @param {boolean} isAsync whether this is an async tick
* @param {FakeTimersFunction} [resolve] promise resolve function
* @param {FakeTimersFunction} [reject] promise reject function
* @returns {number|undefined} the new clock.now or nothing for async
*/
function doTick(tickValue, isAsync, resolve, reject) {
/** @type {ClockState} */
const state = createTickState(tickValue);
nanos = state.nanosTotal;
clock.duringTick = true;
runInitialJobs(state);
const compensationCheck = function () {
applyClockChangeCompensation(state, state.oldNow, {
includePrevious: true,
});
};
const nextPromiseTick =
isAsync &&
function () {
try {
compensationCheck();
selectNextTimerInRange(state);
doTickInner();
} catch (e) {
reject(e);
}
};
//eslint-disable-next-line jsdoc/require-jsdoc
function doTickInner() {
if (
runTimersInRange(
state,
isAsync,
nextPromiseTick,
compensationCheck,
)
) {
return;
}
runPostLoopJobs(state);
clock.duringTick = false;
return finalizeTick(state, isAsync, resolve);
}
return doTickInner();
}
/**
* @param {string|number|TemporalDuration} tickValue number of milliseconds, a human-readable value like "01:11:15", or a Temporal.Duration
* @returns {number} will return the new `now` value
*/
clock.tick = function tick(tickValue) {
return doTick(tickValue, false);
};
clock.next = function next() {
runJobs(clock);
const timer = firstTimer(clock);
if (!timer) {
return clock.now;
}
clock.duringTick = true;
try {
clock.now = timer.callAt;
callTimer(clock, timer);
runJobs(clock);
return clock.now;
} finally {
clock.duringTick = false;
}
};
/**
* @param {(resolve: (value: unknown) => void, reject: (reason?: unknown) => void) => void} callback function to run inside native setTimeout
* @returns {Promise}
*/
function runAsyncWithNativeTimeout(callback) {
return pauseAutoTickUntilFinished(
new _global.Promise(function (resolve, reject) {
originalSetTimeout(function () {
try {
callback(resolve, reject);
} catch (e) {
reject(e);
}
});
}),
);
}
clock.runAll = function runAll() {
runJobs(clock);
for (let i = 0; i < clock.loopLimit; i++) {
if (!clock.timers) {
resetIsNearInfiniteLimit(clock);
return clock.now;
}
const numTimers = clock.timerHeap.timers.length;
if (numTimers === 0) {
resetIsNearInfiniteLimit(clock);
return clock.now;
}
checkIsNearInfiniteLimit(clock, i);
clock.next();
}
const excessJob = firstTimer(clock);
throw getInfiniteLoopError(clock, excessJob);
};
clock.runToFrame = function runToFrame() {
return clock.tick(getTimeToNextFrame());
};
clock.runToLast = function runToLast() {
const timer = lastTimer(clock);
if (!timer) {
runJobs(clock);
return clock.now;
}
return clock.tick(timer.callAt - clock.now);
};
if (typeof _global.Promise !== "undefined") {
/**
* @param {string|number|TemporalDuration} tickValue number of milliseconds, a human-readable value like "01:11:15", or a Temporal.Duration
* @returns {Promise}
*/
clock.tickAsync = function tickAsync(tickValue) {
return runAsyncWithNativeTimeout(function (resolve, reject) {
doTick(tickValue, true, resolve, reject);
});
};
clock.nextAsync = function nextAsync() {
return runAsyncWithNativeTimeout(function (resolve, reject) {
const timer = firstTimer(clock);
if (!timer) {
resolve(clock.now);
return;
}
let err;
clock.duringTick = true;
clock.now = timer.callAt;
try {
callTimer(clock, timer);
} catch (e) {
err = e;
}
clock.duringTick = false;
originalSetTimeout(function () {
if (err) {
reject(err);
} else {
resolve(clock.now);
}
});
});
};
clock.runAllAsync = function runAllAsync() {
let i = 0;
/**
* @param {(value: unknown) => void} resolve promise resolve function
* @param {(reason?: unknown) => void} reject promise reject function
*/
function doRun(resolve, reject) {
try {
runJobs(clock);
let numTimers;
if (i < clock.loopLimit) {
if (!clock.timerHeap) {
resetIsNearInfiniteLimit(clock);
resolve(clock.now);
return;
}
numTimers = clock.timerHeap.timers.length;
if (numTimers === 0) {
resetIsNearInfiniteLimit(clock);
resolve(clock.now);
return;
}
checkIsNearInfiniteLimit(clock, i);
clock.next();
i++;
originalSetTimeout(function () {
doRun(resolve, reject);
});
return;
}
const excessJob = firstTimer(clock);
reject(getInfiniteLoopError(clock, excessJob));
} catch (e) {
reject(e);
}
}
return runAsyncWithNativeTimeout(function (resolve, reject) {
doRun(resolve, reject);
});
};
clock.runToLastAsync = function runToLastAsync() {
return runAsyncWithNativeTimeout(function (resolve) {
const timer = lastTimer(clock);
if (!timer) {
runJobs(clock);
resolve(clock.now);
return;
}
resolve(clock.tickAsync(timer.callAt - clock.now));
});
};
}
clock.reset = function reset() {
nanos = 0;
clock.timers = new Map();
clock.timerHeap = new TimerHeap();
clock.jobs = [];
clock.now = start;
};
clock.setSystemTime = function setSystemTime(systemTime) {
// determine time difference
const newNow = getEpoch(systemTime);
const difference = newNow - clock.now;
adjustedSystemTime[0] = adjustedSystemTime[0] + difference;
adjustedSystemTime[1] = adjustedSystemTime[1] + nanos;
// update 'system clock'
clock.now = newNow;
nanos = 0;
// update timers and intervals to keep them stable
forEachActiveTimer(clock, (timer) => {
timer.createdAt += difference;
timer.callAt += difference;
});
};
/**
* @param {string|number|TemporalDuration} tickValue number of milliseconds, a human-readable value like "01:11:15", or a Temporal.Duration
* @returns {number} the new `now` value
*/
clock.jump = function jump(tickValue) {
const msFloat = tickValueToMs(tickValue);
const ms = Math.floor(msFloat);
forEachActiveTimer(clock, (timer) => {
if (clock.now + ms > timer.callAt) {
timer.callAt = clock.now + ms;
}
});
// Rebuild heap as order might have changed
rebuildTimerHeap(clock);
clock.tick(ms);
return clock.now;
};
if (isPresent.performance) {
clock.performance = Object.create(null);
clock.performance.now = fakePerformanceNow;
}
if (isPresent.hrtime) {
clock.hrtime = hrtime;
}
/**
* @returns {Timer[]}
*/
clock.uninstall = function () {
uninstalled = true;
clock.setTickMode({ mode: "manual" });
if (clock.methods) {
const installedHrTime = "_hrtime";
const installedNextTick = "_nextTick";
let method, i, l;
for (i = 0, l = clock.methods.length; i < l; i++) {
method = clock.methods[i];
if (method === "hrtime" && _global.process) {
_global.process.hrtime = clock[installedHrTime];
} else if (method === "nextTick" && _global.process) {
_global.process.nextTick = clock[installedNextTick];
} else if (method === "performance") {
const originalPerfDescriptor =
Object.getOwnPropertyDescriptor(
clock,
`_${method}`,
);
if (
originalPerfDescriptor &&
originalPerfDescriptor.get &&
!originalPerfDescriptor.set
) {
Object.defineProperty(
_global,
method,
originalPerfDescriptor,
);
} else if (originalPerfDescriptor.configurable) {
_global[method] = clock[`_${method}`];
}
} else {
if (clock[method] && clock[method].hasOwnProperty) {
_global[method] = clock[`_${method}`];
} else {
try {
delete _global[method];
} catch {
/* eslint no-empty: "off" */
}
}
}
if (clock.timersModuleMethods !== undefined) {
for (
let j = 0;
j < clock.timersModuleMethods.length;
j++
) {
const entry = clock.timersModuleMethods[j];
timersModule[entry.methodName] = entry.original;
}
}
if (clock.timersPromisesModuleMethods !== undefined) {
for (
let j = 0;
j < clock.timersPromisesModuleMethods.length;
j++
) {
const entry = clock.timersPromisesModuleMethods[j];
timersPromisesModule[entry.methodName] =
entry.original;
}
}
}
// Prevent multiple executions which will completely remove these props
clock.methods = [];
}
if (clock.abortListenerMap) {
for (const [
listener,
signal,
] of clock.abortListenerMap.entries()) {
signal.removeEventListener("abort", listener);
clock.abortListenerMap.delete(listener);
}
}
// return pending timers, to enable checking what timers remained on uninstall
if (!clock.timerHeap) {
return [];
}
return clock.timerHeap.timers.slice();
};
return clock;
}
/**
* Starts the interval used to advance the clock automatically.
* @param {Clock} clock
* @param {number} delta
*/
function createIntervalTick(clock, delta) {
const intervalTick = doIntervalTick.bind(null, clock, delta);
const intervalId = originalSetInterval(intervalTick, delta);
clock.attachedInterval = intervalId;
}
/* eslint-disable complexity */
/**
* @param {Config=} [config] Optional config
* @returns {Clock}
*/
function install(config) {
if (
arguments.length > 1 ||
config instanceof Date ||
Array.isArray(config) ||
typeof config === "number"
) {
throw new TypeError(
`FakeTimers.install called with ${String(
config,
)} install requires an object parameter`,
);
}
if (_global.Date.isFake === true) {
// Timers are already faked; this is a problem.
// Make the user reset timers before continuing.
throw new TypeError(
"Can't install fake timers twice on the same global object.",
);
}
// eslint-disable-next-line no-param-reassign
config = typeof config !== "undefined" ? config : {};
config.shouldAdvanceTime = config.shouldAdvanceTime || false;
config.advanceTimeDelta = config.advanceTimeDelta || 20;
config.shouldClearNativeTimers =
config.shouldClearNativeTimers || false;
const hasToFake = Object.prototype.hasOwnProperty.call(
config,
"toFake",
);
const hasToNotFake = Object.prototype.hasOwnProperty.call(
config,
"toNotFake",
);
if (hasToFake && hasToNotFake) {
throw new TypeError(
"config.toFake and config.toNotFake cannot be used together",
);
}
if (config.target) {
throw new TypeError(
"config.target is no longer supported. Use `withGlobal(target)` instead.",
);
}
/**
* Handles a missing timer or API name during installation.
* @param {string} timer - the name of the missing timer or object
*/
function handleMissingTimer(timer) {
if (config.ignoreMissingTimers) {
return;
}
throw new ReferenceError(
`non-existent timers and/or objects cannot be faked: '${timer}'`,
);
}
let i, l;
const clock = createClock(config.now, config.loopLimit);
clock.shouldClearNativeTimers = config.shouldClearNativeTimers;
clock.abortListenerMap = new Map();
if (hasToFake) {
clock.methods = /** @type {FakeMethod[]} */ (config.toFake || []);
if (clock.methods.length === 0) {
clock.methods = /** @type {FakeMethod[]} */ (
Object.keys(timers)
);
}
} else if (hasToNotFake) {
const methodsToNotFake = /** @type {string[]} */ (
config.toNotFake || []
);
clock.methods = /** @type {FakeMethod[]} */ (
Object.keys(timers).filter(
(method) => !methodsToNotFake.includes(method),
)
);
} else {
clock.methods = /** @type {FakeMethod[]} */ (Object.keys(timers));
}
if (config.shouldAdvanceTime === true) {
clock.setTickMode({
mode: "interval",
delta: config.advanceTimeDelta,
});
}
if (clock.methods.includes("performance")) {
const proto = (() => {
if (hasPerformanceConstructorPrototype) {
return _global.performance.constructor.prototype;
}
if (hasPerformancePrototype) {
return _global.Performance.prototype;
}
})();
if (proto) {
Object.getOwnPropertyNames(proto).forEach(function (name) {
if (name !== "now") {
clock.performance[name] =
name.indexOf("getEntries") === 0
? NOOP_ARRAY
: NOOP;
}
});
// ensure `mark` returns a value that is valid
clock.performance.mark = (name) =>
new FakePerformanceEntry(name, "mark", 0, 0);
clock.performance.measure = (name) =>
new FakePerformanceEntry(name, "measure", 0, 100);
// `timeOrigin` should return the time of when the Window session started
// (or the Worker was installed)
clock.performance.timeOrigin = getEpoch(config.now);
} else if ((config.toFake || []).includes("performance")) {
handleMissingTimer("performance");
}
}
if (_global === globalObject && timersModule) {
clock.timersModuleMethods = [];
}
if (_global === globalObject && timersPromisesModule) {
clock.timersPromisesModuleMethods = [];
}
for (i = 0, l = clock.methods.length; i < l; i++) {
const nameOfMethodToReplace = clock.methods[i];
if (!isPresent[nameOfMethodToReplace]) {
handleMissingTimer(nameOfMethodToReplace);
// eslint-disable-next-line
continue;
}
if (nameOfMethodToReplace === "hrtime") {
if (
_global.process &&
typeof _global.process.hrtime === "function"
) {
hijackMethod(_global.process, nameOfMethodToReplace, clock);
}
} else if (nameOfMethodToReplace === "nextTick") {
if (
_global.process &&
typeof _global.process.nextTick === "function"
) {
hijackMethod(_global.process, nameOfMethodToReplace, clock);
}
} else {
hijackMethod(_global, nameOfMethodToReplace, clock);
}
if (
clock.timersModuleMethods !== undefined &&
timersModule[nameOfMethodToReplace]
) {
const original = timersModule[nameOfMethodToReplace];
clock.timersModuleMethods.push({
methodName: nameOfMethodToReplace,
original: original,
});
timersModule[nameOfMethodToReplace] =
_global[nameOfMethodToReplace];
}
if (clock.timersPromisesModuleMethods !== undefined) {
if (nameOfMethodToReplace === "setTimeout") {
clock.timersPromisesModuleMethods.push({
methodName: "setTimeout",
original: timersPromisesModule.setTimeout,
});
timersPromisesModule.setTimeout = (
delay,
value,
options = {},
) =>
new Promise((resolve, reject) => {
const abort = () => {
options.signal.removeEventListener(
"abort",
abort,
);
clock.abortListenerMap.delete(abort);
// This is safe, there is no code path that leads to this function
// being invoked before handle has been assigned.
// eslint-disable-next-line no-use-before-define
clock.clearTimeout(handle);
reject(options.signal.reason);
};
const handle = clock.setTimeout(() => {
if (options.signal) {
options.signal.removeEventListener(
"abort",
abort,
);
clock.abortListenerMap.delete(abort);
}
resolve(value);
}, delay);
if (options.signal) {
if (options.signal.aborted) {
abort();
} else {
options.signal.addEventListener(
"abort",
abort,
);
clock.abortListenerMap.set(
abort,
options.signal,
);
}
}
});
} else if (nameOfMethodToReplace === "setImmediate") {
clock.timersPromisesModuleMethods.push({
methodName: "setImmediate",
original: timersPromisesModule.setImmediate,
});
timersPromisesModule.setImmediate = (value, options = {}) =>
new Promise((resolve, reject) => {
const abort = () => {
options.signal.removeEventListener(
"abort",
abort,
);
clock.abortListenerMap.delete(abort);
// This is safe, there is no code path that leads to this function
// being invoked before handle has been assigned.
// eslint-disable-next-line no-use-before-define
clock.clearImmediate(handle);
reject(options.signal.reason);
};
const handle = clock.setImmediate(() => {
if (options.signal) {
options.signal.removeEventListener(
"abort",
abort,
);
clock.abortListenerMap.delete(abort);
}
resolve(value);
});
if (options.signal) {
if (options.signal.aborted) {
abort();
} else {
options.signal.addEventListener(
"abort",
abort,
);
clock.abortListenerMap.set(
abort,
options.signal,
);
}
}
});
} else if (nameOfMethodToReplace === "setInterval") {
clock.timersPromisesModuleMethods.push({
methodName: "setInterval",
original: timersPromisesModule.setInterval,
});
timersPromisesModule.setInterval = (
delay,
value,
options = {},
) => ({
[Symbol.asyncIterator]: () => {
const createResolvable = () => {
let resolve, reject;
const promise =
/** @type {Promise<unknown> & { resolve: (value: unknown) => void; reject: (reason: unknown) => void }} */ (
new Promise((res, rej) => {
resolve = res;
reject = rej;
})
);
promise.resolve = resolve;
promise.reject = reject;
return promise;
};
let done = false;
let hasThrown = false;
let returnCall;
let nextAvailable = 0;
const nextQueue = [];
const handle = clock.setInterval(() => {
if (nextQueue.length > 0) {
nextQueue.shift().resolve();
} else {
nextAvailable++;
}
}, delay);
const abort = () => {
options.signal.removeEventListener(
"abort",
abort,
);
clock.abortListenerMap.delete(abort);
clock.clearInterval(handle);
done = true;
for (const resolvable of nextQueue) {
resolvable.resolve();
}
};
if (options.signal) {
if (options.signal.aborted) {
done = true;
} else {
options.signal.addEventListener(
"abort",
abort,
);
clock.abortListenerMap.set(
abort,
options.signal,
);
}
}
return {
next: async () => {
if (options.signal?.aborted && !hasThrown) {
hasThrown = true;
throw options.signal.reason;
}
if (done) {
return { done: true, value: undefined };
}
if (nextAvailable > 0) {
nextAvailable--;
return { done: false, value: value };
}
const resolvable = createResolvable();
nextQueue.push(resolvable);
await resolvable;
if (returnCall && nextQueue.length === 0) {
returnCall.resolve();
}
if (options.signal?.aborted && !hasThrown) {
hasThrown = true;
throw options.signal.reason;
}
if (done) {
return { done: true, value: undefined };
}
return { done: false, value: value };
},
return: async () => {
if (done) {
return { done: true, value: undefined };
}
if (nextQueue.length > 0) {
returnCall = createResolvable();
await returnCall;
}
clock.clearInterval(handle);
done = true;
if (options.signal) {
options.signal.removeEventListener(
"abort",
abort,
);
clock.abortListenerMap.delete(abort);
}
return { done: true, value: undefined };
},
};
},
});
}
}
}
return clock;
}
/* eslint-enable complexity */
return {
timers: timers,
createClock: createClock,
install: install,
withGlobal: withGlobal,
};
}
/** @type {FakeTimers} */
const defaultImplementation = withGlobal(globalObject);
exports.timers = defaultImplementation.timers;
exports.createClock = defaultImplementation.createClock;
exports.install = defaultImplementation.install;
/** @type {WithGlobal} */
exports.withGlobal = withGlobal;
+257
View File
@@ -0,0 +1,257 @@
export type TickMode = "nextAsync" | "manual" | "interval";
export type NextAsyncTickMode = {
mode: "nextAsync";
};
export type ManualTickMode = {
mode: "manual";
};
export type IntervalTickMode = {
mode: "interval";
delta?: number;
};
export type TimerTickMode = IntervalTickMode | NextAsyncTickMode | ManualTickMode;
export type FakeTimersFunction = (...args: unknown[]) => unknown;
export type VoidVarArgsFunc = (...args: unknown[]) => void;
export type NextTick = (callback: VoidVarArgsFunc, ...args: unknown[]) => void;
export type SetImmediate = (callback: VoidVarArgsFunc, ...args: unknown[]) => NodeImmediate;
export type SetTimeout = (callback: VoidVarArgsFunc, delay?: number, ...args: unknown[]) => TimerId;
export type ClearTimeout = (id?: TimerId) => void;
export type SetInterval = (callback: VoidVarArgsFunc, delay?: number, ...args: unknown[]) => TimerId;
export type ClearInterval = (id?: TimerId) => void;
export type QueueMicrotask = (callback: VoidVarArgsFunc) => void;
export type TimeRemaining = () => number;
export type IdleDeadline = {
didTimeout: boolean;
timeRemaining: TimeRemaining;
};
export type RequestIdleCallbackCallback = (deadline: IdleDeadline) => any;
export type RequestIdleCallback = (callback: RequestIdleCallbackCallback, options?: {
timeout?: number;
}) => number;
export type AnimationFrameCallback = (timestamp: number) => any;
export type RequestAnimationFrame = (callback: AnimationFrameCallback) => TimerId;
export type CancelAnimationFrame = (id: TimerId) => void;
export type CancelIdleCallback = (id: TimerId) => void;
export type ClearImmediate = (id: NodeImmediate) => void;
export type CountTimers = () => number;
export type RunMicrotasks = () => void;
export type TemporalDuration = {
years: number;
months: number;
weeks: number;
days: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
microseconds: number;
nanoseconds: number;
total: (options: {
unit: string;
relativeTo?: unknown;
}) => number;
};
export type TemporalTimelike = {
epochMilliseconds: number;
};
export type Tick = (tickValue: number | string | TemporalDuration) => number;
export type TickAsync = (tickValue: number | string | TemporalDuration) => Promise<number>;
export type Next = () => number;
export type NextAsync = () => Promise<number>;
export type RunAll = () => number;
export type RunToFrame = () => number;
export type RunAllAsync = () => Promise<number>;
export type RunToLast = () => number;
export type RunToLastAsync = () => Promise<number>;
export type Reset = () => void;
export type SetSystemTime = (now?: number | Date | TemporalTimelike) => void;
export type Jump = (tickValue: number | string | TemporalDuration) => number;
export type Uninstall = () => void;
export type SetTickMode = (tickModeConfig: SetTickModeConfig) => void;
export type Hrtime = (prev?: Array<number>) => Array<number>;
export type WithGlobal = (_global: object) => FakeTimers;
export type FakeMethod = "setTimeout" | "clearTimeout" | "setImmediate" | "clearImmediate" | "setInterval" | "clearInterval" | "Date" | "nextTick" | "hrtime" | "requestAnimationFrame" | "cancelAnimationFrame" | "requestIdleCallback" | "cancelIdleCallback" | "performance" | "queueMicrotask" | "Intl" | "Temporal";
export type TimerId = number | NodeImmediate | Timer;
export type GlobalObject = Record<string, any> & {
setTimeout?: SetTimeout;
clearTimeout?: ClearTimeout;
setInterval?: SetInterval;
clearInterval?: ClearInterval;
setImmediate?: SetImmediate;
clearImmediate?: ClearImmediate;
queueMicrotask?: QueueMicrotask;
requestAnimationFrame?: RequestAnimationFrame;
cancelAnimationFrame?: CancelAnimationFrame;
requestIdleCallback?: RequestIdleCallback;
cancelIdleCallback?: CancelIdleCallback;
process?: any;
performance?: any;
Performance?: any;
Intl?: any;
Temporal?: any;
Promise?: typeof Promise;
Date: typeof Date & {
isFake?: boolean;
toSource?: () => string;
clock?: any;
};
};
export type TimerHeap = {
timers: Timer[];
peek: () => Timer | undefined;
push: (timer: Timer) => void;
pop: () => Timer | undefined;
remove: (timer: Timer) => void;
};
export type ClockTickMode = {
mode: TickMode;
counter: number;
delta?: number;
};
export type SetTickModeConfig = {
mode: TickMode;
delta?: number;
};
export type IntlWithClock = Record<string, any> & {
clock: Clock;
};
export type PerformanceLike = Record<string, any> & {
now: () => number;
};
export type Timers = {
setTimeout: SetTimeout;
clearTimeout: ClearTimeout;
setInterval: SetInterval;
clearInterval: ClearInterval;
Date: typeof Date;
Intl?: typeof Intl;
Temporal?: any;
setImmediate?: SetImmediate;
clearImmediate?: ClearImmediate;
hrtime?: Hrtime;
nextTick?: NextTick;
performance?: PerformanceLike;
requestAnimationFrame?: RequestAnimationFrame;
queueMicrotask?: QueueMicrotask;
cancelAnimationFrame?: CancelAnimationFrame;
requestIdleCallback?: RequestIdleCallback;
cancelIdleCallback?: CancelIdleCallback;
};
export type ClockState = {
tickFrom: number;
tickTo: number;
previous?: number;
oldNow?: number | null;
timer?: Timer;
firstException?: unknown;
nanosTotal?: number;
msFloat?: number;
ms?: number;
};
export type TimerInitialProps = {
func: VoidVarArgsFunc;
args?: unknown[];
type?: 'Timeout' | 'Interval' | 'Immediate' | 'AnimationFrame' | 'IdleCallback';
delay?: number;
callAt?: number;
createdAt?: number;
immediate?: boolean;
id?: number;
error?: Error;
interval?: number;
animation?: boolean;
requestIdleCallback?: boolean;
order?: number;
heapIndex?: number;
};
export type CreateClockCallback = (start?: number | Date | TemporalTimelike, loopLimit?: number) => Clock;
export type InstallCallback = (config?: Config) => Clock;
export type FakeTimers = {
timers: Timers;
createClock: CreateClockCallback;
install: InstallCallback;
withGlobal: WithGlobal;
};
export type Clock = {
now: number;
Date: typeof Date & {
clock?: Clock;
isFake?: boolean;
toSource?: () => string;
};
loopLimit: number;
requestIdleCallback: RequestIdleCallback;
cancelIdleCallback: CancelIdleCallback;
setTimeout: SetTimeout;
clearTimeout: ClearTimeout;
nextTick: NextTick;
queueMicrotask: QueueMicrotask;
setInterval: SetInterval;
clearInterval: ClearInterval;
setImmediate: SetImmediate;
clearImmediate: ClearImmediate;
countTimers: CountTimers;
requestAnimationFrame: RequestAnimationFrame;
cancelAnimationFrame: CancelAnimationFrame;
runMicrotasks: RunMicrotasks;
tick: Tick;
tickAsync: TickAsync;
next: Next;
nextAsync: NextAsync;
runAll: RunAll;
runToFrame: RunToFrame;
runAllAsync: RunAllAsync;
runToLast: RunToLast;
runToLastAsync: RunToLastAsync;
reset: Reset;
setSystemTime: SetSystemTime;
jump: Jump;
performance: any;
hrtime: Hrtime;
uninstall: Uninstall;
methods: string[];
shouldClearNativeTimers?: boolean;
timersModuleMethods: {
methodName: string;
original: unknown;
}[] | undefined;
timersPromisesModuleMethods: {
methodName: string;
original: unknown;
}[] | undefined;
abortListenerMap: Map<VoidVarArgsFunc, AbortSignal>;
setTickMode: SetTickMode;
timers?: Map<number, Timer>;
timerHeap?: TimerHeap;
duringTick?: boolean;
isNearInfiniteLimit: boolean;
attachedInterval?: TimerId;
tickMode?: ClockTickMode;
jobs?: Timer[];
Intl?: IntlWithClock;
Temporal?: any;
};
export type Config = {
now?: number | Date | TemporalTimelike;
toFake?: FakeMethod[];
toNotFake?: FakeMethod[];
loopLimit?: number;
shouldAdvanceTime?: boolean;
advanceTimeDelta?: number;
shouldClearNativeTimers?: boolean;
ignoreMissingTimers?: boolean;
target?: GlobalObject;
};
export type Timer = TimerInitialProps;
export type NodeImmediateHasRef = () => boolean;
export type NodeImmediateRef = () => NodeImmediate;
export type NodeImmediateUnref = () => NodeImmediate;
export type NodeImmediate = {
hasRef: NodeImmediateHasRef;
ref: NodeImmediateRef;
unref: NodeImmediateUnref;
};
export declare var timers: Timers;
export declare var createClock: CreateClockCallback;
export declare var install: InstallCallback;
export declare var withGlobal: WithGlobal;