vitest-auto-spy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/index.cjs +369 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +238 -0
- package/dist/index.d.ts +238 -0
- package/dist/index.js +359 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexey Popov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# vitest-auto-spy
|
|
2
|
+
|
|
3
|
+
Create automatic, fully-typed test spies from a class — powered by Vitest's `vi.fn()`.
|
|
4
|
+
|
|
5
|
+
A **drop-in replacement for [`jest-auto-spies`](https://www.npmjs.com/package/jest-auto-spies)**:
|
|
6
|
+
the same API (`createSpyFromClass`, `provideAutoSpy`, `calledWith`, `resolveWith`,
|
|
7
|
+
`nextWith`, `accessorSpies`, …), but spying only on **Vitest** instead of Jest.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -D vitest-auto-spy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dependencies (provided by your project): `vitest`, `rxjs`, and — for the Angular
|
|
14
|
+
helpers — `@angular/core`.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
Manually mocking a service is tedious and brittle:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// 😫 the old way
|
|
24
|
+
const userService = {
|
|
25
|
+
getUser: vi.fn(),
|
|
26
|
+
getUserList: vi.fn(),
|
|
27
|
+
// ...one line per method, kept in sync by hand
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`createSpyFromClass` reads the class and generates a typed spy for **every** method:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// 😎 the auto-spy way
|
|
35
|
+
let userService: Spy<UserService>;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
userService = createSpyFromClass(UserService);
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`Spy<UserService>` exposes each method as a `vi.fn()` **plus** the right helpers based on
|
|
43
|
+
the method's return type (sync / `Promise` / `Observable`).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// 1. all methods (default)
|
|
51
|
+
createSpyFromClass(MyService);
|
|
52
|
+
|
|
53
|
+
// 2. only these methods
|
|
54
|
+
createSpyFromClass(MyService, ['getName', 'getAge']);
|
|
55
|
+
|
|
56
|
+
// 3. full config object
|
|
57
|
+
createSpyFromClass(MyService, {
|
|
58
|
+
methodsToSpyOn: ['getName'],
|
|
59
|
+
observablePropsToSpyOn: ['products$'], // Observable *properties*
|
|
60
|
+
gettersToSpyOn: ['userName'],
|
|
61
|
+
settersToSpyOn: ['userName'],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Synchronous methods
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// standard vi.fn() API works as-is
|
|
71
|
+
myService.getName.mockReturnValue('Fake Name');
|
|
72
|
+
|
|
73
|
+
// return a value only for specific arguments
|
|
74
|
+
myService.getName.calledWith(1).mockReturnValue('Fake Name');
|
|
75
|
+
expect(myService.getName(1)).toBe('Fake Name');
|
|
76
|
+
expect(myService.getName(2)).toBeUndefined();
|
|
77
|
+
|
|
78
|
+
// throw if called with the "wrong" arguments
|
|
79
|
+
myService.getName.mustBeCalledWith(1).mockReturnValue('Fake Name');
|
|
80
|
+
expect(() => myService.getName(2)).toThrow();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Promise-returning methods
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
myService.getProducts.resolveWith([{ name: 'Product 1' }]);
|
|
89
|
+
await expect(myService.getProducts()).resolves.toEqual([{ name: 'Product 1' }]);
|
|
90
|
+
|
|
91
|
+
myService.getProducts.rejectWith('FAKE ERROR');
|
|
92
|
+
await expect(myService.getProducts()).rejects.toBe('FAKE ERROR');
|
|
93
|
+
|
|
94
|
+
// per-call values, and conditional-by-args
|
|
95
|
+
myService.getProducts.resolveWithPerCall([{ value: ['a'] }, { value: ['b'] }]);
|
|
96
|
+
myService.getProducts.calledWith(1).resolveWith(['one']);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Observable-returning methods & Observable properties
|
|
102
|
+
|
|
103
|
+
Both spied **methods** that return an `Observable` and spied **properties** of type
|
|
104
|
+
`Observable` get the same control surface:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
myService.getProducts$.nextWith([{ name: 'Product 1' }]); // emit, stream stays open
|
|
108
|
+
myService.getProducts$.nextOneTimeWith([{ name: 'X' }]); // emit one value, then complete
|
|
109
|
+
myService.getProducts$.throwWith('FAKE ERROR'); // error the stream
|
|
110
|
+
myService.getProducts$.complete(); // complete the stream
|
|
111
|
+
|
|
112
|
+
// emit a precise sequence — values, errors, completion, optional delays
|
|
113
|
+
myService.getProducts$.nextWithValues([
|
|
114
|
+
{ value: [{ name: 'Product 1' }] },
|
|
115
|
+
{ errorValue: 'FAKE ERROR' },
|
|
116
|
+
{ complete: true },
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
// a fresh stream per call
|
|
120
|
+
myService.getProducts$.nextWithPerCall([{ value: ['a'] }, { value: ['b'] }]);
|
|
121
|
+
|
|
122
|
+
// grab the underlying Subject for full manual control
|
|
123
|
+
const subject = myService.getProducts$.returnSubject();
|
|
124
|
+
subject.next([{ name: 'manual' }]);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`calledWith(...)` / `mustBeCalledWith(...)` also chain into the observable helpers:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
myService.getProducts$.calledWith(1).nextWith([{ name: 'Product 1' }]);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Standalone observable builder
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { createObservableWithValues } from 'vitest-auto-spy';
|
|
137
|
+
|
|
138
|
+
const fake$ = createObservableWithValues([{ value: 1 }, { value: 2 }, { complete: true }]);
|
|
139
|
+
|
|
140
|
+
// or get the subject too
|
|
141
|
+
const { values$, subject } = createObservableWithValues([{ value: 1 }], { returnSubject: true });
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Getters & setters
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const spy = createSpyFromClass(MyService, {
|
|
150
|
+
gettersToSpyOn: ['userName'],
|
|
151
|
+
settersToSpyOn: ['userName'],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// configure / assert the getter
|
|
155
|
+
spy.accessorSpies.getters.userName.mockReturnValue('Fake Name');
|
|
156
|
+
expect(spy.userName).toBe('Fake Name');
|
|
157
|
+
|
|
158
|
+
// assert the setter was called
|
|
159
|
+
spy.userName = 'New Name';
|
|
160
|
+
expect(spy.accessorSpies.setters.userName).toHaveBeenCalledWith('New Name');
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Angular helpers
|
|
166
|
+
|
|
167
|
+
`provideAutoSpy` is the shorthand for providing an auto-spy in a `TestBed`:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { provideAutoSpy, injectSpy } from 'vitest-auto-spy';
|
|
171
|
+
|
|
172
|
+
TestBed.configureTestingModule({
|
|
173
|
+
providers: [
|
|
174
|
+
provideAutoSpy(MyService),
|
|
175
|
+
// accepts the same second argument as createSpyFromClass
|
|
176
|
+
provideAutoSpy(ApiService, { methodsToSpyOn: ['get', 'post'] }),
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
let myService: Spy<MyService>;
|
|
181
|
+
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
myService = injectSpy(MyService);
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
> The spies are change-detection agnostic, so they work in **both zoneless and
|
|
188
|
+
> zone.js** Angular projects — nothing here touches `NgZone` or change detection.
|
|
189
|
+
> You only need the usual Vitest + Angular wiring:
|
|
190
|
+
> [`@analogjs/vite-plugin-angular`](https://www.npmjs.com/package/@analogjs/vite-plugin-angular)
|
|
191
|
+
> plus a TestBed setup file (e.g. `@analogjs/vitest-angular`'s `setupTestBed()`).
|
|
192
|
+
|
|
193
|
+
### Signal / readonly property mocking (bonus)
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { mockReadonlyProp, mockReadonlyPropGetter, mockAccessorsProp } from 'vitest-auto-spy';
|
|
197
|
+
|
|
198
|
+
mockReadonlyProp(service, 'isReady', true); // static value (incl. signals)
|
|
199
|
+
mockReadonlyPropGetter(service, 'label', () => 'A'); // dynamic getter
|
|
200
|
+
mockAccessorsProp(service, 'theme'); // spied get + set
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## API reference
|
|
206
|
+
|
|
207
|
+
| Export | Description |
|
|
208
|
+
| --- | --- |
|
|
209
|
+
| `createSpyFromClass(Class, methodsOrConfig?)` | Build a fully-typed `Spy<T>` from a class |
|
|
210
|
+
| `provideAutoSpy(Class, methodsOrConfig?)` | Angular `{ provide, useValue }` shorthand |
|
|
211
|
+
| `injectSpy(token)` | `TestBed.inject` typed as `Spy<T>` |
|
|
212
|
+
| `createFunctionSpy(name)` | A single standalone function spy with all helpers |
|
|
213
|
+
| `createObservableWithValues(configs, opts?)` | Build an Observable from value configs |
|
|
214
|
+
| `mockReadonlyProp` / `mockReadonlyPropGetter` / `mockAccessorsProp` | Mock readonly / accessor / signal props |
|
|
215
|
+
| `errorHandler` | The `mustBeCalledWith` argument-mismatch error helper |
|
|
216
|
+
|
|
217
|
+
**Spied sync method:** `mockReturnValue`, `calledWith(...)`, `mustBeCalledWith(...)`
|
|
218
|
+
|
|
219
|
+
**Spied Promise method:** `resolveWith`, `rejectWith`, `resolveWithPerCall`
|
|
220
|
+
|
|
221
|
+
**Spied Observable method / property:** `nextWith`, `nextOneTimeWith`, `nextWithValues`,
|
|
222
|
+
`nextWithPerCall`, `throwWith`, `complete`, `returnSubject`
|
|
223
|
+
|
|
224
|
+
**Config (`ClassSpyConfiguration`):** `methodsToSpyOn`, `observablePropsToSpyOn`,
|
|
225
|
+
`gettersToSpyOn`, `settersToSpyOn`
|
|
226
|
+
|
|
227
|
+
`ValueConfig` (for `nextWithValues`): `{ value, delay? }` | `{ errorValue, delay? }` | `{ complete?, delay? }`.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT © Alexey Popov
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var rxjs = require('rxjs');
|
|
4
|
+
var operators = require('rxjs/operators');
|
|
5
|
+
var testing = require('@angular/core/testing');
|
|
6
|
+
var javascriptStringify = require('javascript-stringify');
|
|
7
|
+
var vitest = require('vitest');
|
|
8
|
+
|
|
9
|
+
// src/auto-spy.ts
|
|
10
|
+
var ArgsMap = class {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.map = {};
|
|
13
|
+
}
|
|
14
|
+
// `stringify` always serializes an argument array to a string, so the cast is safe.
|
|
15
|
+
set(key, value) {
|
|
16
|
+
this.map[javascriptStringify.stringify(key)] = value;
|
|
17
|
+
}
|
|
18
|
+
get(key) {
|
|
19
|
+
return this.map[javascriptStringify.stringify(key)];
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var errorHandler = {
|
|
23
|
+
throwArgumentsError(actualArgs, functionName) {
|
|
24
|
+
let errorMessage = `The function '${functionName}' was configured with 'mustBeCalledWith' and expects to be called with specific arguments. `;
|
|
25
|
+
if (actualArgs.length === 0) {
|
|
26
|
+
errorMessage += `But the function was called without any arguments.`;
|
|
27
|
+
} else {
|
|
28
|
+
let formattedArgs = javascriptStringify.stringify(actualArgs);
|
|
29
|
+
formattedArgs = formattedArgs.substring(1, formattedArgs.length - 1);
|
|
30
|
+
errorMessage += `But the actual arguments were: ${formattedArgs}`;
|
|
31
|
+
}
|
|
32
|
+
throw new Error(errorMessage);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function createReplaySubject() {
|
|
36
|
+
return new rxjs.ReplaySubject(1);
|
|
37
|
+
}
|
|
38
|
+
function mergeSubjectWithDefaultValues(subject, valuesConfigs) {
|
|
39
|
+
const onCompleteSubject = new rxjs.ReplaySubject(1);
|
|
40
|
+
const results$ = rxjs.from(valuesConfigs).pipe(
|
|
41
|
+
operators.concatMap((valueConfig) => {
|
|
42
|
+
if ("complete" in valueConfig && valueConfig.complete && valueConfig.delay) {
|
|
43
|
+
return rxjs.of(valueConfig).pipe(operators.delay(valueConfig.delay));
|
|
44
|
+
}
|
|
45
|
+
return rxjs.of(valueConfig);
|
|
46
|
+
}),
|
|
47
|
+
operators.takeWhile((valueConfig) => {
|
|
48
|
+
if (!("complete" in valueConfig)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (valueConfig.complete) {
|
|
52
|
+
onCompleteSubject.next();
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}),
|
|
57
|
+
operators.concatMap((valueConfig) => {
|
|
58
|
+
if ("value" in valueConfig && valueConfig.value) {
|
|
59
|
+
if (valueConfig.delay) {
|
|
60
|
+
return rxjs.of(valueConfig.value).pipe(operators.delay(valueConfig.delay));
|
|
61
|
+
}
|
|
62
|
+
return rxjs.of(valueConfig.value);
|
|
63
|
+
}
|
|
64
|
+
if ("errorValue" in valueConfig && valueConfig.errorValue) {
|
|
65
|
+
if (valueConfig.delay) {
|
|
66
|
+
return rxjs.timer(valueConfig.delay).pipe(operators.switchMap(() => rxjs.throwError(() => valueConfig.errorValue)));
|
|
67
|
+
}
|
|
68
|
+
return rxjs.throwError(() => valueConfig.errorValue);
|
|
69
|
+
}
|
|
70
|
+
return rxjs.EMPTY;
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
return rxjs.merge(results$, subject.pipe(operators.takeUntil(onCompleteSubject)));
|
|
74
|
+
}
|
|
75
|
+
function addObservableHelpers(objectToDecorate, providedSubject, onSubjectConfigured) {
|
|
76
|
+
objectToDecorate.nextWith = (value) => {
|
|
77
|
+
providedSubject.next(value);
|
|
78
|
+
onSubjectConfigured(providedSubject);
|
|
79
|
+
};
|
|
80
|
+
objectToDecorate.nextOneTimeWith = (value) => {
|
|
81
|
+
providedSubject.next(value);
|
|
82
|
+
providedSubject.complete();
|
|
83
|
+
onSubjectConfigured(providedSubject);
|
|
84
|
+
};
|
|
85
|
+
objectToDecorate.nextWithValues = (valuesConfigs) => {
|
|
86
|
+
if (valuesConfigs.length === 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
onSubjectConfigured(mergeSubjectWithDefaultValues(providedSubject, valuesConfigs));
|
|
90
|
+
};
|
|
91
|
+
objectToDecorate.throwWith = (value) => {
|
|
92
|
+
providedSubject.error(value);
|
|
93
|
+
onSubjectConfigured(providedSubject);
|
|
94
|
+
};
|
|
95
|
+
objectToDecorate.complete = () => {
|
|
96
|
+
providedSubject.complete();
|
|
97
|
+
onSubjectConfigured(providedSubject);
|
|
98
|
+
};
|
|
99
|
+
objectToDecorate.returnSubject = () => {
|
|
100
|
+
onSubjectConfigured(providedSubject);
|
|
101
|
+
return providedSubject;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function addNextWithPerCall(objectToDecorate, returnValueContainer, onConfigured = () => void 0) {
|
|
105
|
+
objectToDecorate.nextWithPerCall = (valueConfigsPerCall) => {
|
|
106
|
+
const returnedSubjects = [];
|
|
107
|
+
if (valueConfigsPerCall.length === 0) {
|
|
108
|
+
return returnedSubjects;
|
|
109
|
+
}
|
|
110
|
+
returnValueContainer.valuesPerCalls = [];
|
|
111
|
+
valueConfigsPerCall.forEach((valueConfiguration) => {
|
|
112
|
+
const replaySubject = new rxjs.ReplaySubject(1);
|
|
113
|
+
replaySubject.next(valueConfiguration.value);
|
|
114
|
+
returnedSubjects.push(replaySubject);
|
|
115
|
+
let returnedObservable = replaySubject.asObservable();
|
|
116
|
+
if (valueConfiguration.delay) {
|
|
117
|
+
returnedObservable = returnedObservable.pipe(operators.delay(valueConfiguration.delay));
|
|
118
|
+
}
|
|
119
|
+
if (!valueConfiguration.doNotComplete) {
|
|
120
|
+
returnedObservable = returnedObservable.pipe(operators.take(1));
|
|
121
|
+
}
|
|
122
|
+
returnValueContainer.valuesPerCalls.push({ wrappedValue: returnedObservable });
|
|
123
|
+
});
|
|
124
|
+
onConfigured(returnValueContainer);
|
|
125
|
+
return returnedSubjects;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function addObservableHelpersToFunctionSpy(spyFunction, valueContainer) {
|
|
129
|
+
const subject = createReplaySubject();
|
|
130
|
+
addObservableHelpers(spyFunction, subject, (configuredSubject) => {
|
|
131
|
+
valueContainer.value = configuredSubject;
|
|
132
|
+
});
|
|
133
|
+
addNextWithPerCall(spyFunction, valueContainer);
|
|
134
|
+
}
|
|
135
|
+
function addObservableHelpersToCalledWithObject(calledWithObject, calledWithArgs) {
|
|
136
|
+
const subject = createReplaySubject();
|
|
137
|
+
const returnValueContainer = { value: void 0 };
|
|
138
|
+
addObservableHelpers(calledWithObject, subject, (configuredSubject) => {
|
|
139
|
+
returnValueContainer.value = configuredSubject;
|
|
140
|
+
calledWithObject.argsToValuesMap.set(calledWithArgs, returnValueContainer);
|
|
141
|
+
});
|
|
142
|
+
addNextWithPerCall(calledWithObject, returnValueContainer, (configured) => {
|
|
143
|
+
calledWithObject.argsToValuesMap.set(calledWithArgs, configured);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function createObservableWithValues(valuesConfigs, config) {
|
|
147
|
+
const subject = createReplaySubject();
|
|
148
|
+
const values$ = mergeSubjectWithDefaultValues(subject, valuesConfigs);
|
|
149
|
+
if (config && config.returnSubject) {
|
|
150
|
+
return { values$, subject };
|
|
151
|
+
}
|
|
152
|
+
return values$;
|
|
153
|
+
}
|
|
154
|
+
function createObservablePropSpy() {
|
|
155
|
+
let subject = createReplaySubject();
|
|
156
|
+
const observableSpy = rxjs.defer(() => subject);
|
|
157
|
+
addObservableHelpers(observableSpy, subject, (configuredSubject) => {
|
|
158
|
+
subject = configuredSubject;
|
|
159
|
+
});
|
|
160
|
+
return observableSpy;
|
|
161
|
+
}
|
|
162
|
+
function addPromiseHelpersToFunctionSpy(spyFunction, valueContainer) {
|
|
163
|
+
spyFunction.resolveWith = (value) => {
|
|
164
|
+
valueContainer.value = Promise.resolve(value);
|
|
165
|
+
};
|
|
166
|
+
spyFunction.rejectWith = (value) => {
|
|
167
|
+
valueContainer.value = value;
|
|
168
|
+
valueContainer._isRejectedPromise = true;
|
|
169
|
+
};
|
|
170
|
+
spyFunction.resolveWithPerCall = (valueConfigsPerCall) => {
|
|
171
|
+
if (valueConfigsPerCall.length === 0) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
valueContainer.valuesPerCalls = [];
|
|
175
|
+
valueConfigsPerCall.forEach((valueConfiguration) => {
|
|
176
|
+
valueContainer.valuesPerCalls.push({
|
|
177
|
+
wrappedValue: Promise.resolve(valueConfiguration.value),
|
|
178
|
+
delay: valueConfiguration.delay
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function addPromiseHelpersToCalledWithObject(calledWithObject, calledWithArgs) {
|
|
184
|
+
calledWithObject.resolveWith = (value) => {
|
|
185
|
+
calledWithObject.argsToValuesMap.set(calledWithArgs, { value: Promise.resolve(value) });
|
|
186
|
+
};
|
|
187
|
+
calledWithObject.rejectWith = (value) => {
|
|
188
|
+
calledWithObject.argsToValuesMap.set(calledWithArgs, { value, _isRejectedPromise: true });
|
|
189
|
+
};
|
|
190
|
+
calledWithObject.resolveWithPerCall = (valueConfigsPerCall) => {
|
|
191
|
+
if (valueConfigsPerCall.length === 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const valueContainer = { value: void 0, valuesPerCalls: [] };
|
|
195
|
+
valueConfigsPerCall.forEach((valueConfiguration) => {
|
|
196
|
+
valueContainer.valuesPerCalls.push({
|
|
197
|
+
wrappedValue: Promise.resolve(valueConfiguration.value),
|
|
198
|
+
delay: valueConfiguration.delay
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
calledWithObject.argsToValuesMap.set(calledWithArgs, valueContainer);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function getNextCallValue(valueContainer) {
|
|
205
|
+
const wrapped = valueContainer.valuesPerCalls.shift();
|
|
206
|
+
let returnedValue = wrapped?.wrappedValue;
|
|
207
|
+
if (wrapped && wrapped.delay) {
|
|
208
|
+
returnedValue = returnedValue.then(
|
|
209
|
+
(value) => new Promise((resolve) => setTimeout(() => resolve(value), wrapped.delay))
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return returnedValue;
|
|
213
|
+
}
|
|
214
|
+
function returnTheCorrectFakeValue(calledWithObject, mustBeCalledWithObject, valueContainer, actualArgs, functionName) {
|
|
215
|
+
if (calledWithObject.wasConfigured) {
|
|
216
|
+
const configured = calledWithObject.argsToValuesMap.get(actualArgs);
|
|
217
|
+
if (configured) {
|
|
218
|
+
return unwrapContainer(configured);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (mustBeCalledWithObject.wasConfigured) {
|
|
222
|
+
const configured = mustBeCalledWithObject.argsToValuesMap.get(actualArgs);
|
|
223
|
+
if (configured) {
|
|
224
|
+
return unwrapContainer(configured);
|
|
225
|
+
}
|
|
226
|
+
errorHandler.throwArgumentsError(actualArgs, functionName);
|
|
227
|
+
}
|
|
228
|
+
return unwrapContainer(valueContainer);
|
|
229
|
+
}
|
|
230
|
+
function unwrapContainer(container) {
|
|
231
|
+
if (container._isRejectedPromise) {
|
|
232
|
+
return Promise.reject(container.value);
|
|
233
|
+
}
|
|
234
|
+
if (container.valuesPerCalls?.length) {
|
|
235
|
+
return getNextCallValue(container);
|
|
236
|
+
}
|
|
237
|
+
return container.value;
|
|
238
|
+
}
|
|
239
|
+
function addMethodsToCalledWith(calledWith, calledWithArgs) {
|
|
240
|
+
calledWith.wasConfigured = true;
|
|
241
|
+
calledWith.mockReturnValue = (value) => {
|
|
242
|
+
calledWith.argsToValuesMap.set(calledWithArgs, { value });
|
|
243
|
+
};
|
|
244
|
+
addPromiseHelpersToCalledWithObject(calledWith, calledWithArgs);
|
|
245
|
+
addObservableHelpersToCalledWithObject(calledWith, calledWithArgs);
|
|
246
|
+
return calledWith;
|
|
247
|
+
}
|
|
248
|
+
function createFunctionSpy(name) {
|
|
249
|
+
const calledWithObject = { wasConfigured: false, argsToValuesMap: new ArgsMap() };
|
|
250
|
+
const mustBeCalledWithObject = { wasConfigured: false, argsToValuesMap: new ArgsMap() };
|
|
251
|
+
const valueContainer = { value: void 0 };
|
|
252
|
+
const functionSpy = vitest.vi.fn(
|
|
253
|
+
(...actualArgs) => returnTheCorrectFakeValue(calledWithObject, mustBeCalledWithObject, valueContainer, actualArgs, name)
|
|
254
|
+
);
|
|
255
|
+
functionSpy.mockName(name);
|
|
256
|
+
addPromiseHelpersToFunctionSpy(functionSpy, valueContainer);
|
|
257
|
+
addObservableHelpersToFunctionSpy(functionSpy, valueContainer);
|
|
258
|
+
functionSpy.calledWith = (...calledWithArgs) => addMethodsToCalledWith(calledWithObject, calledWithArgs);
|
|
259
|
+
functionSpy.mustBeCalledWith = (...calledWithArgs) => addMethodsToCalledWith(mustBeCalledWithObject, calledWithArgs);
|
|
260
|
+
return functionSpy;
|
|
261
|
+
}
|
|
262
|
+
function defineWithEmptyAccessors(obj, prop) {
|
|
263
|
+
Object.defineProperty(obj, prop, {
|
|
264
|
+
get() {
|
|
265
|
+
return void 0;
|
|
266
|
+
},
|
|
267
|
+
set(_value) {
|
|
268
|
+
},
|
|
269
|
+
configurable: true
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function accessorSpyFactory(autoSpy, accessorName, accessorType) {
|
|
273
|
+
if (accessorType === "setter") {
|
|
274
|
+
return vitest.vi.spyOn(autoSpy, accessorName, "set");
|
|
275
|
+
}
|
|
276
|
+
return vitest.vi.spyOn(autoSpy, accessorName, "get");
|
|
277
|
+
}
|
|
278
|
+
function createAccessorsSpies(autoSpy, gettersToSpyOn, settersToSpyOn) {
|
|
279
|
+
autoSpy.accessorSpies = { getters: {}, setters: {} };
|
|
280
|
+
gettersToSpyOn.forEach((getterName) => {
|
|
281
|
+
defineWithEmptyAccessors(autoSpy, getterName);
|
|
282
|
+
autoSpy.accessorSpies.getters[getterName] = accessorSpyFactory(autoSpy, getterName, "getter");
|
|
283
|
+
});
|
|
284
|
+
settersToSpyOn.forEach((setterName) => {
|
|
285
|
+
if (!Object.prototype.hasOwnProperty.call(autoSpy, setterName)) {
|
|
286
|
+
defineWithEmptyAccessors(autoSpy, setterName);
|
|
287
|
+
}
|
|
288
|
+
autoSpy.accessorSpies.setters[setterName] = accessorSpyFactory(autoSpy, setterName, "setter");
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function extractMethodsFromObject(obj) {
|
|
292
|
+
const descriptors = Object.getOwnPropertyDescriptors(obj);
|
|
293
|
+
return Object.keys(descriptors).reduce((names, name) => {
|
|
294
|
+
if (name !== "constructor" && !descriptors[name].get) {
|
|
295
|
+
names.push(name);
|
|
296
|
+
}
|
|
297
|
+
return names;
|
|
298
|
+
}, []);
|
|
299
|
+
}
|
|
300
|
+
function getAllMethodNames(obj) {
|
|
301
|
+
let methods = [];
|
|
302
|
+
while (obj) {
|
|
303
|
+
const parentObj = Object.getPrototypeOf(obj);
|
|
304
|
+
if (parentObj) {
|
|
305
|
+
methods = methods.concat(extractMethodsFromObject(obj));
|
|
306
|
+
}
|
|
307
|
+
obj = parentObj;
|
|
308
|
+
}
|
|
309
|
+
return methods;
|
|
310
|
+
}
|
|
311
|
+
function createSpyFromClass(ObjectClass, methodsToSpyOnOrConfig) {
|
|
312
|
+
const methodNames = getAllMethodNames(ObjectClass.prototype);
|
|
313
|
+
let methodsToSpyOn = [];
|
|
314
|
+
let observablePropsToSpyOn = [];
|
|
315
|
+
let settersToSpyOn = [];
|
|
316
|
+
let gettersToSpyOn = [];
|
|
317
|
+
if (methodsToSpyOnOrConfig) {
|
|
318
|
+
if (Array.isArray(methodsToSpyOnOrConfig)) {
|
|
319
|
+
methodsToSpyOn = methodsToSpyOnOrConfig;
|
|
320
|
+
} else {
|
|
321
|
+
methodsToSpyOn = methodsToSpyOnOrConfig.methodsToSpyOn || [];
|
|
322
|
+
observablePropsToSpyOn = methodsToSpyOnOrConfig.observablePropsToSpyOn || [];
|
|
323
|
+
settersToSpyOn = methodsToSpyOnOrConfig.settersToSpyOn || [];
|
|
324
|
+
gettersToSpyOn = methodsToSpyOnOrConfig.gettersToSpyOn || [];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (methodsToSpyOn.length > 0) {
|
|
328
|
+
methodNames.push(...methodsToSpyOn);
|
|
329
|
+
}
|
|
330
|
+
const autoSpy = {};
|
|
331
|
+
observablePropsToSpyOn.forEach((observablePropName) => {
|
|
332
|
+
autoSpy[observablePropName] = createObservablePropSpy();
|
|
333
|
+
});
|
|
334
|
+
createAccessorsSpies(autoSpy, gettersToSpyOn, settersToSpyOn);
|
|
335
|
+
methodNames.forEach((methodName) => {
|
|
336
|
+
autoSpy[methodName] = createFunctionSpy(methodName);
|
|
337
|
+
});
|
|
338
|
+
return autoSpy;
|
|
339
|
+
}
|
|
340
|
+
function provideAutoSpy(ObjectClass, methodsToSpyOnOrConfig) {
|
|
341
|
+
return {
|
|
342
|
+
provide: ObjectClass,
|
|
343
|
+
useValue: createSpyFromClass(ObjectClass, methodsToSpyOnOrConfig)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function injectSpy(token) {
|
|
347
|
+
return testing.TestBed.inject(token);
|
|
348
|
+
}
|
|
349
|
+
function mockReadonlyProp(object, property, value) {
|
|
350
|
+
Object.defineProperty(object, property, { get: () => value, configurable: true });
|
|
351
|
+
}
|
|
352
|
+
function mockReadonlyPropGetter(object, property, getter) {
|
|
353
|
+
Object.defineProperty(object, property, { get: getter, configurable: true });
|
|
354
|
+
}
|
|
355
|
+
function mockAccessorsProp(object, property) {
|
|
356
|
+
Object.defineProperty(object, property, { get: vitest.vi.fn(), set: vitest.vi.fn(), configurable: true });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
exports.createFunctionSpy = createFunctionSpy;
|
|
360
|
+
exports.createObservableWithValues = createObservableWithValues;
|
|
361
|
+
exports.createSpyFromClass = createSpyFromClass;
|
|
362
|
+
exports.errorHandler = errorHandler;
|
|
363
|
+
exports.injectSpy = injectSpy;
|
|
364
|
+
exports.mockAccessorsProp = mockAccessorsProp;
|
|
365
|
+
exports.mockReadonlyProp = mockReadonlyProp;
|
|
366
|
+
exports.mockReadonlyPropGetter = mockReadonlyPropGetter;
|
|
367
|
+
exports.provideAutoSpy = provideAutoSpy;
|
|
368
|
+
//# sourceMappingURL=index.cjs.map
|
|
369
|
+
//# sourceMappingURL=index.cjs.map
|