Skip to content
This repository was archived by the owner on Apr 18, 2022. It is now read-only.

Commit 46bbfda

Browse files
authored
Add mockSession, mockSessionAsync, and restore (#35)
1 parent 467aeb9 commit 46bbfda

File tree

3 files changed

+246
-12
lines changed

3 files changed

+246
-12
lines changed

mock.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,94 @@ function isSpy<Self, Args extends unknown[], Return>(
107107
Array.isArray(spy.calls);
108108
}
109109

110+
// deno-lint-ignore no-explicit-any
111+
const sessions: Set<Spy<any, any[], any>>[] = [];
112+
// deno-lint-ignore no-explicit-any
113+
function getSession(): Set<Spy<any, any[], any>> {
114+
if (sessions.length === 0) sessions.push(new Set());
115+
return sessions[sessions.length - 1];
116+
}
117+
// deno-lint-ignore no-explicit-any
118+
function registerMock(spy: Spy<any, any[], any>): void {
119+
const session = getSession();
120+
session.add(spy);
121+
}
122+
// deno-lint-ignore no-explicit-any
123+
function unregisterMock(spy: Spy<any, any[], any>): void {
124+
const session = getSession();
125+
session.delete(spy);
126+
}
127+
128+
/**
129+
* Creates a session that tracks all mocks created before it's restored.
130+
* If a callback is provided, it restores all mocks created within it.
131+
*/
132+
export function mockSession(): number;
133+
export function mockSession<
134+
Self,
135+
Args extends unknown[],
136+
Return,
137+
>(
138+
func: (this: Self, ...args: Args) => Return,
139+
): (this: Self, ...args: Args) => Return;
140+
export function mockSession<
141+
Self,
142+
Args extends unknown[],
143+
Return,
144+
>(
145+
func?: (this: Self, ...args: Args) => Return,
146+
): number | ((this: Self, ...args: Args) => Return) {
147+
if (func) {
148+
return function (this: Self, ...args: Args): Return {
149+
const id = sessions.length;
150+
sessions.push(new Set());
151+
try {
152+
return func.apply(this, args);
153+
} finally {
154+
restore(id);
155+
}
156+
};
157+
} else {
158+
sessions.push(new Set());
159+
return sessions.length - 1;
160+
}
161+
}
162+
163+
/** Creates an async session that tracks all mocks created before the promise resolves. */
164+
export function mockSessionAsync<
165+
Self,
166+
Args extends unknown[],
167+
Return,
168+
>(
169+
func: (this: Self, ...args: Args) => Promise<Return>,
170+
): (this: Self, ...args: Args) => Promise<Return> {
171+
return async function (this: Self, ...args: Args): Promise<Return> {
172+
const id = sessions.length;
173+
sessions.push(new Set());
174+
try {
175+
return await func.apply(this, args);
176+
} finally {
177+
restore(id);
178+
}
179+
};
180+
}
181+
182+
/**
183+
* Restores all mocks registered in the current session that have not already been restored.
184+
* If an id is provided, it will restore all mocks registered in the session associed with that id that have not already been restored.
185+
*/
186+
export function restore(id?: number): void {
187+
id ??= (sessions.length || 1) - 1;
188+
while (id < sessions.length) {
189+
const session = sessions.pop();
190+
if (session) {
191+
for (const value of session) {
192+
value.restore();
193+
}
194+
}
195+
}
196+
}
197+
110198
/** Wraps an instance method with a Spy. */
111199
function methodSpy<
112200
Self,
@@ -169,6 +257,7 @@ function methodSpy<
169257
delete self[property];
170258
}
171259
restored = true;
260+
unregisterMock(spy);
172261
},
173262
},
174263
});
@@ -180,6 +269,7 @@ function methodSpy<
180269
value: spy,
181270
});
182271

272+
registerMock(spy);
183273
return spy;
184274
}
185275

@@ -207,13 +297,14 @@ export function spy<
207297
funcOrSelf?: ((this: Self, ...args: Args) => Return) | Self,
208298
property?: keyof Self,
209299
): Spy<Self, Args, Return> {
210-
return typeof property !== "undefined"
300+
const spy = typeof property !== "undefined"
211301
? methodSpy<Self, Args, Return>(funcOrSelf as Self, property)
212302
: typeof funcOrSelf === "function"
213303
? functionSpy<Self, Args, Return>(
214304
funcOrSelf as (this: Self, ...args: Args) => Return,
215305
)
216306
: functionSpy<Self, Args, Return>();
307+
return spy;
217308
}
218309

219310
// Create Stub interface that extends Spy to have fake
@@ -312,6 +403,7 @@ export function stub<
312403
delete self[property];
313404
}
314405
restored = true;
406+
unregisterMock(stub);
315407
},
316408
},
317409
});
@@ -323,5 +415,6 @@ export function stub<
323415
value: stub,
324416
});
325417

418+
registerMock(stub);
326419
return stub;
327420
}

mock_test.ts

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { assertSpyCall, assertSpyCalls } from "./asserts.ts";
22
import { assertEquals, assertNotEquals, assertThrows } from "./deps.ts";
3-
import { MockError, spy, stub } from "./mock.ts";
3+
import {
4+
MockError,
5+
mockSession,
6+
mockSessionAsync,
7+
restore,
8+
Spy,
9+
spy,
10+
stub,
11+
} from "./mock.ts";
412
import { Point, stringifyPoint } from "./test_shared.ts";
513

614
Deno.test("spy default", () => {
@@ -376,3 +384,129 @@ Deno.test("stub function", () => {
376384
);
377385
assertEquals(func.restored, true);
378386
});
387+
388+
Deno.test("mockSession and mockSessionAsync", async () => {
389+
const points = Array(6).fill(undefined).map(() => new Point(2, 3));
390+
let actions: Spy<Point, unknown[], unknown>[] = [];
391+
function assertRestored(expected: boolean[]): void {
392+
assertEquals(actions.map((action) => action.restored), expected);
393+
}
394+
await mockSessionAsync(async () => {
395+
actions.push(spy(points[0], "action"));
396+
assertRestored([false]);
397+
await mockSessionAsync(async () => {
398+
await Promise.resolve();
399+
actions.push(spy(points[1], "action"));
400+
assertRestored([false, false]);
401+
mockSession(() => {
402+
actions.push(spy(points[2], "action"));
403+
actions.push(spy(points[3], "action"));
404+
assertRestored([false, false, false, false]);
405+
})();
406+
actions.push(spy(points[4], "action"));
407+
assertRestored([false, false, true, true, false]);
408+
})();
409+
actions.push(spy(points[5], "action"));
410+
assertRestored([false, true, true, true, true, false]);
411+
})();
412+
assertRestored(Array(6).fill(true));
413+
restore();
414+
assertRestored(Array(6).fill(true));
415+
416+
actions = [];
417+
mockSession(() => {
418+
actions = points.map((point) => spy(point, "action"));
419+
assertRestored(Array(6).fill(false));
420+
})();
421+
assertRestored(Array(6).fill(true));
422+
restore();
423+
assertRestored(Array(6).fill(true));
424+
});
425+
426+
Deno.test("mockSession and restore current session", () => {
427+
const points = Array(6).fill(undefined).map(() => new Point(2, 3));
428+
let actions: Spy<Point, unknown[], unknown>[];
429+
function assertRestored(expected: boolean[]): void {
430+
assertEquals(actions.map((action) => action.restored), expected);
431+
}
432+
try {
433+
actions = points.map((point) => spy(point, "action"));
434+
435+
assertRestored(Array(6).fill(false));
436+
restore();
437+
assertRestored(Array(6).fill(true));
438+
restore();
439+
assertRestored(Array(6).fill(true));
440+
441+
actions = [];
442+
try {
443+
actions.push(spy(points[0], "action"));
444+
try {
445+
mockSession();
446+
actions.push(spy(points[1], "action"));
447+
try {
448+
mockSession();
449+
actions.push(spy(points[2], "action"));
450+
actions.push(spy(points[3], "action"));
451+
} finally {
452+
assertRestored([false, false, false, false]);
453+
restore();
454+
}
455+
actions.push(spy(points[4], "action"));
456+
} finally {
457+
assertRestored([false, false, true, true, false]);
458+
restore();
459+
}
460+
actions.push(spy(points[5], "action"));
461+
} finally {
462+
assertRestored([false, true, true, true, true, false]);
463+
restore();
464+
}
465+
assertRestored(Array(6).fill(true));
466+
restore();
467+
assertRestored(Array(6).fill(true));
468+
469+
actions = points.map((point) => spy(point, "action"));
470+
assertRestored(Array(6).fill(false));
471+
restore();
472+
assertRestored(Array(6).fill(true));
473+
restore();
474+
assertRestored(Array(6).fill(true));
475+
} finally {
476+
restore();
477+
}
478+
});
479+
480+
Deno.test("mockSession and restore multiple sessions", () => {
481+
const points = Array(6).fill(undefined).map(() => new Point(2, 3));
482+
let actions: Spy<Point, unknown[], unknown>[];
483+
function assertRestored(expected: boolean[]): void {
484+
assertEquals(actions.map((action) => action.restored), expected);
485+
}
486+
try {
487+
actions = [];
488+
try {
489+
actions.push(spy(points[0], "action"));
490+
const id = mockSession();
491+
try {
492+
actions.push(spy(points[1], "action"));
493+
actions.push(spy(points[2], "action"));
494+
mockSession();
495+
actions.push(spy(points[3], "action"));
496+
actions.push(spy(points[4], "action"));
497+
} finally {
498+
assertRestored([false, false, false, false, false]);
499+
restore(id);
500+
}
501+
actions.push(spy(points[5], "action"));
502+
} finally {
503+
assertRestored([false, true, true, true, true, false]);
504+
restore();
505+
}
506+
assertRestored(Array(6).fill(true));
507+
restore();
508+
assertRestored(Array(6).fill(true));
509+
} finally {
510+
restore();
511+
}
512+
});

mod.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
/** This module is browser compatible. */
22

3-
export { MockError, spy, stub } from "./mock.ts";
4-
export type { Spy, SpyCall, Stub } from "./mock.ts";
5-
63
export {
7-
FakeDate,
8-
FakeTime,
9-
NativeDate,
10-
NativeTime,
11-
TimeError,
12-
} from "./time.ts";
13-
export type { FakeDateConstructor, NativeDateConstructor } from "./time.ts";
4+
MockError,
5+
mockSession,
6+
mockSessionAsync,
7+
restore,
8+
spy,
9+
stub,
10+
} from "./mock.ts";
11+
export type { Spy, SpyCall, Stub } from "./mock.ts";
1412

1513
export {
1614
resolvesNext,
@@ -28,3 +26,12 @@ export {
2826
assertSpyCalls,
2927
} from "./asserts.ts";
3028
export type { ExpectedSpyCall } from "./asserts.ts";
29+
30+
export {
31+
FakeDate,
32+
FakeTime,
33+
NativeDate,
34+
NativeTime,
35+
TimeError,
36+
} from "./time.ts";
37+
export type { FakeDateConstructor, NativeDateConstructor } from "./time.ts";

0 commit comments

Comments
 (0)