@grest-ts/testkit 0.0.5
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 +413 -0
- package/dist/src/GGBundleTest.d.ts +8 -0
- package/dist/src/GGBundleTest.d.ts.map +1 -0
- package/dist/src/GGBundleTest.js +75 -0
- package/dist/src/GGBundleTest.js.map +1 -0
- package/dist/src/GGTest.d.ts +131 -0
- package/dist/src/GGTest.d.ts.map +1 -0
- package/dist/src/GGTest.js +245 -0
- package/dist/src/GGTest.js.map +1 -0
- package/dist/src/GGTestContext.d.ts +36 -0
- package/dist/src/GGTestContext.d.ts.map +1 -0
- package/dist/src/GGTestContext.js +63 -0
- package/dist/src/GGTestContext.js.map +1 -0
- package/dist/src/GGTestRunner.d.ts +108 -0
- package/dist/src/GGTestRunner.d.ts.map +1 -0
- package/dist/src/GGTestRunner.js +242 -0
- package/dist/src/GGTestRunner.js.map +1 -0
- package/dist/src/GGTestRuntime.d.ts +103 -0
- package/dist/src/GGTestRuntime.d.ts.map +1 -0
- package/dist/src/GGTestRuntime.js +219 -0
- package/dist/src/GGTestRuntime.js.map +1 -0
- package/dist/src/GGTestRuntimeWorker.d.ts +41 -0
- package/dist/src/GGTestRuntimeWorker.d.ts.map +1 -0
- package/dist/src/GGTestRuntimeWorker.js +136 -0
- package/dist/src/GGTestRuntimeWorker.js.map +1 -0
- package/dist/src/GGTestSharedRef.d.ts +35 -0
- package/dist/src/GGTestSharedRef.d.ts.map +1 -0
- package/dist/src/GGTestSharedRef.js +126 -0
- package/dist/src/GGTestSharedRef.js.map +1 -0
- package/dist/src/GGTestkitExtensionsDiscovery.d.ts +21 -0
- package/dist/src/GGTestkitExtensionsDiscovery.d.ts.map +1 -0
- package/dist/src/GGTestkitExtensionsDiscovery.js +24 -0
- package/dist/src/GGTestkitExtensionsDiscovery.js.map +1 -0
- package/dist/src/IGGLocalDiscoveryServer.d.ts +16 -0
- package/dist/src/IGGLocalDiscoveryServer.d.ts.map +1 -0
- package/dist/src/IGGLocalDiscoveryServer.js +2 -0
- package/dist/src/IGGLocalDiscoveryServer.js.map +1 -0
- package/dist/src/callOn/GGCallOnSelector.d.ts +42 -0
- package/dist/src/callOn/GGCallOnSelector.d.ts.map +1 -0
- package/dist/src/callOn/GGCallOnSelector.js +35 -0
- package/dist/src/callOn/GGCallOnSelector.js.map +1 -0
- package/dist/src/callOn/GGContractClass.implement.d.ts +8 -0
- package/dist/src/callOn/GGContractClass.implement.d.ts.map +1 -0
- package/dist/src/callOn/GGContractClass.implement.js +31 -0
- package/dist/src/callOn/GGContractClass.implement.js.map +1 -0
- package/dist/src/callOn/GGTestActionForLocatorOnCall.d.ts +28 -0
- package/dist/src/callOn/GGTestActionForLocatorOnCall.d.ts.map +1 -0
- package/dist/src/callOn/GGTestActionForLocatorOnCall.js +118 -0
- package/dist/src/callOn/GGTestActionForLocatorOnCall.js.map +1 -0
- package/dist/src/callOn/TestableIPC.d.ts +72 -0
- package/dist/src/callOn/TestableIPC.d.ts.map +1 -0
- package/dist/src/callOn/TestableIPC.js +34 -0
- package/dist/src/callOn/TestableIPC.js.map +1 -0
- package/dist/src/callOn/callOn.d.ts +113 -0
- package/dist/src/callOn/callOn.d.ts.map +1 -0
- package/dist/src/callOn/callOn.js +122 -0
- package/dist/src/callOn/callOn.js.map +1 -0
- package/dist/src/callOn/registerOnCallHandler.d.ts +13 -0
- package/dist/src/callOn/registerOnCallHandler.d.ts.map +1 -0
- package/dist/src/callOn/registerOnCallHandler.js +111 -0
- package/dist/src/callOn/registerOnCallHandler.js.map +1 -0
- package/dist/src/index-node.d.ts +35 -0
- package/dist/src/index-node.d.ts.map +1 -0
- package/dist/src/index-node.js +50 -0
- package/dist/src/index-node.js.map +1 -0
- package/dist/src/mockable/GGMockable.d.ts +19 -0
- package/dist/src/mockable/GGMockable.d.ts.map +1 -0
- package/dist/src/mockable/GGMockable.js +2 -0
- package/dist/src/mockable/GGMockable.js.map +1 -0
- package/dist/src/mockable/GGMockableCall.d.ts +2 -0
- package/dist/src/mockable/GGMockableCall.d.ts.map +1 -0
- package/dist/src/mockable/GGMockableCall.js +41 -0
- package/dist/src/mockable/GGMockableCall.js.map +1 -0
- package/dist/src/mockable/GGMockableIPC.d.ts +17 -0
- package/dist/src/mockable/GGMockableIPC.d.ts.map +1 -0
- package/dist/src/mockable/GGMockableIPC.js +8 -0
- package/dist/src/mockable/GGMockableIPC.js.map +1 -0
- package/dist/src/mockable/GGMockableInterceptor.d.ts +24 -0
- package/dist/src/mockable/GGMockableInterceptor.d.ts.map +1 -0
- package/dist/src/mockable/GGMockableInterceptor.js +32 -0
- package/dist/src/mockable/GGMockableInterceptor.js.map +1 -0
- package/dist/src/mockable/GGMockableInterceptorsServer.d.ts +12 -0
- package/dist/src/mockable/GGMockableInterceptorsServer.d.ts.map +1 -0
- package/dist/src/mockable/GGMockableInterceptorsServer.js +55 -0
- package/dist/src/mockable/GGMockableInterceptorsServer.js.map +1 -0
- package/dist/src/mockable/mockable.d.ts +46 -0
- package/dist/src/mockable/mockable.d.ts.map +1 -0
- package/dist/src/mockable/mockable.js +47 -0
- package/dist/src/mockable/mockable.js.map +1 -0
- package/dist/src/runner/InlineRunner.d.ts +12 -0
- package/dist/src/runner/InlineRunner.d.ts.map +1 -0
- package/dist/src/runner/InlineRunner.js +42 -0
- package/dist/src/runner/InlineRunner.js.map +1 -0
- package/dist/src/runner/IsolatedRunner.d.ts +17 -0
- package/dist/src/runner/IsolatedRunner.d.ts.map +1 -0
- package/dist/src/runner/IsolatedRunner.js +155 -0
- package/dist/src/runner/IsolatedRunner.js.map +1 -0
- package/dist/src/runner/RuntimeRunner.d.ts +14 -0
- package/dist/src/runner/RuntimeRunner.d.ts.map +1 -0
- package/dist/src/runner/RuntimeRunner.js +2 -0
- package/dist/src/runner/RuntimeRunner.js.map +1 -0
- package/dist/src/runner/WorkerRunner.d.ts +17 -0
- package/dist/src/runner/WorkerRunner.d.ts.map +1 -0
- package/dist/src/runner/WorkerRunner.js +155 -0
- package/dist/src/runner/WorkerRunner.js.map +1 -0
- package/dist/src/runner/isolated-loader.mjs +91 -0
- package/dist/src/runner/worker-loader.mjs +49 -0
- package/dist/src/testers/GGCallInterceptor.d.ts +71 -0
- package/dist/src/testers/GGCallInterceptor.d.ts.map +1 -0
- package/dist/src/testers/GGCallInterceptor.js +170 -0
- package/dist/src/testers/GGCallInterceptor.js.map +1 -0
- package/dist/src/testers/GGMockWith.d.ts +30 -0
- package/dist/src/testers/GGMockWith.d.ts.map +1 -0
- package/dist/src/testers/GGMockWith.js +70 -0
- package/dist/src/testers/GGMockWith.js.map +1 -0
- package/dist/src/testers/GGSpyWith.d.ts +40 -0
- package/dist/src/testers/GGSpyWith.d.ts.map +1 -0
- package/dist/src/testers/GGSpyWith.js +90 -0
- package/dist/src/testers/GGSpyWith.js.map +1 -0
- package/dist/src/testers/GGTestAction.d.ts +126 -0
- package/dist/src/testers/GGTestAction.d.ts.map +1 -0
- package/dist/src/testers/GGTestAction.js +245 -0
- package/dist/src/testers/GGTestAction.js.map +1 -0
- package/dist/src/testers/GGTestComponent.d.ts +15 -0
- package/dist/src/testers/GGTestComponent.d.ts.map +1 -0
- package/dist/src/testers/GGTestComponent.js +2 -0
- package/dist/src/testers/GGTestComponent.js.map +1 -0
- package/dist/src/testers/GGTestSelector.d.ts +54 -0
- package/dist/src/testers/GGTestSelector.d.ts.map +1 -0
- package/dist/src/testers/GGTestSelector.js +179 -0
- package/dist/src/testers/GGTestSelector.js.map +1 -0
- package/dist/src/testers/IGGTestInterceptor.d.ts +8 -0
- package/dist/src/testers/IGGTestInterceptor.d.ts.map +1 -0
- package/dist/src/testers/IGGTestInterceptor.js +2 -0
- package/dist/src/testers/IGGTestInterceptor.js.map +1 -0
- package/dist/src/testers/IGGTestWith.d.ts +13 -0
- package/dist/src/testers/IGGTestWith.d.ts.map +1 -0
- package/dist/src/testers/IGGTestWith.js +2 -0
- package/dist/src/testers/IGGTestWith.js.map +1 -0
- package/dist/src/testers/RuntimeSelector.d.ts +117 -0
- package/dist/src/testers/RuntimeSelector.d.ts.map +1 -0
- package/dist/src/testers/RuntimeSelector.js +2 -0
- package/dist/src/testers/RuntimeSelector.js.map +1 -0
- package/dist/src/tsconfig.json +17 -0
- package/dist/src/utils/GGExpectations.d.ts +18 -0
- package/dist/src/utils/GGExpectations.d.ts.map +1 -0
- package/dist/src/utils/GGExpectations.js +59 -0
- package/dist/src/utils/GGExpectations.js.map +1 -0
- package/dist/src/utils/GGTestError.d.ts +13 -0
- package/dist/src/utils/GGTestError.d.ts.map +1 -0
- package/dist/src/utils/GGTestError.js +26 -0
- package/dist/src/utils/GGTestError.js.map +1 -0
- package/dist/src/utils/captureStack.d.ts +9 -0
- package/dist/src/utils/captureStack.d.ts.map +1 -0
- package/dist/src/utils/captureStack.js +51 -0
- package/dist/src/utils/captureStack.js.map +1 -0
- package/dist/tsconfig.publish.tsbuildinfo +1 -0
- package/package.json +66 -0
- package/src/GGBundleTest.ts +89 -0
- package/src/GGTest.ts +318 -0
- package/src/GGTestContext.ts +74 -0
- package/src/GGTestRunner.ts +308 -0
- package/src/GGTestRuntime.ts +265 -0
- package/src/GGTestRuntimeWorker.ts +159 -0
- package/src/GGTestSharedRef.ts +116 -0
- package/src/GGTestkitExtensionsDiscovery.ts +26 -0
- package/src/IGGLocalDiscoveryServer.ts +16 -0
- package/src/callOn/GGCallOnSelector.ts +61 -0
- package/src/callOn/GGContractClass.implement.ts +43 -0
- package/src/callOn/GGTestActionForLocatorOnCall.ts +134 -0
- package/src/callOn/TestableIPC.ts +81 -0
- package/src/callOn/callOn.ts +224 -0
- package/src/callOn/registerOnCallHandler.ts +123 -0
- package/src/index-node.ts +64 -0
- package/src/mockable/GGMockable.ts +22 -0
- package/src/mockable/GGMockableCall.ts +45 -0
- package/src/mockable/GGMockableIPC.ts +20 -0
- package/src/mockable/GGMockableInterceptor.ts +44 -0
- package/src/mockable/GGMockableInterceptorsServer.ts +69 -0
- package/src/mockable/mockable.ts +71 -0
- package/src/runner/InlineRunner.ts +47 -0
- package/src/runner/IsolatedRunner.ts +179 -0
- package/src/runner/RuntimeRunner.ts +15 -0
- package/src/runner/WorkerRunner.ts +179 -0
- package/src/runner/isolated-loader.mjs +91 -0
- package/src/runner/worker-loader.mjs +49 -0
- package/src/testers/GGCallInterceptor.ts +224 -0
- package/src/testers/GGMockWith.ts +92 -0
- package/src/testers/GGSpyWith.ts +115 -0
- package/src/testers/GGTestAction.ts +333 -0
- package/src/testers/GGTestComponent.ts +16 -0
- package/src/testers/GGTestSelector.ts +223 -0
- package/src/testers/IGGTestInterceptor.ts +11 -0
- package/src/testers/IGGTestWith.ts +15 -0
- package/src/testers/RuntimeSelector.ts +151 -0
- package/src/tsconfig.json +17 -0
- package/src/utils/GGExpectations.ts +78 -0
- package/src/utils/GGTestError.ts +37 -0
- package/src/utils/captureStack.ts +54 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified callOn function for invoking methods on runtime instances.
|
|
3
|
+
*
|
|
4
|
+
* Works with:
|
|
5
|
+
* - @testable classes: Direct service invocation via IPC
|
|
6
|
+
* - GGLocatorKey: Direct instance lookup via IPC
|
|
7
|
+
* - GGContractClass: Contract implementation lookup via IPC
|
|
8
|
+
* - Contract holders (HTTP APIs, etc.): Delegated to their own factory
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Call @testable service
|
|
12
|
+
* await callOn(MyService).doSomething("hello")
|
|
13
|
+
*
|
|
14
|
+
* // Call HTTP API (goes through HTTP transport)
|
|
15
|
+
* await callOn(ChainApi).quickWeatherCheck({city: "NYC"})
|
|
16
|
+
*
|
|
17
|
+
* // Call contract directly (skips HTTP, uses IPC)
|
|
18
|
+
* await callOn(ChainApiContract).quickWeatherCheck({city: "NYC"})
|
|
19
|
+
*
|
|
20
|
+
* // With context
|
|
21
|
+
* await callOn(MyService, ctx).doSomething("hello")
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {GGLocatorKey} from "@grest-ts/locator";
|
|
25
|
+
import {GGTestActionForLocatorOnCall} from "./GGTestActionForLocatorOnCall";
|
|
26
|
+
import {LOCATOR_KEY_PREFIX_FOR_TESTABLE} from "@grest-ts/testkit-runtime";
|
|
27
|
+
import {GGContractApiDefinition, GGContractClass, GGContractMethod} from "@grest-ts/schema";
|
|
28
|
+
import {GGContext} from "@grest-ts/context";
|
|
29
|
+
import {LOCATOR_KEY_PREFIX_FOR_CONTRACT} from "./GGContractClass.implement";
|
|
30
|
+
import type {GGTestRuntime} from "../GGTestRuntime";
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Factory symbol for protocol-specific callOn support
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Symbol used by contract holders (HTTP APIs, WebSocket APIs, etc.) to provide
|
|
38
|
+
* their own callOn proxy. This allows protocols to control how calls are made
|
|
39
|
+
* without callOn needing to know about specific protocols.
|
|
40
|
+
*
|
|
41
|
+
* The factory can return anything - there's no constraint on the return type.
|
|
42
|
+
* Each protocol fully controls what methods and types are available.
|
|
43
|
+
*/
|
|
44
|
+
export const CALL_ON_FACTORY = Symbol.for("gg:callOnFactory");
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Marker interface for targets that provide their own callOn proxy.
|
|
48
|
+
* The factory can return any type - protocols have full control.
|
|
49
|
+
*/
|
|
50
|
+
export interface GGCallOnFactory {
|
|
51
|
+
[CALL_ON_FACTORY](ctx?: GGContext): unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Type definitions
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Maps a class's async methods to GGLocatorLookupTestAction calls.
|
|
60
|
+
* Used for @testable classes and GGLocatorKey lookups.
|
|
61
|
+
*/
|
|
62
|
+
export type LocatorLookupAccess<T> = {
|
|
63
|
+
[K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>
|
|
64
|
+
? (...args: A) => GGTestActionForLocatorOnCall<R>
|
|
65
|
+
: never
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Maps contract methods to GGLocatorLookupTestAction calls.
|
|
70
|
+
* Used for direct GGContractClass lookups via IPC.
|
|
71
|
+
*/
|
|
72
|
+
export type ContractLocatorAccess<TContract> = {
|
|
73
|
+
[K in keyof TContract]: TContract[K] extends GGContractMethod<infer Input, infer Output>
|
|
74
|
+
? Input extends undefined ? () => GGTestActionForLocatorOnCall<Output> : (data: Input) => GGTestActionForLocatorOnCall<Output>
|
|
75
|
+
: never
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type GGTestCallOnCollection<T> = { [K in keyof T]: GGTestCallOn<T[K]> }
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolves the return type for callOn(target):
|
|
82
|
+
* - Factory targets → extract factory's return type (protocol controls everything)
|
|
83
|
+
* - GGContractClass → ContractLocatorAccess (IPC)
|
|
84
|
+
* - Constructor/GGLocatorKey → LocatorLookupAccess (IPC)
|
|
85
|
+
* - Plain objects → recurse into properties
|
|
86
|
+
*/
|
|
87
|
+
export type GGTestCallOn<T> =
|
|
88
|
+
T extends { [CALL_ON_FACTORY]: (ctx?: GGContext) => infer R } ? R
|
|
89
|
+
: T extends GGContractClass<infer TContract> ? ContractLocatorAccess<TContract>
|
|
90
|
+
: T extends GGLocatorKey<infer Instance> ? LocatorLookupAccess<Instance>
|
|
91
|
+
: T extends new (...args: any[]) => infer Instance ? LocatorLookupAccess<Instance>
|
|
92
|
+
: T extends object ? { [K in keyof T]: GGTestCallOn<T[K]> }
|
|
93
|
+
: never;
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// The callOn function
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if target implements the CALL_ON_FACTORY interface.
|
|
101
|
+
*/
|
|
102
|
+
function hasCallOnFactory(target: unknown): target is GGCallOnFactory {
|
|
103
|
+
return target != null && typeof (target as any)[CALL_ON_FACTORY] === 'function';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if target is a simple class (constructor function).
|
|
108
|
+
*/
|
|
109
|
+
function isSimpleClass(value: unknown): value is new (...args: any[]) => any {
|
|
110
|
+
return typeof value === 'function' && value.prototype !== undefined && value.prototype.constructor === value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Invoke methods on runtime instances via a unified interface.
|
|
115
|
+
*
|
|
116
|
+
* Resolution order:
|
|
117
|
+
* 1. Factory symbol (HTTP APIs, WebSocket APIs provide their own)
|
|
118
|
+
* 2. GGContractClass → IPC lookup via @contract: prefix
|
|
119
|
+
* 3. GGLocatorKey → IPC lookup via key name
|
|
120
|
+
* 4. Simple class → IPC lookup via @testable: prefix
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* await callOn(MyService, ctx).doSomething("hello")
|
|
124
|
+
* await callOn(ChainApi, ctx).quickWeatherCheck({city: "NYC"})
|
|
125
|
+
* await callOn(ChainApiContract, ctx).quickWeatherCheck({city: "NYC"})
|
|
126
|
+
*/
|
|
127
|
+
export function callOn<T extends GGCallOnFactory>(target: T, ctx?: GGContext): GGTestCallOn<T>;
|
|
128
|
+
export function callOn<T extends GGContractApiDefinition>(target: GGContractClass<T>, ctx?: GGContext): ContractLocatorAccess<T>;
|
|
129
|
+
export function callOn<T>(target: GGLocatorKey<T>, ctx?: GGContext): LocatorLookupAccess<T>;
|
|
130
|
+
export function callOn<T extends new (...args: any[]) => any>(target: T, ctx?: GGContext): LocatorLookupAccess<InstanceType<T>>;
|
|
131
|
+
export function callOn<T extends object>(target: T, ctx?: GGContext): GGTestCallOn<T>;
|
|
132
|
+
export function callOn(target: any, ctx?: GGContext): any {
|
|
133
|
+
ctx ??= new GGContext("Test")
|
|
134
|
+
|
|
135
|
+
if (hasCallOnFactory(target)) {
|
|
136
|
+
return target[CALL_ON_FACTORY](ctx);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const keyName = resolveKeyName(target);
|
|
140
|
+
return createCallOnProxy(keyName, ctx);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// Helper functions for key resolution
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolve a target to its locator key name.
|
|
149
|
+
* Handles GGContractClass, GGLocatorKey, and simple classes.
|
|
150
|
+
*/
|
|
151
|
+
function resolveKeyName(target: any): string {
|
|
152
|
+
if (target instanceof GGContractClass) {
|
|
153
|
+
return LOCATOR_KEY_PREFIX_FOR_CONTRACT + target.name;
|
|
154
|
+
}
|
|
155
|
+
if (target instanceof GGLocatorKey) {
|
|
156
|
+
return target.name;
|
|
157
|
+
}
|
|
158
|
+
if (isSimpleClass(target)) {
|
|
159
|
+
return LOCATOR_KEY_PREFIX_FOR_TESTABLE + target.name;
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`Unknown callOn target: ${target?.name ?? target?.constructor?.name ?? typeof target}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a proxy that intercepts method calls and returns GGTestActionForLocatorOnCall.
|
|
166
|
+
*/
|
|
167
|
+
function createCallOnProxy(keyName: string, ctx: GGContext, targetRuntimes?: GGTestRuntime[]): any {
|
|
168
|
+
return new Proxy({}, {
|
|
169
|
+
get(_, methodName: string) {
|
|
170
|
+
return (...args: any[]) => new GGTestActionForLocatorOnCall<any>(ctx, keyName, methodName, args, targetRuntimes);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Targeted callOn - for explicit runtime selection
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Invoke methods on runtime instances, targeting specific runtimes.
|
|
181
|
+
* Use this when you need to explicitly select which runtime to call
|
|
182
|
+
* when multiple different runtime classes have the same service.
|
|
183
|
+
*
|
|
184
|
+
* @param target - The target to call (class, GGLocatorKey, or GGContractClass)
|
|
185
|
+
* @param runtimes - The specific runtimes to target
|
|
186
|
+
* @param ctx - Optional context for the call
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* const f = GGTest.startWorker({chain: ChainRuntime, weather: WeatherOnlyRuntime});
|
|
190
|
+
* // Both have WeatherService - explicitly target one:
|
|
191
|
+
* await f.chain.callOn(WeatherService).getWeather("Test")
|
|
192
|
+
*/
|
|
193
|
+
export function callOnTargeted<T>(target: T, runtimes: GGTestRuntime[], ctx?: GGContext): GGTestCallOn<T>;
|
|
194
|
+
export function callOnTargeted(target: any, runtimes: GGTestRuntime[], ctx?: GGContext): any {
|
|
195
|
+
ctx ??= new GGContext("Test");
|
|
196
|
+
|
|
197
|
+
// Factory targets don't support runtime targeting - they control their own routing
|
|
198
|
+
if (hasCallOnFactory(target)) {
|
|
199
|
+
return target[CALL_ON_FACTORY](ctx);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const keyName = resolveKeyName(target);
|
|
203
|
+
return createCallOnProxy(keyName, ctx, runtimes);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Process a collection of targets and create callOn proxies for each.
|
|
208
|
+
* It can be a recursive object where all "testable things" are replaced with callOn(X) handlers.
|
|
209
|
+
* {serviceA: ApiA, sub: { serviceB: ApiB}} -> {serviceA: callOn(ApiA), sub: { serviceB: callOn(ApiB)}}
|
|
210
|
+
*/
|
|
211
|
+
export function callOnCollection<T extends object | any[]>(target: T, ctx?: GGContext): GGTestCallOnCollection<T> {
|
|
212
|
+
const result: any = Array.isArray(target) ? [] : {};
|
|
213
|
+
for (const key in target) {
|
|
214
|
+
const value = target[key];
|
|
215
|
+
if (value != null) {
|
|
216
|
+
if (hasCallOnFactory(value) || value instanceof GGContractClass || value instanceof GGLocatorKey || isSimpleClass(value)) {
|
|
217
|
+
result[key] = callOn(value as any, ctx);
|
|
218
|
+
} else if (typeof value === 'object') {
|
|
219
|
+
result[key] = callOnCollection(value, ctx);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-side handler for GGLocator-based service invocation.
|
|
3
|
+
*
|
|
4
|
+
* Receives IPC requests from tests to invoke methods on registered instances
|
|
5
|
+
* (@testable, @contract, etc.) and returns the results.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {GGLocator, GGLocatorKey} from "@grest-ts/locator";
|
|
9
|
+
import {GGLog} from "@grest-ts/logger";
|
|
10
|
+
import {SerializedContext, TestableIPC, TestableInvokePayload, TestableInvokeResult} from "./TestableIPC";
|
|
11
|
+
import {GGTestRuntimeWorker} from "../GGTestRuntimeWorker";
|
|
12
|
+
import {GGContext} from "@grest-ts/context";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Context for logging
|
|
16
|
+
*/
|
|
17
|
+
const LOG_CONTEXT = {name: "GGLocatorWorkerHandler"};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deserialize context data into a GGContext.
|
|
21
|
+
* Directly populates the internal values map.
|
|
22
|
+
*/
|
|
23
|
+
function deserializeContext(data: SerializedContext): GGContext {
|
|
24
|
+
const ctx = new GGContext("ipc-context");
|
|
25
|
+
// Access internal values map and populate directly
|
|
26
|
+
const ctxAny = ctx as any;
|
|
27
|
+
for (const [keyName, value] of Object.entries(data)) {
|
|
28
|
+
ctxAny.values.set(keyName, value);
|
|
29
|
+
}
|
|
30
|
+
return ctx;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register the locator lookup handler on the worker.
|
|
35
|
+
* Called during worker initialization.
|
|
36
|
+
*/
|
|
37
|
+
export function registerOnCallHandler(worker: GGTestRuntimeWorker): void {
|
|
38
|
+
worker.onIpcRequest(TestableIPC.invoke, async (payload: TestableInvokePayload): Promise<TestableInvokeResult> => {
|
|
39
|
+
const {keyName, methodName, args, context} = payload;
|
|
40
|
+
|
|
41
|
+
GGLog.debug(LOG_CONTEXT, `Invoking ${keyName}.${methodName}`, {args, hasContext: !!context});
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Look up the instance in the current scope
|
|
45
|
+
const scope = GGLocator.tryGetScope();
|
|
46
|
+
if (!scope) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: `No GGLocator scope available - is the runtime running?`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const key = new GGLocatorKey<any>(keyName);
|
|
54
|
+
const instance = scope.tryGet(key);
|
|
55
|
+
|
|
56
|
+
if (!instance) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: `Instance '${keyName}' not found in GGLocator. ` +
|
|
60
|
+
`Make sure the class is decorated with @testable or registered during compose().`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if method exists
|
|
65
|
+
const method = instance[methodName];
|
|
66
|
+
if (typeof method !== 'function') {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
error: `Method '${methodName}' not found on instance '${keyName}'. ` +
|
|
70
|
+
`Available methods: ${getMethodNames(instance).join(', ')}`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Invoke the method, optionally within the provided context
|
|
75
|
+
let result: any;
|
|
76
|
+
if (context) {
|
|
77
|
+
const ctx = deserializeContext(context);
|
|
78
|
+
result = await ctx.run(() => method.apply(instance, args));
|
|
79
|
+
} else {
|
|
80
|
+
result = await method.apply(instance, args);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
GGLog.debug(LOG_CONTEXT, `Invocation ${keyName}.${methodName} completed`, {result});
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
result
|
|
88
|
+
};
|
|
89
|
+
} catch (error: any) {
|
|
90
|
+
GGLog.error(LOG_CONTEXT, `Error invoking ${keyName}.${methodName}`, error);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: error.message || String(error),
|
|
95
|
+
stack: error.stack
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get method names from an instance for error messages.
|
|
103
|
+
*/
|
|
104
|
+
function getMethodNames(instance: any): string[] {
|
|
105
|
+
const names: string[] = [];
|
|
106
|
+
let proto = Object.getPrototypeOf(instance);
|
|
107
|
+
|
|
108
|
+
while (proto && proto !== Object.prototype) {
|
|
109
|
+
const propNames = Object.getOwnPropertyNames(proto)
|
|
110
|
+
.filter(name => {
|
|
111
|
+
if (name === 'constructor') return false;
|
|
112
|
+
try {
|
|
113
|
+
return typeof proto[name] === 'function';
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
names.push(...propNames);
|
|
119
|
+
proto = Object.getPrototypeOf(proto);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return [...new Set(names)].sort();
|
|
123
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @grest-ts/testkit - Component testing library
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Core test framework
|
|
6
|
+
import path from "path";
|
|
7
|
+
import {fileURLToPath} from "url";
|
|
8
|
+
import {WorkerRunner} from "./runner/WorkerRunner";
|
|
9
|
+
import {IsolatedRunner} from "./runner/IsolatedRunner";
|
|
10
|
+
|
|
11
|
+
export * from './GGTest'
|
|
12
|
+
export * from './GGTestRunner'
|
|
13
|
+
export * from './IGGLocalDiscoveryServer'
|
|
14
|
+
export * from './GGTestRuntime'
|
|
15
|
+
export * from './testers/GGTestComponent'
|
|
16
|
+
export * from './mockable/GGMockable'
|
|
17
|
+
export * from './mockable/GGMockableCall'
|
|
18
|
+
export * from './mockable/mockable'
|
|
19
|
+
export * from './GGTestContext'
|
|
20
|
+
|
|
21
|
+
// Mockable component server (import triggers factory registration)
|
|
22
|
+
export * from './mockable/GGMockableInterceptorsServer'
|
|
23
|
+
|
|
24
|
+
// Testable - direct service invocation from tests
|
|
25
|
+
export * from './callOn/callOn'
|
|
26
|
+
export * from './callOn/GGTestActionForLocatorOnCall'
|
|
27
|
+
export * from './callOn/GGCallOnSelector'
|
|
28
|
+
|
|
29
|
+
// Contract registration - patches GGContractClass.implement() to auto-register
|
|
30
|
+
import './callOn/GGContractClass.implement'
|
|
31
|
+
|
|
32
|
+
// Control channel for runtime config updates
|
|
33
|
+
export * from './GGTestRuntimeWorker'
|
|
34
|
+
|
|
35
|
+
// Test utilities - core infrastructure
|
|
36
|
+
export * from './testers/IGGTestWith'
|
|
37
|
+
export * from './testers/GGTestAction'
|
|
38
|
+
export * from './testers/GGMockWith'
|
|
39
|
+
export * from './testers/GGSpyWith'
|
|
40
|
+
export * from './testers/GGCallInterceptor'
|
|
41
|
+
export * from './utils/GGExpectations'
|
|
42
|
+
export * from './utils/GGTestError'
|
|
43
|
+
export {captureStackSourceFile} from './utils/captureStack'
|
|
44
|
+
export * from './GGTestSharedRef'
|
|
45
|
+
|
|
46
|
+
// Production bundle DCE verification
|
|
47
|
+
export * from './GGBundleTest'
|
|
48
|
+
|
|
49
|
+
// Mockable interceptor
|
|
50
|
+
export * from './mockable/GGMockableInterceptor'
|
|
51
|
+
|
|
52
|
+
// Selector system for runtime access
|
|
53
|
+
export * from './testers/GGTestSelector'
|
|
54
|
+
export * from './testers/RuntimeSelector'
|
|
55
|
+
|
|
56
|
+
// Worker runner (path configured by @grest-ts/testkit-vitest)
|
|
57
|
+
export * from './runner/WorkerRunner'
|
|
58
|
+
|
|
59
|
+
export type * from "./testers/IGGTestWith";
|
|
60
|
+
export type * from "./testers/IGGTestInterceptor";
|
|
61
|
+
|
|
62
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
WorkerRunner.setWorkerLoaderPath(path.join(__dirname, 'runner', 'worker-loader.mjs'));
|
|
64
|
+
IsolatedRunner.setIsolatedLoaderPath(path.join(__dirname, 'runner', 'isolated-loader.mjs'));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker interface for external API services that can be mocked in tests.
|
|
3
|
+
* When a class implements this interface, the test framework can intercept
|
|
4
|
+
* all method calls and provide mock responses.
|
|
5
|
+
*
|
|
6
|
+
* Examples: REST APIs, GraphQL clients, third-party SDKs
|
|
7
|
+
*/
|
|
8
|
+
export interface GGMockableExternalApi {
|
|
9
|
+
// Marker interface - no methods required
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Marker interface for database/storage services that can be mocked in tests.
|
|
14
|
+
* When a class implements this interface, the test framework can intercept
|
|
15
|
+
* all method calls and provide mock responses.
|
|
16
|
+
*
|
|
17
|
+
* Examples: Database clients, cache services, file storage
|
|
18
|
+
*/
|
|
19
|
+
export interface GGMockableDatabaseApi {
|
|
20
|
+
// Marker interface - no methods required
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {GG_TEST_RUNTIME_WORKER} from "../GGTestRuntimeWorker";
|
|
2
|
+
import {CALL_THROUGH} from "./GGMockableInterceptorsServer";
|
|
3
|
+
import {GGMockableIPC} from "./GGMockableIPC";
|
|
4
|
+
|
|
5
|
+
const MOCKABLE_WRAPPED = Symbol('GGMockableWrapped');
|
|
6
|
+
|
|
7
|
+
export function GGMockableCall(cls: any, methodName: string, nameMapping: string[]): void {
|
|
8
|
+
// Skip if already wrapped (happens in INLINE mode with multiple runtime instances)
|
|
9
|
+
if (cls.prototype[methodName]?.[MOCKABLE_WRAPPED]) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const originalMethod = cls.prototype[methodName];
|
|
14
|
+
const wrappedMethod = async function (this: any, ...inputArgs: any[]) {
|
|
15
|
+
const worker = GG_TEST_RUNTIME_WORKER.get();
|
|
16
|
+
|
|
17
|
+
const args: any = {}
|
|
18
|
+
for (let i = 0; i < inputArgs.length; i++) {
|
|
19
|
+
if (!nameMapping[i]) break;
|
|
20
|
+
args[nameMapping[i]] = inputArgs[i];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = await worker.ipcClient.sendFrameworkRequest(GGMockableIPC.testServer.call, {
|
|
24
|
+
className: cls.name,
|
|
25
|
+
methodName: methodName,
|
|
26
|
+
callArgs: args
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (result === CALL_THROUGH) {
|
|
30
|
+
const realResult = await originalMethod.apply(this, inputArgs);
|
|
31
|
+
await worker.ipcClient.sendFrameworkRequest(GGMockableIPC.testServer.spyResult, {
|
|
32
|
+
className: cls.name,
|
|
33
|
+
methodName: methodName,
|
|
34
|
+
callResult: realResult
|
|
35
|
+
});
|
|
36
|
+
return realResult;
|
|
37
|
+
} else {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Mark as wrapped and assign to prototype
|
|
43
|
+
(wrappedMethod as any)[MOCKABLE_WRAPPED] = true;
|
|
44
|
+
cls.prototype[methodName] = wrappedMethod;
|
|
45
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {IPCServer} from "@grest-ts/ipc";
|
|
2
|
+
|
|
3
|
+
export interface MockableCallPayload {
|
|
4
|
+
className: string;
|
|
5
|
+
methodName: string;
|
|
6
|
+
callArgs: any;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MockableSpyResultPayload {
|
|
10
|
+
className: string;
|
|
11
|
+
methodName: string;
|
|
12
|
+
callResult: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GGMockableIPC = {
|
|
16
|
+
testServer: {
|
|
17
|
+
call: IPCServer.defineRequest<MockableCallPayload, any>("mockable/call"),
|
|
18
|
+
spyResult: IPCServer.defineRequest<MockableSpyResultPayload, any>("mockable/spy-result"),
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {GGTestRunner} from "../GGTestRunner";
|
|
2
|
+
import {GGCallInterceptor, GGCallInterceptorConfig} from "../testers/GGCallInterceptor";
|
|
3
|
+
import {GGMockableInterceptorsServer} from "./GGMockableInterceptorsServer";
|
|
4
|
+
|
|
5
|
+
export interface MockableInterceptorConfig extends GGCallInterceptorConfig {
|
|
6
|
+
className: string;
|
|
7
|
+
methodName: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Interceptor for mocking/spying on mockable class methods.
|
|
12
|
+
* Registers with GGMockableInterceptorsServer which handles the IPC communication.
|
|
13
|
+
*
|
|
14
|
+
* For spy mode, the flow is two-phase:
|
|
15
|
+
* 1. mockable/call → onRequest() validates input, returns undefined (CALL_THROUGH)
|
|
16
|
+
* 2. mockable/spy-result → onResponse() validates output
|
|
17
|
+
*/
|
|
18
|
+
export class GGMockableInterceptor extends GGCallInterceptor {
|
|
19
|
+
|
|
20
|
+
public readonly className: string;
|
|
21
|
+
public readonly methodName: string;
|
|
22
|
+
|
|
23
|
+
constructor(test: GGTestRunner, config: MockableInterceptorConfig) {
|
|
24
|
+
super(test, config);
|
|
25
|
+
this.className = config.className;
|
|
26
|
+
this.methodName = config.methodName;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public getKey(): string {
|
|
30
|
+
return `${this.className}.${this.methodName}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected doRegister(): void {
|
|
34
|
+
this.test.getExtensionInstance(GGMockableInterceptorsServer).addInterceptor(this);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected doUnregister(): void {
|
|
38
|
+
this.test.getExtensionInstance(GGMockableInterceptorsServer).deleteInterceptor(this);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected parseResponseData(result: any): any {
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type {GGMockableInterceptor} from "./GGMockableInterceptor";
|
|
2
|
+
import {GGTestComponent} from "../testers/GGTestComponent";
|
|
3
|
+
import {GGTestRunner} from "../GGTestRunner";
|
|
4
|
+
import {GGMockableIPC} from "./GGMockableIPC";
|
|
5
|
+
|
|
6
|
+
export const CALL_THROUGH = "__spyCallThrough|migo0am5g0htea";
|
|
7
|
+
|
|
8
|
+
export class GGMockableInterceptorsServer implements GGTestComponent {
|
|
9
|
+
|
|
10
|
+
private readonly interceptors: Map<string, GGMockableInterceptor> = new Map();
|
|
11
|
+
|
|
12
|
+
constructor(runner: GGTestRunner) {
|
|
13
|
+
const server = runner.ipcServer;
|
|
14
|
+
server.onFrameworkMessage(GGMockableIPC.testServer.call, async (body) => {
|
|
15
|
+
const key = body.className + "." + body.methodName;
|
|
16
|
+
const handler = this.interceptors.get(key);
|
|
17
|
+
|
|
18
|
+
if (!handler) {
|
|
19
|
+
// No mock configured - call through to real implementation
|
|
20
|
+
// This allows testable() to invoke real methods on @mockable services
|
|
21
|
+
// throw new Error(
|
|
22
|
+
// `Expected handler to be set for mockable '${key}'!\n` +
|
|
23
|
+
// "Did you forget to call .with(...)?"
|
|
24
|
+
// );
|
|
25
|
+
return CALL_THROUGH;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// onRequest validates input and returns mock data (or undefined for spy)
|
|
29
|
+
const result = await handler.onRequest(body.callArgs);
|
|
30
|
+
|
|
31
|
+
if (handler.passThrough) {
|
|
32
|
+
// Spy mode - signal worker to call through
|
|
33
|
+
return CALL_THROUGH;
|
|
34
|
+
} else {
|
|
35
|
+
// Mock mode - return the mock data
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
server.onFrameworkMessage(GGMockableIPC.testServer.spyResult, async (body) => {
|
|
41
|
+
const key = body.className + "." + body.methodName;
|
|
42
|
+
const handler = this.interceptors.get(key);
|
|
43
|
+
|
|
44
|
+
if (!handler || !handler.passThrough) {
|
|
45
|
+
// No spy handler configured - just ignore the result
|
|
46
|
+
// This allows testable() to call through without requiring spyOn()
|
|
47
|
+
// throw new Error(`Expected spy handler for '${key}'`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Validate the response from the real implementation
|
|
52
|
+
await handler.onResponse(body.callResult);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public addInterceptor(interceptor: GGMockableInterceptor) {
|
|
57
|
+
this.interceptors.set(interceptor.getKey(), interceptor);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public deleteInterceptor(interceptor: GGMockableInterceptor) {
|
|
61
|
+
this.interceptors.delete(interceptor.getKey());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public async teardown(): Promise<void> {
|
|
65
|
+
this.interceptors.clear();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
GGTestRunner.registerExtension(GGMockableInterceptorsServer);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mockable decorator and test helpers.
|
|
3
|
+
*
|
|
4
|
+
* The @mockable decorator is re-exported from mockable-runtime.ts for production use.
|
|
5
|
+
* mockBy() and spyOn() are test-only utilities that require the full testkit.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Test-only imports - these pull in testkit infrastructure
|
|
9
|
+
import {GGMockableInterceptor} from "./GGMockableInterceptor";
|
|
10
|
+
import {GGMockWith} from "../testers/GGMockWith";
|
|
11
|
+
import {GGSpyWith} from "../testers/GGSpyWith";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Type helpers for mockBy/spyOn
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Type for mock access - maps class methods to GGMockWith
|
|
19
|
+
*/
|
|
20
|
+
type MockAccess<T> = {
|
|
21
|
+
[K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>
|
|
22
|
+
? GGMockWith<A extends [infer Single] ? (Single extends object ? Single : Record<string, Single>) : Record<string, any>, Awaited<R>, never>
|
|
23
|
+
: never
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Type for spy access - maps class methods to GGSpyWith
|
|
28
|
+
*/
|
|
29
|
+
type SpyAccess<T> = {
|
|
30
|
+
[K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>
|
|
31
|
+
? GGSpyWith<A extends [infer Single] ? (Single extends object ? Single : Record<string, Single>) : Record<string, any>, Awaited<R>, never>
|
|
32
|
+
: never
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get mock access for a @mockable class.
|
|
37
|
+
* Use this to create mock expectations in tests.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* .with(mockOf(AddressResolverService).resolveAddress
|
|
42
|
+
* .toEqual({address: "123 Main St"})
|
|
43
|
+
* .andReturn({lat: 40.7, lng: -74.0}))
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function mockOf<T>(cls: new (...args: any[]) => T): MockAccess<T> {
|
|
47
|
+
return new Proxy({} as any, {
|
|
48
|
+
get(_, methodName: string) {
|
|
49
|
+
return new GGMockWith(GGMockableInterceptor, {className: cls.name, methodName})
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get spy access for a @mockable class.
|
|
56
|
+
* Use this to create spy expectations in tests - the real method will be called.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* .with(spyOn(AddressResolverService).resolveAddress
|
|
61
|
+
* .toEqual({address: "123 Main St"})
|
|
62
|
+
* .responseToMatchObject({lat: 40.7}))
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function spyOn<T>(cls: new (...args: any[]) => T): SpyAccess<T> {
|
|
66
|
+
return new Proxy({} as any, {
|
|
67
|
+
get(_, methodName: string) {
|
|
68
|
+
return new GGSpyWith(GGMockableInterceptor, {className: cls.name, methodName})
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|