Skip to content

Commit 5b26a8d

Browse files
feat: add replayMock
1 parent b1afe12 commit 5b26a8d

File tree

10 files changed

+417
-15
lines changed

10 files changed

+417
-15
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
- [Use the mock to do literally anything](#use-the-mock-to-do-literally-anything)
1313
- [Override the default proxy behavior with custom values](#override-the-default-proxy-behavior-with-custom-values)
1414
- [Inspect what was done to the mock](#inspect-what-was-done-to-the-mock)
15+
- [Store and replay all operations](#store-and-replay-all-operations)
1516
- [API Documentation](#api-documentation)
1617
- [`recursiveProxyMock([overrides]) => Proxy`](#recursiveproxymockoverrides--proxy)
1718
- [TypeScript Support](#typescript-support)
1819
- [`ProxySymbol`](#proxysymbol)
1920
- [`hasPathBeenVisited(proxy, path) => boolean`](#haspathbeenvisitedproxy-path--boolean)
2021
- [`hasPathBeenCalledWith(proxy, path, args) => boolean`](#haspathbeencalledwithproxy-path-args--boolean)
2122
- [`getVisitedPathData(proxy, path) => ProxyData[] | null`](#getvisitedpathdataproxy-path--proxydata--null)
23+
- [`replayMock(proxy, target)`](#replaymockproxy-target)
2224
- [ProxyData](#proxydata)
2325
- [`listAllProxyOperations(proxy) => ProxyData[]`](#listallproxyoperationsproxy--proxydata)
2426
- [ProxyPath](#proxypath)
@@ -146,6 +148,21 @@ if (hasPathBeenCalledWith(mock, ["a", "b", "c", ProxySymbol.APPLY], ["hi", true,
146148
}
147149
```
148150

151+
### Store and replay all operations
152+
153+
```ts
154+
import { recursiveProxyMock, replayProxy } from "recursive-proxy-mock";
155+
156+
const mock = recursiveProxyMock();
157+
158+
// Queue up operations on a mock
159+
mock.metrics.pageLoad(Date.now());
160+
mock.users.addUser("name");
161+
162+
// Sometime later once the module is loaded
163+
replayProxy(mock, apiModule);
164+
```
165+
149166
## API Documentation
150167

151168
### `recursiveProxyMock([overrides]) => Proxy`
@@ -209,6 +226,15 @@ Function to get details about every time a path was visited. Useful in conjuncti
209226

210227
- `proxy` - the root proxy object that was returned from `recursiveProxyMock`
211228
- `path` - see the [ProxyPath section](#proxypath) for more details.
229+
230+
### `replayMock(proxy, target)`
231+
232+
Replay every operation performed on a proxy mock object onto a target object. This can effectively let you time travel to queue up any actions and replay them as many times as you would like. Every property accessor, every function call, etc will be replayed onto the target.
233+
234+
- `proxy` - the root proxy object that was returned from `recursiveProxyMock`
235+
236+
- `target` - any object/function/class etc which will be operated on in the same way that the `proxy` object was.
237+
212238
- Returns: Array of `ProxyData` objects, one for each time the path was visited on the proxy object. `null` if it was never visited.
213239

214240
#### ProxyData

lint-staged.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = {
22
"*": "prettier --write --ignore-unknown",
3-
"*.{js,ts,json,md}": "cspell",
3+
"*.{js,ts,json,md}": "cspell --no-must-find-files",
44
"*.{js,ts}": "eslint --max-warnings 0 --fix",
55
"README.md": "npm run readme-toc",
66
};

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { hasPathBeenCalledWith } from "~/hasPathBeenCalledWith";
55
export { getVisitedPathData } from "~/getVisitedPathData";
66
export { listAllProxyOperations } from "~/listAllProxyOperations";
77
export { isRecursiveProxyMock } from "~/isRecursiveProxyMock";
8+
export { replayProxy } from "~/replayProxy";
89
export {
910
ProxyData,
1011
ProxyOverrideConfig,

src/proxyTypes.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,94 @@
11
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in JSDoc
2-
import type { ProxySymbol } from "./ProxySymbol";
2+
import type { ProxySymbol } from "~/ProxySymbol";
33

4-
type ApplyProxyItem = {
4+
export type ApplyProxyItem = {
55
name: "apply";
66
pathKey: ProxyPath;
77
args: unknown[];
88
parent: number;
99
self: number;
1010
};
1111

12-
type ConstructProxyItem = {
12+
export type ConstructProxyItem = {
1313
name: "construct";
1414
pathKey: ProxyPath;
1515
args: unknown[];
1616
parent: number;
1717
self: number;
1818
};
1919

20-
type DefinePropertyProxyItem = {
20+
export type DefinePropertyProxyItem = {
2121
name: "defineProperty";
2222
pathKey: ProxyPath;
2323
prop: string | symbol;
2424
descriptor: PropertyDescriptor;
2525
parent: number;
2626
};
2727

28-
type DeletePropertyProxyItem = {
28+
export type DeletePropertyProxyItem = {
2929
name: "deleteProperty";
3030
pathKey: ProxyPath;
3131
prop: string | symbol;
3232
parent: number;
3333
};
3434

35-
type GetProxyItem = {
35+
export type GetProxyItem = {
3636
name: "get";
3737
pathKey: ProxyPath;
3838
prop: string | symbol;
3939
parent: number;
4040
self: number;
4141
};
4242

43-
type GetOwnPropertyDescriptorProxyItem = {
43+
export type GetOwnPropertyDescriptorProxyItem = {
4444
name: "getOwnPropertyDescriptor";
4545
pathKey: ProxyPath;
4646
prop: string | symbol;
4747
parent: number;
4848
self: number;
4949
};
5050

51-
type GetPrototypeOfProxyItem = {
51+
export type GetPrototypeOfProxyItem = {
5252
name: "getPrototypeOf";
5353
pathKey: ProxyPath;
5454
parent: number;
5555
self: number;
5656
};
5757

58-
type HasProxyItem = {
58+
export type HasProxyItem = {
5959
name: "has";
6060
pathKey: ProxyPath;
6161
prop: string | symbol;
6262
parent: number;
6363
};
6464

65-
type IsExtensibleProxyItem = {
65+
export type IsExtensibleProxyItem = {
6666
name: "isExtensible";
6767
pathKey: ProxyPath;
6868
parent: number;
6969
};
7070

71-
type OwnKeysProxyItem = {
71+
export type OwnKeysProxyItem = {
7272
name: "ownKeys";
7373
pathKey: ProxyPath;
7474
parent: number;
7575
};
7676

77-
type PreventExtensionsProxyItem = {
77+
export type PreventExtensionsProxyItem = {
7878
name: "preventExtensions";
7979
pathKey: ProxyPath;
8080
parent: number;
8181
};
8282

83-
type SetProxyItem = {
83+
export type SetProxyItem = {
8484
name: "set";
8585
pathKey: ProxyPath;
8686
prop: string | symbol;
8787
value: unknown;
8888
parent: number;
8989
};
9090

91-
type SetPrototypeOfProxyItem = {
91+
export type SetPrototypeOfProxyItem = {
9292
name: "setPrototypeOf";
9393
pathKey: ProxyPath;
9494
// eslint-disable-next-line @typescript-eslint/ban-types

src/replayProxy/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { replayProxy } from "./replayProxy";

src/replayProxy/replayProxy.test.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-call */
2+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
3+
import { replayProxy } from "./replayProxy";
4+
import { hasPathBeenVisited } from "~/hasPathBeenVisited";
5+
import { ProxySymbol } from "~/ProxySymbol";
6+
import { recursiveProxyMock } from "~/recursiveProxyMock";
7+
8+
describe("replayProxy", () => {
9+
test("console.warn when argument isn't a proxy mock", () => {
10+
const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
11+
replayProxy(null, []);
12+
expect(consoleWarnSpy).toHaveBeenCalled();
13+
});
14+
15+
test("example: store and replay operations", () => {
16+
const greeter = jest.fn();
17+
class TestClass {
18+
public e(name: string): void {
19+
greeter(`Hi ${name}`);
20+
}
21+
}
22+
const target = {
23+
a: {
24+
b: function (): { c: TestClass } {
25+
return {
26+
c: new TestClass(),
27+
};
28+
},
29+
},
30+
};
31+
const proxy = recursiveProxyMock<typeof target>();
32+
proxy.a.b().c.e("Jason");
33+
replayProxy(proxy, target);
34+
expect(greeter).toHaveBeenCalledWith("Hi Jason");
35+
});
36+
37+
test("replay: apply", () => {
38+
const target = jest.fn();
39+
const proxy = recursiveProxyMock();
40+
proxy("potato");
41+
replayProxy(proxy, target);
42+
expect(target).toHaveBeenCalledWith("potato");
43+
});
44+
45+
test("replay: construct", () => {
46+
const greeter = jest.fn();
47+
class TestClass {
48+
public e(name: string): void {
49+
greeter(`Hi ${name}`);
50+
}
51+
}
52+
const proxy = recursiveProxyMock<typeof TestClass>();
53+
new proxy().e("test");
54+
replayProxy(proxy, TestClass);
55+
expect(greeter).toHaveBeenCalledWith("Hi test");
56+
});
57+
58+
test("replay: defineProperty", () => {
59+
const obj: Record<string, Record<string, number>> = {};
60+
const proxy = recursiveProxyMock<typeof obj>();
61+
Object.defineProperty(proxy, "k", {
62+
value: {},
63+
enumerable: true,
64+
configurable: true,
65+
writable: true,
66+
});
67+
Object.defineProperty(proxy.k, "number", {
68+
value: 100,
69+
enumerable: true,
70+
configurable: true,
71+
writable: true,
72+
});
73+
replayProxy(proxy, obj);
74+
expect(obj).toStrictEqual({
75+
k: {
76+
number: 100,
77+
},
78+
});
79+
});
80+
81+
test("replay: deleteProperty", () => {
82+
type PartialObj = {
83+
num?: number;
84+
person: {
85+
name?: string;
86+
};
87+
};
88+
const obj: PartialObj = {
89+
num: 7,
90+
person: {
91+
name: "Joe",
92+
},
93+
};
94+
const proxy = recursiveProxyMock<PartialObj>();
95+
delete proxy.num;
96+
delete proxy.person.name;
97+
replayProxy(proxy, obj);
98+
expect(obj).toStrictEqual({
99+
person: {},
100+
});
101+
});
102+
103+
test("replay: get", () => {
104+
const getter = jest.fn();
105+
const obj = {
106+
a: {
107+
b: {
108+
get c(): number {
109+
getter();
110+
return 7;
111+
},
112+
},
113+
},
114+
};
115+
const proxy = recursiveProxyMock<typeof obj>();
116+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
117+
proxy.a.b.c;
118+
replayProxy(proxy, obj);
119+
expect(getter).toHaveBeenCalled();
120+
});
121+
122+
test("replay: getOwnPropertyDescriptor", () => {
123+
const obj = recursiveProxyMock<unknown>();
124+
const proxy = recursiveProxyMock<typeof obj>();
125+
Object.getOwnPropertyDescriptor(Object.getOwnPropertyDescriptor(proxy, "person")!.value, "name");
126+
replayProxy(proxy, obj);
127+
expect(
128+
hasPathBeenVisited(obj, [
129+
"person",
130+
ProxySymbol.GET_OWN_PROPERTY_DESCRIPTOR,
131+
"name",
132+
ProxySymbol.GET_OWN_PROPERTY_DESCRIPTOR,
133+
])
134+
).toStrictEqual(true);
135+
});
136+
137+
test("replay: getPrototypeOf", () => {
138+
const obj = recursiveProxyMock<unknown>();
139+
const proxy = recursiveProxyMock<typeof obj>();
140+
Object.getPrototypeOf(proxy);
141+
replayProxy(proxy, obj);
142+
expect(hasPathBeenVisited(obj, [ProxySymbol.GET_PROTOTYPE_OF])).toStrictEqual(true);
143+
});
144+
145+
test("replay: has", () => {
146+
const obj = recursiveProxyMock<Record<string, unknown>>();
147+
const proxy = recursiveProxyMock<typeof obj>();
148+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
149+
"favorite" in proxy;
150+
replayProxy(proxy, obj);
151+
expect(hasPathBeenVisited(obj, ["favorite", ProxySymbol.HAS])).toStrictEqual(true);
152+
});
153+
154+
test("replay: isExtensible", () => {
155+
const obj = recursiveProxyMock<unknown>();
156+
const proxy = recursiveProxyMock<typeof obj>();
157+
Object.isExtensible(proxy);
158+
replayProxy(proxy, obj);
159+
expect(hasPathBeenVisited(obj, [ProxySymbol.IS_EXTENSIBLE])).toStrictEqual(true);
160+
});
161+
162+
test("replay: isOwnKeys", () => {
163+
const obj = recursiveProxyMock<Record<string, unknown>>();
164+
const proxy = recursiveProxyMock<typeof obj>();
165+
Object.keys(proxy);
166+
replayProxy(proxy, obj);
167+
expect(hasPathBeenVisited(obj, [ProxySymbol.OWN_KEYS])).toStrictEqual(true);
168+
});
169+
170+
test("replay: preventExtensions", () => {
171+
const obj = recursiveProxyMock<Record<string, unknown>>();
172+
const proxy = recursiveProxyMock<typeof obj>();
173+
expect(() => {
174+
Object.preventExtensions(proxy);
175+
}).toThrow(TypeError);
176+
expect(() => {
177+
replayProxy(proxy, obj);
178+
}).toThrow(TypeError);
179+
expect(hasPathBeenVisited(obj, [ProxySymbol.PREVENT_EXTENSIONS])).toStrictEqual(true);
180+
});
181+
182+
test("replay: set", () => {
183+
const obj = {
184+
num: 7,
185+
person: {
186+
name: "Joe",
187+
},
188+
};
189+
const proxy = recursiveProxyMock<typeof obj>();
190+
proxy.num = 10;
191+
proxy.person.name = "Bob";
192+
replayProxy(proxy, obj);
193+
expect(obj).toStrictEqual({
194+
num: 10,
195+
person: {
196+
name: "Bob",
197+
},
198+
});
199+
});
200+
201+
test("replay: setPrototypeOf", () => {
202+
// eslint-disable-next-line @typescript-eslint/no-empty-function
203+
function target(): void {}
204+
const proxy = recursiveProxyMock<typeof target>();
205+
const prototype = {
206+
getName: jest.fn(),
207+
};
208+
Object.setPrototypeOf(proxy, prototype);
209+
replayProxy(proxy, target);
210+
expect(Object.getPrototypeOf(target)).toStrictEqual(prototype);
211+
});
212+
});

0 commit comments

Comments
 (0)