@znight/testing 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,118 @@
1
+ import type { ClassKey, Token, TypedToken } from '@znight/core'
2
+ import type { MetadataIndex } from './metadata-index.ts'
3
+ import type { DeepMock, Mocked } from './types.ts'
4
+
5
+ import { describeToken, displayName, ErrorCode, ZnightError } from '@znight/core'
6
+
7
+ import { clearMock, resetMock } from './mock.ts'
8
+ import { describeKey } from './utils.ts'
9
+
10
+ /**
11
+ * The auto-mocks of a `TestBed`, looked up by class, token, alias or display name.
12
+ *
13
+ * ```ts
14
+ * const { instance, mocks } = await TestBed.create(UserService, { metadata })
15
+ * mocks.get(UserRepository).findById.mockResolvedValue(user) // typed
16
+ * mocks.get('Logger').info // untyped deep mock
17
+ * ```
18
+ */
19
+ export interface MockRegistry {
20
+ /**
21
+ * The auto-mock injected for `key`: a class (typed result), a token, an alias (interface, base
22
+ * class) or a unique display name (`'UserRepository'`).
23
+ *
24
+ * @throws ZnightError ZNIGHT001 when no auto-mock matches (the message says why: provided value,
25
+ * configuration class, optional dependency...), ZNIGHT008 when a display name is ambiguous.
26
+ */
27
+ get<T>(key: ClassKey<T>): Mocked<T>
28
+ get<T = never>(key: TypedToken<T>): [T] extends [never] ? DeepMock : Mocked<T>
29
+ /** Whether an auto-mock matches `key`. */
30
+ has(key: ClassKey | Token): boolean
31
+ /** Tokens of the auto-mocked dependencies, in injection order. */
32
+ tokens(): Token[]
33
+ /** `mockClear()` on every spy of every auto-mock (recorded calls are forgotten). */
34
+ clearAll(): void
35
+ /** Resets every auto-mock to its initial state (calls and programmed behaviours are forgotten). */
36
+ resetAll(): void
37
+ }
38
+
39
+ /** Why a dependency was not auto-mocked (used in error messages). */
40
+ export type InjectionKind = 'provided' | 'override' | 'config' | 'optional' | 'interceptor' | 'real'
41
+
42
+ const REASONS: Readonly<Record<InjectionKind, string>> = {
43
+ provided: 'its value was passed with `provide`',
44
+ override: 'it was replaced with `overrides`',
45
+ config: 'it is a configuration class (a real instance is injected; set values with `config`)',
46
+ optional: 'it is an optional dependency (`undefined` is injected unless provided)',
47
+ interceptor: 'it is an interceptor (not applied unless `aop: "real"`)',
48
+ real: 'it was built for real (aop: "real")',
49
+ }
50
+
51
+ /** Default {@link MockRegistry} implementation (internal). */
52
+ export class TestMocks implements MockRegistry {
53
+ readonly #mocks: ReadonlyMap<Token, object>
54
+ readonly #others: ReadonlyMap<Token, InjectionKind>
55
+ readonly #index: MetadataIndex
56
+ readonly #owner: Token
57
+
58
+ constructor(
59
+ mocks: ReadonlyMap<Token, object>,
60
+ others: ReadonlyMap<Token, InjectionKind>,
61
+ index: MetadataIndex,
62
+ owner: Token,
63
+ ) {
64
+ this.#mocks = mocks
65
+ this.#others = others
66
+ this.#index = index
67
+ this.#owner = owner
68
+ }
69
+
70
+ get<T>(key: ClassKey<T>): Mocked<T>
71
+ get<T = never>(key: TypedToken<T>): [T] extends [never] ? DeepMock : Mocked<T>
72
+ get(key: unknown): unknown {
73
+ const token = this.#index.find(key, new Set(this.#mocks.keys()))
74
+ if (token !== undefined) return this.#mocks.get(token)
75
+ throw this.#notMockedError(key)
76
+ }
77
+
78
+ has(key: ClassKey | Token): boolean {
79
+ return this.#index.find(key, new Set(this.#mocks.keys())) !== undefined
80
+ }
81
+
82
+ tokens(): Token[] {
83
+ return [...this.#mocks.keys()]
84
+ }
85
+
86
+ clearAll(): void {
87
+ for (const mock of this.#mocks.values()) clearMock(mock)
88
+ }
89
+
90
+ resetAll(): void {
91
+ for (const mock of this.#mocks.values()) resetMock(mock)
92
+ }
93
+
94
+ #notMockedError(key: unknown): ZnightError {
95
+ const owner = displayName(this.#owner)
96
+ let other: Token | undefined
97
+ try {
98
+ other = this.#index.find(key, new Set(this.#others.keys()))
99
+ } catch {
100
+ other = undefined
101
+ }
102
+ if (other !== undefined) {
103
+ const kind = this.#others.get(other) as InjectionKind
104
+ return new ZnightError(ErrorCode.MissingProvider, `${describeToken(other)} is not mocked: ${REASONS[kind]}`)
105
+ }
106
+ const mocked = [...this.#mocks.keys()].map(displayName)
107
+ return new ZnightError(
108
+ ErrorCode.MissingProvider,
109
+ `No mock for ${describeKey(key)}: it is not a direct dependency of ${owner}`,
110
+ {
111
+ hints: [
112
+ mocked.length === 0 ? `${owner} has no auto-mocked dependency` : `Auto-mocked: ${mocked.join(', ')}`,
113
+ 'TestBed only mocks direct dependencies; regenerate the metadata if the constructor changed',
114
+ ],
115
+ },
116
+ )
117
+ }
118
+ }
package/src/mock.ts ADDED
@@ -0,0 +1,270 @@
1
+ import type { ClassKey } from '@znight/core'
2
+ import type { AnyFunction, DeepMock, Mocked, MockFnFactory, MockFnOption } from './types.ts'
3
+
4
+ import { resolveMockFn } from './mock-fn.ts'
5
+
6
+ /** Options of {@link createMock}. */
7
+ export interface CreateMockOptions {
8
+ /**
9
+ * Name of the mock, used to name its spies in assertion messages (`UserRepository.findById`).
10
+ * Default: the class name, or `mock`.
11
+ */
12
+ readonly name?: string
13
+ /**
14
+ * When `true`, calling an unconfigured method returns a deep mock (the same one for every call)
15
+ * instead of `undefined`, so fluent APIs work: `db.select().from('users')`. Default: `false`.
16
+ */
17
+ readonly deep?: boolean
18
+ /** Spy implementation. Default: auto-detected (see `MockFnOption`). */
19
+ readonly mockFn?: MockFnOption
20
+ }
21
+
22
+ /** Shared state of a mock and every spy created under it. */
23
+ interface MockTree {
24
+ readonly create: MockFnFactory
25
+ readonly deep: boolean
26
+ readonly spies: SpyRecord[]
27
+ }
28
+
29
+ interface SpyRecord {
30
+ readonly spy: SpyLike
31
+ readonly implementation: AnyFunction | undefined
32
+ }
33
+
34
+ interface SpyLike {
35
+ (...args: unknown[]): unknown
36
+ mockName?: (name: string) => unknown
37
+ mockClear?: () => unknown
38
+ mockReset?: () => unknown
39
+ mockImplementation?: (implementation: AnyFunction) => unknown
40
+ }
41
+
42
+ /**
43
+ * Property names never auto-mocked (they read as `undefined` unless set explicitly). Libraries probe
44
+ * them to detect promises (`then`), JSON values (`toJSON`), asymmetric matchers (`asymmetricMatch`),
45
+ * React elements (`$$typeof`), DOM nodes (`nodeType`, `tagName`), ES module namespaces
46
+ * (`__esModule`) or mock functions (`_isMockFunction`). Names starting with `@@` (Immutable.js
47
+ * sentinels) are reserved too. Symbol-keyed properties are never auto-mocked.
48
+ */
49
+ const RESERVED: ReadonlySet<string> = new Set([
50
+ 'then',
51
+ 'toJSON',
52
+ 'asymmetricMatch',
53
+ '$$typeof',
54
+ 'nodeType',
55
+ 'tagName',
56
+ '__esModule',
57
+ '_isMockFunction',
58
+ ])
59
+
60
+ const TREE: unique symbol = Symbol('znight.mock')
61
+
62
+ const mocks = new WeakSet<object>()
63
+
64
+ function isReserved(key: string): boolean {
65
+ return RESERVED.has(key) || key.startsWith('@@')
66
+ }
67
+
68
+ /**
69
+ * Creates a deep auto-mock.
70
+ *
71
+ * - Every property read returns a spy (`MockFunction`), created on first access and then stable.
72
+ * Spies are deep too: `mock.pool.query.mockResolvedValue(rows)`.
73
+ * - Unconfigured spies return `undefined` (or a deep mock with `{ deep: true }`).
74
+ * - The mock is **not thenable** and inert for serialisers: `then`, `toJSON`, `asymmetricMatch`,
75
+ * `$$typeof`, `Symbol.toPrimitive`... read as `undefined`, so `await mock`, `JSON.stringify(mock)`
76
+ * and `expect(...).toEqual(mock)` behave.
77
+ * - Assigned properties win: `mock.enabled = false`.
78
+ * - With a class, the mock inherits its prototype (`instanceof` works) and getters read as
79
+ * `undefined` instead of a (truthy) spy.
80
+ * - Functions of `partial` become spies with that implementation; other values are set as is.
81
+ *
82
+ * ```ts
83
+ * const repository = createMock(UserRepository, { findById: async id => ({ id }) })
84
+ * const logger = createMock<Logger>()
85
+ * logger.info('hello')
86
+ * expect(logger.info).toHaveBeenCalledWith('hello')
87
+ * ```
88
+ */
89
+ export function createMock<T>(cls: ClassKey<T>, partial?: Partial<T>, options?: CreateMockOptions): Mocked<T>
90
+ export function createMock<T = never>(
91
+ partial?: Partial<T>,
92
+ options?: CreateMockOptions,
93
+ ): [T] extends [never] ? DeepMock : Mocked<T>
94
+ export function createMock(first?: unknown, second?: unknown, third?: unknown): unknown {
95
+ if (typeof first === 'function') {
96
+ return mockObject(first as ClassKey<unknown>, second as object | undefined, third as CreateMockOptions | undefined)
97
+ }
98
+ return mockObject(undefined, first as object | undefined, second as CreateMockOptions | undefined)
99
+ }
100
+
101
+ /** {@link createMock} with an optional class (internal). */
102
+ export function mockObject(
103
+ cls: ClassKey<unknown> | undefined,
104
+ partial: object | undefined,
105
+ options: CreateMockOptions = {},
106
+ ): object {
107
+ if (partial !== undefined && (partial === null || typeof partial !== 'object')) {
108
+ throw new TypeError('[znight] createMock(): the partial implementation must be an object')
109
+ }
110
+ const tree: MockTree = {
111
+ create: resolveMockFn(options.mockFn).create,
112
+ deep: options.deep === true,
113
+ spies: [],
114
+ }
115
+ const className = typeof cls === 'function' ? (cls as { name?: unknown }).name : undefined
116
+ const name = options.name ?? (typeof className === 'string' && className !== '' ? className : 'mock')
117
+ return createNode(name, tree, cls, partial)
118
+ }
119
+
120
+ /** Whether `value` is a mock (or one of its spies) created by {@link createMock}. */
121
+ export function isMock(value: unknown): boolean {
122
+ return value !== null && (typeof value === 'object' || typeof value === 'function') && mocks.has(value)
123
+ }
124
+
125
+ /** Forgets the calls recorded by every spy of `mock` (`mockClear()`); implementations are kept. */
126
+ export function clearMock(mock: object): void {
127
+ for (const { spy } of treeOf(mock, 'clearMock').spies) spy.mockClear?.()
128
+ }
129
+
130
+ /**
131
+ * Resets every spy of `mock` to its initial state: recorded calls and programmed implementations
132
+ * are forgotten, implementations given to {@link createMock} (and deep returns) are restored.
133
+ */
134
+ export function resetMock(mock: object): void {
135
+ for (const { spy, implementation } of treeOf(mock, 'resetMock').spies) {
136
+ spy.mockReset?.()
137
+ if (implementation !== undefined) spy.mockImplementation?.(implementation)
138
+ }
139
+ }
140
+
141
+ function treeOf(mock: object, where: string): MockTree {
142
+ const tree = isMock(mock) ? (mock as { [TREE]?: MockTree })[TREE] : undefined
143
+ if (tree === undefined) throw new TypeError(`[znight] ${where}(): expected a mock created by createMock()`)
144
+ return tree
145
+ }
146
+
147
+ /** An object mock (the root of a mock, or the value returned by a deep spy). */
148
+ function createNode(
149
+ name: string,
150
+ tree: MockTree,
151
+ cls: ClassKey<unknown> | undefined,
152
+ partial: object | undefined,
153
+ ): object {
154
+ const target: Record<PropertyKey, unknown> = Object.create(
155
+ typeof cls === 'function' && isObjectLike(cls.prototype) ? (cls.prototype as object) : Object.prototype,
156
+ )
157
+ const accessors = typeof cls === 'function' ? accessorKeys(cls.prototype) : undefined
158
+ const children = new Map<string, unknown>()
159
+ if (partial !== undefined) {
160
+ for (const key of Reflect.ownKeys(partial)) {
161
+ const value = (partial as Record<PropertyKey, unknown>)[key]
162
+ if (typeof key === 'string' && typeof value === 'function') {
163
+ children.set(key, createSpyNode(`${name}.${key}`, tree, value as AnyFunction))
164
+ } else {
165
+ defineValue(target, key, value)
166
+ }
167
+ }
168
+ }
169
+ const proxy: object = new Proxy(target, {
170
+ get(object, key, receiver): unknown {
171
+ if (typeof key === 'symbol') {
172
+ if (key === TREE) return tree
173
+ return Object.hasOwn(object, key) ? object[key] : undefined
174
+ }
175
+ if (Object.hasOwn(object, key)) return object[key]
176
+ if (key === 'constructor') return Reflect.get(object, key, receiver)
177
+ if (isReserved(key)) return undefined
178
+ if (Object.hasOwn(Object.prototype, key)) return Reflect.get(Object.prototype, key, receiver)
179
+ if (accessors?.has(key) === true) return undefined
180
+ let child = children.get(key)
181
+ if (child === undefined) {
182
+ child = createSpyNode(`${name}.${key}`, tree, undefined)
183
+ children.set(key, child)
184
+ }
185
+ return child
186
+ },
187
+ set(object, key, value): boolean {
188
+ defineValue(object, key, value)
189
+ return true
190
+ },
191
+ deleteProperty(object, key): boolean {
192
+ if (typeof key === 'string') children.delete(key)
193
+ return Reflect.deleteProperty(object, key)
194
+ },
195
+ })
196
+ mocks.add(proxy)
197
+ return proxy
198
+ }
199
+
200
+ /** A spy whose unknown properties are deep spies (`spy.nested.method`). */
201
+ function createSpyNode(path: string, tree: MockTree, implementation: AnyFunction | undefined): SpyLike {
202
+ const effective = implementation ?? (tree.deep ? deepReturn(path, tree) : undefined)
203
+ const spy = tree.create(effective) as SpyLike
204
+ if (typeof spy !== 'function') {
205
+ throw new TypeError(`[znight] The mockFn factory returned ${typeof spy} instead of a function`)
206
+ }
207
+ if (typeof spy.mockName === 'function') spy.mockName(path)
208
+ makeDeep(spy, path, tree)
209
+ tree.spies.push({ spy, implementation: effective })
210
+ mocks.add(spy)
211
+ return spy
212
+ }
213
+
214
+ /** The implementation of a deep spy: returns the same deep mock on every call. */
215
+ function deepReturn(path: string, tree: MockTree): AnyFunction {
216
+ let result: object | undefined
217
+ return () => {
218
+ result ??= createNode(`${path}()`, tree, undefined, undefined)
219
+ return result
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Inserts a proxy between `spy` and its prototype: reading a property the spy does not have (own or
225
+ * inherited) creates a child spy. The spy itself stays a genuine spy of its framework, which
226
+ * `bun:test`'s `expect` requires (it rejects proxies).
227
+ */
228
+ function makeDeep(spy: SpyLike, path: string, tree: MockTree): void {
229
+ const original: unknown = Object.getPrototypeOf(spy)
230
+ if (!isObjectLike(original)) return
231
+ const children = new Map<string, SpyLike>()
232
+ const prototype = new Proxy(original as object, {
233
+ get(object, key, receiver): unknown {
234
+ if (typeof key === 'symbol' || key in object || isReserved(key)) return Reflect.get(object, key, receiver)
235
+ let child = children.get(key)
236
+ if (child === undefined) {
237
+ child = createSpyNode(`${path}.${key}`, tree, undefined)
238
+ children.set(key, child)
239
+ }
240
+ return child
241
+ },
242
+ })
243
+ try {
244
+ Object.setPrototypeOf(spy, prototype)
245
+ } catch {
246
+ // a frozen or exotic spy: it keeps working, without deep members
247
+ }
248
+ }
249
+
250
+ function defineValue(object: object, key: PropertyKey, value: unknown): void {
251
+ Object.defineProperty(object, key, { value, writable: true, enumerable: true, configurable: true })
252
+ }
253
+
254
+ /** Names of the accessors (getters/setters) declared along a prototype chain, `Object.prototype` excluded. */
255
+ function accessorKeys(prototype: unknown): ReadonlySet<string> {
256
+ const keys = new Set<string>()
257
+ let current: unknown = prototype
258
+ while (isObjectLike(current) && current !== Object.prototype && current !== Function.prototype) {
259
+ for (const key of Object.getOwnPropertyNames(current)) {
260
+ const descriptor = Object.getOwnPropertyDescriptor(current, key)
261
+ if (descriptor !== undefined && (descriptor.get !== undefined || descriptor.set !== undefined)) keys.add(key)
262
+ }
263
+ current = Object.getPrototypeOf(current)
264
+ }
265
+ return keys
266
+ }
267
+
268
+ function isObjectLike(value: unknown): value is object {
269
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
270
+ }
@@ -0,0 +1,54 @@
1
+ import type { DependencyRef, ProviderDefinition, ProviderFactory, ProviderKey, Scope } from '@znight/core'
2
+
3
+ /** A typed `[key, value]` entry for `overrides` / `providers` / `provide`. */
4
+ export type OverrideEntry<T = unknown> = readonly [ProviderKey<T>, T]
5
+
6
+ /** A `[key, definition]` entry replacing a provider with a factory. */
7
+ export type OverrideDefinitionEntry<T = unknown> = readonly [ProviderKey<T>, ProviderDefinition<T>]
8
+
9
+ /**
10
+ * A type-checked override: the value must match the type designated by the class or typed token.
11
+ *
12
+ * ```ts
13
+ * await TestApp.create({ metadata, overrides: [override(UserRepository, new InMemoryUserRepository())] })
14
+ * ```
15
+ */
16
+ export function override<T>(key: ProviderKey<T>, value: NoInfer<T>): OverrideEntry<T> {
17
+ return [key, value]
18
+ }
19
+
20
+ /** Options of {@link overrideFactory}. */
21
+ export interface OverrideFactoryOptions {
22
+ /** Tokens resolved and passed to the factory, in order. Default: none. */
23
+ readonly deps?: readonly DependencyRef[]
24
+ /** Default: `'singleton'`. */
25
+ readonly scope?: Scope
26
+ /** Set when the factory returns a promise (singletons only). */
27
+ readonly async?: boolean
28
+ /** Method called when the instance is disposed. */
29
+ readonly dispose?: string
30
+ }
31
+
32
+ /**
33
+ * Replaces a provider with a factory whose dependencies are resolved by the container (or, in
34
+ * `TestBed`, like the target's dependencies).
35
+ *
36
+ * ```ts
37
+ * overrideFactory(UserRepository, ([db]) => new InMemoryUserRepository(db), { deps: ['src/db.ts#Database'] })
38
+ * ```
39
+ */
40
+ export function overrideFactory<T>(
41
+ key: ProviderKey<T>,
42
+ factory: ProviderFactory<NoInfer<T>>,
43
+ options: OverrideFactoryOptions = {},
44
+ ): OverrideDefinitionEntry<T> {
45
+ if (typeof factory !== 'function') throw new TypeError('[znight] overrideFactory() expects a factory function')
46
+ const definition: ProviderDefinition<T> = {
47
+ scope: options.scope ?? 'singleton',
48
+ deps: options.deps ?? [],
49
+ factory,
50
+ ...(options.async === undefined ? {} : { async: options.async }),
51
+ ...(options.dispose === undefined ? {} : { dispose: options.dispose }),
52
+ }
53
+ return [key, definition]
54
+ }
package/src/spy.ts ADDED
@@ -0,0 +1,246 @@
1
+ import type { AnyFunction, MockFunction, UntypedFunction } from './types.ts'
2
+
3
+ type Implementation = (this: unknown, ...args: unknown[]) => unknown
4
+
5
+ interface MutableResult {
6
+ type: 'return' | 'throw' | 'incomplete'
7
+ value: unknown
8
+ }
9
+
10
+ interface MutableState {
11
+ readonly calls: unknown[][]
12
+ readonly results: MutableResult[]
13
+ readonly instances: unknown[]
14
+ readonly contexts: unknown[]
15
+ readonly invocationCallOrder: number[]
16
+ readonly lastCall: unknown[] | undefined
17
+ }
18
+
19
+ interface SpyInternals {
20
+ name: string
21
+ implementation: Implementation | undefined
22
+ readonly initial: Implementation | undefined
23
+ readonly once: Implementation[]
24
+ readonly state: MutableState
25
+ }
26
+
27
+ const DEFAULT_NAME = 'spy'
28
+
29
+ /** Global call counter shared by every built-in spy (`mock.invocationCallOrder`). */
30
+ let callOrder = 0
31
+
32
+ const internalsBySpy = new WeakMap<object, SpyInternals>()
33
+
34
+ function internalsOf(spy: unknown): SpyInternals {
35
+ const internals = typeof spy === 'function' ? internalsBySpy.get(spy) : undefined
36
+ if (internals === undefined) throw new TypeError('[znight] Not a spy created by createSpy()')
37
+ return internals
38
+ }
39
+
40
+ function createState(): MutableState {
41
+ const calls: unknown[][] = []
42
+ return {
43
+ calls,
44
+ results: [],
45
+ instances: [],
46
+ contexts: [],
47
+ invocationCallOrder: [],
48
+ get lastCall(): unknown[] | undefined {
49
+ return calls[calls.length - 1]
50
+ },
51
+ }
52
+ }
53
+
54
+ function clearState(state: MutableState): void {
55
+ state.calls.length = 0
56
+ state.results.length = 0
57
+ state.instances.length = 0
58
+ state.contexts.length = 0
59
+ state.invocationCallOrder.length = 0
60
+ }
61
+
62
+ function isObjectLike(value: unknown): boolean {
63
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
64
+ }
65
+
66
+ function invoke(internals: SpyInternals, self: unknown, args: unknown[], isNew: boolean): unknown {
67
+ const state = internals.state
68
+ state.calls.push(args)
69
+ state.contexts.push(self)
70
+ state.instances.push(self)
71
+ state.invocationCallOrder.push(++callOrder)
72
+ const result: MutableResult = { type: 'incomplete', value: undefined }
73
+ state.results.push(result)
74
+ const implementation = internals.once.shift() ?? internals.implementation
75
+ try {
76
+ const value = implementation === undefined ? undefined : implementation.apply(self, args)
77
+ const returned = isNew && !isObjectLike(value) ? self : value
78
+ result.type = 'return'
79
+ result.value = returned
80
+ return returned
81
+ } catch (error) {
82
+ result.type = 'throw'
83
+ result.value = error
84
+ throw error
85
+ }
86
+ }
87
+
88
+ function returning(value: unknown): Implementation {
89
+ return () => value
90
+ }
91
+
92
+ function resolving(value: unknown): Implementation {
93
+ return () => Promise.resolve(value)
94
+ }
95
+
96
+ function rejecting(error: unknown): Implementation {
97
+ return () => Promise.reject(error)
98
+ }
99
+
100
+ function returnThis(this: unknown): unknown {
101
+ return this
102
+ }
103
+
104
+ /** Methods shared by every built-in spy (the prototype of the spy functions). */
105
+ const spyPrototype: object = Object.create(Function.prototype, {
106
+ _isMockFunction: { value: true },
107
+ mock: {
108
+ get(this: unknown): MutableState {
109
+ return internalsOf(this).state
110
+ },
111
+ },
112
+ mockImplementation: {
113
+ value(this: unknown, implementation: Implementation): unknown {
114
+ internalsOf(this).implementation = implementation
115
+ return this
116
+ },
117
+ },
118
+ mockImplementationOnce: {
119
+ value(this: unknown, implementation: Implementation): unknown {
120
+ internalsOf(this).once.push(implementation)
121
+ return this
122
+ },
123
+ },
124
+ mockReturnValue: {
125
+ value(this: unknown, value: unknown): unknown {
126
+ internalsOf(this).implementation = returning(value)
127
+ return this
128
+ },
129
+ },
130
+ mockReturnValueOnce: {
131
+ value(this: unknown, value: unknown): unknown {
132
+ internalsOf(this).once.push(returning(value))
133
+ return this
134
+ },
135
+ },
136
+ mockResolvedValue: {
137
+ value(this: unknown, value: unknown): unknown {
138
+ internalsOf(this).implementation = resolving(value)
139
+ return this
140
+ },
141
+ },
142
+ mockResolvedValueOnce: {
143
+ value(this: unknown, value: unknown): unknown {
144
+ internalsOf(this).once.push(resolving(value))
145
+ return this
146
+ },
147
+ },
148
+ mockRejectedValue: {
149
+ value(this: unknown, error: unknown): unknown {
150
+ internalsOf(this).implementation = rejecting(error)
151
+ return this
152
+ },
153
+ },
154
+ mockRejectedValueOnce: {
155
+ value(this: unknown, error: unknown): unknown {
156
+ internalsOf(this).once.push(rejecting(error))
157
+ return this
158
+ },
159
+ },
160
+ mockReturnThis: {
161
+ value(this: unknown): unknown {
162
+ internalsOf(this).implementation = returnThis
163
+ return this
164
+ },
165
+ },
166
+ mockName: {
167
+ value(this: unknown, name: string): unknown {
168
+ internalsOf(this).name = String(name)
169
+ return this
170
+ },
171
+ },
172
+ getMockName: {
173
+ value(this: unknown): string {
174
+ return internalsOf(this).name
175
+ },
176
+ },
177
+ getMockImplementation: {
178
+ value(this: unknown): Implementation | undefined {
179
+ return internalsOf(this).implementation
180
+ },
181
+ },
182
+ mockClear: {
183
+ value(this: unknown): unknown {
184
+ clearState(internalsOf(this).state)
185
+ return this
186
+ },
187
+ },
188
+ mockReset: {
189
+ value(this: unknown): unknown {
190
+ const internals = internalsOf(this)
191
+ clearState(internals.state)
192
+ internals.once.length = 0
193
+ internals.implementation = internals.initial
194
+ return this
195
+ },
196
+ },
197
+ mockRestore: {
198
+ value(this: unknown): void {
199
+ const internals = internalsOf(this)
200
+ clearState(internals.state)
201
+ internals.once.length = 0
202
+ internals.implementation = internals.initial
203
+ },
204
+ },
205
+ })
206
+
207
+ /**
208
+ * Creates the built-in, dependency-free, Jest-compatible spy. It records `mock.calls`,
209
+ * `mock.results`, `mock.instances`, `mock.contexts`, `mock.invocationCallOrder` and `mock.lastCall`,
210
+ * supports `mockReturnValue(Once)`, `mockResolvedValue(Once)`, `mockRejectedValue(Once)`,
211
+ * `mockImplementation(Once)`, `mockReturnThis`, `mockName`, `mockClear`, `mockReset` and
212
+ * `mockRestore`, and is recognised by Jest's and Vitest's `expect` (`_isMockFunction`).
213
+ *
214
+ * `mockReset()` restores the implementation given to `createSpy()` (Vitest semantics).
215
+ *
216
+ * ```ts
217
+ * const find = createSpy((id: string) => ({ id }))
218
+ * find.mockReturnValueOnce({ id: 'x' })
219
+ * find('1')
220
+ * find.mock.calls // [['1']]
221
+ * ```
222
+ */
223
+ export function createSpy<F extends AnyFunction = UntypedFunction>(implementation?: F): MockFunction<F> {
224
+ if (implementation !== undefined && typeof implementation !== 'function') {
225
+ throw new TypeError('[znight] createSpy() expects a function implementation')
226
+ }
227
+ const initial = implementation as Implementation | undefined
228
+ const internals: SpyInternals = {
229
+ name: DEFAULT_NAME,
230
+ implementation: initial,
231
+ initial,
232
+ once: [],
233
+ state: createState(),
234
+ }
235
+ const spy = function (this: unknown, ...args: unknown[]): unknown {
236
+ return invoke(internals, this, args, new.target !== undefined)
237
+ }
238
+ Object.setPrototypeOf(spy, spyPrototype)
239
+ internalsBySpy.set(spy, internals)
240
+ return spy as unknown as MockFunction<F>
241
+ }
242
+
243
+ /** Whether `value` is a spy created by {@link createSpy}. */
244
+ export function isBuiltinSpy(value: unknown): boolean {
245
+ return typeof value === 'function' && internalsBySpy.has(value)
246
+ }