Skip to content
Merged
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
41 changes: 41 additions & 0 deletions src/__testUtils__/__tests__/spyOn-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ describe('spyOn', () => {
expect(obj.addToBase(5)).to.equal(15);
expect(obj.addToBase.callCount).to.equal(1);
});

it('passes an empty stack to the matcher when stack traces are unavailable', () => {
const originalStackTraceLimit = Error.stackTraceLimit;
(Error as { stackTraceLimit: number | undefined }).stackTraceLimit =
undefined;

try {
const spy = spyOn(() => 42, {
stackMatcher: (stack) => stack === '',
});

expect(spy()).to.equal(42);
expect(spy.callCount).to.equal(1);
} finally {
Error.stackTraceLimit = originalStackTraceLimit;
}
});
});

describe('spyOnMethod', () => {
Expand Down Expand Up @@ -58,6 +75,30 @@ describe('spyOnMethod', () => {
expect(spy.callCount).to.equal(1);
});

it('can count only method invocations matching the call stack', () => {
const calculator = {
add(a: number, b: number) {
return a + b;
},
};

const spy = spyOnMethod(calculator, 'add', {
stackMatcher: (stack) => stack.includes('callTrackedMethod'),
});

expect(callTrackedMethod()).to.equal(5);
expect(callUntrackedMethod()).to.equal(9);
expect(spy.callCount).to.equal(1);

function callTrackedMethod(): number {
return calculator.add(2, 3);
}

function callUntrackedMethod(): number {
return calculator.add(4, 5);
}
});

it('throws when target property is not a function', () => {
const obj: { maybeMethod?: (value: string) => string } = {};

Expand Down
16 changes: 13 additions & 3 deletions src/__testUtils__/spyOn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@ export interface MethodSpy {
restore: () => void;
}

export interface SpyOptions {
readonly stackMatcher?: (stack: string) => boolean;
}

export type SpyFn<T extends AnyFn> = T & MethodSpy;

export function spyOn<T extends AnyFn>(fn: T): SpyFn<T> {
export function spyOn<T extends AnyFn>(fn: T, options?: SpyOptions): SpyFn<T> {
let callCount = 0;

const spy = function (this: unknown, ...args: Parameters<T>): ReturnType<T> {
callCount += 1;
if (
options?.stackMatcher === undefined ||
options.stackMatcher(new Error().stack ?? '')
) {
callCount += 1;
}
return fn.apply(this, args) as ReturnType<T>;
};

Expand All @@ -28,6 +37,7 @@ export function spyOn<T extends AnyFn>(fn: T): SpyFn<T> {
export function spyOnMethod<T extends object>(
target: T,
key: keyof T,
options?: SpyOptions,
): MethodSpy {
const original = target[key];
const wasOwnProperty = Object.hasOwn(target, key);
Expand All @@ -38,7 +48,7 @@ export function spyOnMethod<T extends object>(
);
}

const spy = spyOn(original as AnyFn);
const spy = spyOn(original as AnyFn, options);
target[key] = spy as T[keyof T];

const methodSpy: MethodSpy = {
Expand Down
Loading
Loading