Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 44 additions & 14 deletions src/sinon/fake.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,76 +17,105 @@ const { slice } = prototypes.array;
* When an `f` argument is supplied, this implementation will be used.
*
* @param {SinonFunction|undefined} [f]
* @param {object} [context] The sinon context for callId tracking
* @returns {SinonFunction}
* @namespace
*/
function fake(f) {
if (arguments.length > 0 && typeof f !== "function") {
function fakeImpl(f, context) {
if (typeof f !== "undefined" && typeof f !== "function") {
throw new TypeError("Expected f argument to be a Function");
}

return wrapFunc(f);
return wrapFunc(f, context);
}

/**
* Returns a `fake` that records all calls, arguments and return values.
*
* When an `f` argument is supplied, this implementation will be used.
*
* @param {SinonFunction|undefined} [f]
* @returns {SinonFunction}
* @namespace
*/
function fake(f) {
return fakeImpl(f, undefined);
}

/**
* Creates a fake with a specific context (for sandbox use).
*
* @param {object} context The sinon context for callId tracking
* @param {SinonFunction|undefined} [f]
* @returns {SinonFunction}
*/
fake.withContext = function (context, f) {
return fakeImpl(f, context);
};

/**
* Creates a `fake` that returns the provided `value`, as well as recording all
* calls, arguments and return values.
*
* @memberof fake
* @param {unknown} value
* @param {object} [context] The sinon context for callId tracking
* @returns {SinonFunction}
*/
fake.returns = function returns(value) {
fake.returns = function returns(value, context) {
function f() {
return value;
}

return wrapFunc(f);
return wrapFunc(f, context);
};

/**
* Creates a `fake` that throws an Error.
*
* @memberof fake
* @param {unknown|Error} value
* @param {object} [context] The sinon context for callId tracking
* @returns {SinonFunction}
*/
fake.throws = function throws(value) {
fake.throws = function throws(value, context) {
function f() {
throw getError(value);
}

return wrapFunc(f);
return wrapFunc(f, context);
};

/**
* Creates a `fake` that returns a promise that resolves to the passed `value`
*
* @memberof fake
* @param {unknown} value
* @param {object} [context] The sinon context for callId tracking
* @returns {SinonFunction}
*/
fake.resolves = function resolves(value) {
fake.resolves = function resolves(value, context) {
function f() {
return Promise.resolve(value);
}

return wrapFunc(f);
return wrapFunc(f, context);
};

/**
* Creates a `fake` that returns a promise that rejects to the passed `value`
*
* @memberof fake
* @param {unknown} value
* @param {object} [context] The sinon context for callId tracking
* @returns {SinonFunction}
*/
fake.rejects = function rejects(value) {
fake.rejects = function rejects(value, context) {
function f() {
return Promise.reject(getError(value));
}

return wrapFunc(f);
return wrapFunc(f, context);
};

/**
Expand Down Expand Up @@ -138,10 +167,11 @@ let uuid = 0;
* Creates a proxy (sinon concept) from the passed function.
*
* @private
* @param {SinonFunction} f
* @param {SinonFunction} f
* @param {object} [context] The sinon context for callId tracking
* @returns {SinonFunction}
*/
function wrapFunc(f) {
function wrapFunc(f, context) {
const fakeInstance = function () {
let firstArg, lastArg;

Expand All @@ -160,7 +190,7 @@ function wrapFunc(f) {

return f && f.apply(this, arguments);
};
const proxy = createProxy(fakeInstance, f || fakeInstance);
const proxy = createProxy(fakeInstance, f || fakeInstance, context);

Object.defineProperty(proxy, "name", {
value: "fake",
Expand Down
10 changes: 7 additions & 3 deletions src/sinon/proxy-invoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ const { push, forEach, concat } = prototypes.array;
const ErrorConstructor = Error.prototype.constructor;
const { bind } = Function.prototype;

let callId = 0;
const maxSafeInteger = Number.MAX_SAFE_INTEGER;

// Default context for backward compatibility when used outside a sandbox
const defaultContext = { callId: 0 };

/**
* @callback SinonFunction
* @param {...unknown} args
Expand All @@ -26,8 +28,10 @@ const maxSafeInteger = Number.MAX_SAFE_INTEGER;
*/
export default function invoke(func, thisValue, args) {
const matchings = this.matchingFakes(args);
const currentCallId = callId;
callId = callId >= maxSafeInteger ? 0 : callId + 1;
// Use the proxy's context if available, otherwise fall back to the default
const ctx = this.sinonContext || defaultContext;
const currentCallId = ctx.callId;
ctx.callId = ctx.callId >= maxSafeInteger ? 0 : ctx.callId + 1;
let exception, returnValue;

proxyCallUtil.incrementCallCount(this);
Expand Down
15 changes: 14 additions & 1 deletion src/sinon/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ delegateToCalls(proxyApi, "alwaysReturned", false, "returned");
delegateToCalls(proxyApi, "calledWithNew", true);
delegateToCalls(proxyApi, "alwaysCalledWithNew", false, "calledWithNew");

/**
* Wraps a function with a proxy that preserves arity.
*
* @param {SinonFunction} func The function to wrap
* @param {SinonFunction} originalFunc The original function (for arity)
* @returns {SinonFunction} The wrapped proxy function
*/
function wrapFunction(func, originalFunc) {
const arity = originalFunc.length;
let p;
Expand Down Expand Up @@ -376,9 +383,10 @@ function wrapFunction(func, originalFunc) {
*
* @param {SinonFunction} func The original function
* @param {SinonFunction} originalFunc The original function (for arity and name)
* @param {object} [context] The sinon context for callId tracking (typically a sandbox)
* @returns {SinonFunction} The proxy function
*/
export default function createProxy(func, originalFunc) {
export default function createProxy(func, originalFunc, context) {
const proxy = wrapFunction(func, originalFunc);

// Inherit function properties:
Expand All @@ -388,5 +396,10 @@ export default function createProxy(func, originalFunc) {

extend.nonEnum(proxy, proxyApi);

// Store context for use in invoke (for parallel test support)
if (context) {
extend.nonEnum(proxy, { sinonContext: context });
}

return proxy;
}
31 changes: 25 additions & 6 deletions src/sinon/sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export default function Sandbox(opts = {}) {
let loggedLeakWarning = false;
sandbox.leakThreshold = DEFAULT_LEAK_THRESHOLD;

// Context object for this sandbox, used to isolate callId between parallel tests
const sandboxContext = { callId: 0 };

function addToCollection(object) {
if (
push(collection, object) > sandbox.leakThreshold &&
Expand Down Expand Up @@ -196,7 +199,12 @@ export default function Sandbox(opts = {}) {
}

sandbox.spy = function () {
const createdSpy = sinonSpy.apply(sinonSpy, arguments);
// Use withContext to pass sandbox context for isolated callId tracking
const args = arrayProto.concat.call(
[sandboxContext],
arrayProto.slice.call(arguments, 0),
);
const createdSpy = sinonSpy.withContext.apply(sinonSpy, args);
const result = commonPostInitSetup(
arguments,
createdSpy,
Expand All @@ -217,7 +225,12 @@ export default function Sandbox(opts = {}) {
extend(sandbox.spy, sinonSpy);

sandbox.stub = function () {
const createdStub = sinonStub.apply(sinonStub, arguments);
// Use withContext to pass sandbox context for isolated callId tracking
const args = arrayProto.concat.call(
[sandboxContext],
arrayProto.slice.call(arguments, 0),
);
const createdStub = sinonStub.withContext.apply(sinonStub, args);
const result = commonPostInitSetup(
arguments,
createdStub,
Expand Down Expand Up @@ -535,7 +548,12 @@ export default function Sandbox(opts = {}) {
};

sandbox.fake = function fake() {
const createdFake = sinonFake.apply(sinonFake, arguments);
// Use withContext to pass sandbox context for isolated callId tracking
const args = arrayProto.concat.call(
[sandboxContext],
arrayProto.slice.call(arguments, 0),
);
const createdFake = sinonFake.withContext.apply(sinonFake, args);
const result = commonPostInitSetup(arguments, createdFake, false);
addToCollection(result);
return result;
Expand All @@ -559,10 +577,11 @@ export default function Sandbox(opts = {}) {
});

function addFakeBehaviorToCollection(method) {
const original = sandbox.fake[method];

sandbox.fake[method] = function () {
const result = original.apply(sinonFake, arguments);
// Add sandboxContext as the second argument for context-aware fakes
const args = arrayProto.slice.call(arguments, 0);
args.push(sandboxContext);
const result = sinonFake[method].apply(sinonFake, args);
addToCollection(result);
return result;
};
Expand Down
65 changes: 56 additions & 9 deletions src/sinon/spy.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,14 @@ delegateToCalls(
},
);

function createSpy(func) {
/**
* Creates a spy from a function.
*
* @param {SinonFunction} func The function to spy on
* @param {object} [context] The sinon context for callId tracking
* @returns {SinonFunction} The spy
*/
function createSpy(func, context) {
let name;
let funk = func;

Expand All @@ -141,14 +148,19 @@ function createSpy(func) {
name = functionName(funk);
}

const proxy = createProxy(funk, funk);
const proxy = createProxy(funk, funk, context);

// Create a bound instantiateFake that preserves context
const instantiateFake = function (f) {
return createSpy(f, context);
};

// Inherit spy API:
extend.nonEnum(proxy, spyApi);
extend.nonEnum(proxy, {
displayName: name || "spy",
fakes: [],
instantiateFake: createSpy,
instantiateFake: instantiateFake,
id: `spy#${uuid++}`,
});
return proxy;
Expand All @@ -160,39 +172,74 @@ function createSpy(func) {
* @param {object|SinonFunction} [object] The object or function to spy on
* @param {string} [property] The property name to spy on
* @param {Array} [types] Types of accessor to spy on (get, set)
* @param {object} [context] The sinon context for callId tracking
* @returns {SinonFunction|object} The spy or an object with spied accessors
*/
export default function spy(object, property, types) {
function spyImpl(object, property, types, context) {
if (isEsModule(object)) {
throw new TypeError("ES Modules cannot be spied");
}

if (!property && typeof object === "function") {
return createSpy(object);
return createSpy(object, context);
}

if (!property && typeof object === "object") {
return walkObject(spy, object);
return walkObject(
function (obj, prop, propTypes) {
return spyImpl(obj, prop, propTypes, context);
},
object,
);
}

if (!object && !property) {
return createSpy(function () {
return;
});
}, context);
}

if (!types) {
return wrapMethod(object, property, createSpy(object[property]));
return wrapMethod(
object,
property,
createSpy(object[property], context),
);
}

const descriptor = {};
const methodDesc = getPropertyDescriptor(object, property);

forEach(types, function (type) {
descriptor[type] = createSpy(methodDesc[type]);
descriptor[type] = createSpy(methodDesc[type], context);
});

return wrapMethod(object, property, descriptor);
}

/**
* Creates a spy (public API, backward compatible).
*
* @param {object|SinonFunction} [object] The object or function to spy on
* @param {string} [property] The property name to spy on
* @param {Array} [types] Types of accessor to spy on (get, set)
* @returns {SinonFunction|object} The spy or an object with spied accessors
*/
export default function spy(object, property, types) {
return spyImpl(object, property, types, undefined);
}

/**
* Creates a spy with a specific context (for sandbox use).
*
* @param {object} context The sinon context for callId tracking
* @param {object|SinonFunction} [object] The object or function to spy on
* @param {string} [property] The property name to spy on
* @param {Array} [types] Types of accessor to spy on (get, set)
* @returns {SinonFunction|object} The spy or an object with spied accessors
*/
spy.withContext = function (context, object, property, types) {
return spyImpl(object, property, types, context);
};

extend(spy, spyApi);
Loading
Loading