@grest-ts/testkit 0.0.5 → 0.0.7
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 -21
- package/README.md +418 -413
- package/dist/src/runner/isolated-loader.mjs +91 -91
- package/dist/src/runner/worker-loader.mjs +49 -49
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +12 -12
- package/src/GGBundleTest.ts +89 -89
- package/src/GGTest.ts +318 -318
- package/src/GGTestContext.ts +74 -74
- package/src/GGTestRunner.ts +308 -308
- package/src/GGTestRuntime.ts +265 -265
- package/src/GGTestRuntimeWorker.ts +159 -159
- package/src/GGTestSharedRef.ts +116 -116
- package/src/GGTestkitExtensionsDiscovery.ts +26 -26
- package/src/IGGLocalDiscoveryServer.ts +16 -16
- package/src/callOn/GGCallOnSelector.ts +61 -61
- package/src/callOn/GGContractClass.implement.ts +43 -43
- package/src/callOn/GGTestActionForLocatorOnCall.ts +134 -134
- package/src/callOn/TestableIPC.ts +81 -81
- package/src/callOn/callOn.ts +224 -224
- package/src/callOn/registerOnCallHandler.ts +123 -123
- package/src/index-node.ts +64 -64
- package/src/mockable/GGMockable.ts +22 -22
- package/src/mockable/GGMockableCall.ts +45 -45
- package/src/mockable/GGMockableIPC.ts +20 -20
- package/src/mockable/GGMockableInterceptor.ts +44 -44
- package/src/mockable/GGMockableInterceptorsServer.ts +69 -69
- package/src/mockable/mockable.ts +71 -71
- package/src/runner/InlineRunner.ts +47 -47
- package/src/runner/IsolatedRunner.ts +179 -179
- package/src/runner/RuntimeRunner.ts +15 -15
- package/src/runner/WorkerRunner.ts +179 -179
- package/src/runner/isolated-loader.mjs +91 -91
- package/src/runner/worker-loader.mjs +49 -49
- package/src/testers/GGCallInterceptor.ts +224 -224
- package/src/testers/GGMockWith.ts +92 -92
- package/src/testers/GGSpyWith.ts +115 -115
- package/src/testers/GGTestAction.ts +332 -332
- package/src/testers/GGTestComponent.ts +16 -16
- package/src/testers/GGTestSelector.ts +223 -223
- package/src/testers/IGGTestInterceptor.ts +10 -10
- package/src/testers/IGGTestWith.ts +15 -15
- package/src/testers/RuntimeSelector.ts +151 -151
- package/src/utils/GGExpectations.ts +78 -78
- package/src/utils/GGTestError.ts +36 -36
- package/src/utils/captureStack.ts +53 -53
|
@@ -1,69 +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);
|
|
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);
|
package/src/mockable/mockable.ts
CHANGED
|
@@ -1,71 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import type {RuntimeRunner} from "./RuntimeRunner";
|
|
2
|
-
import {GGTestRuntimeWorker} from "../GGTestRuntimeWorker";
|
|
3
|
-
import {GGTestEnvConfig} from "../GGTestRuntime";
|
|
4
|
-
import {GGLocatorScope} from "@grest-ts/locator";
|
|
5
|
-
|
|
6
|
-
export class InlineRunner implements RuntimeRunner {
|
|
7
|
-
|
|
8
|
-
private controlClient?: GGTestRuntimeWorker;
|
|
9
|
-
|
|
10
|
-
constructor(
|
|
11
|
-
private readonly config: GGTestEnvConfig,
|
|
12
|
-
private readonly runtimeFactory?: () => any
|
|
13
|
-
) {
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async start(): Promise<void> {
|
|
17
|
-
// Use setTimeout + enterBlank() to create a fully isolated async context.
|
|
18
|
-
// This gives the inline runtime its own context tree where GGLog.init()
|
|
19
|
-
// creates an independent log context, similar to how Worker/Isolated modes
|
|
20
|
-
// naturally get separate contexts by running in different threads/processes.
|
|
21
|
-
const factory = this.runtimeFactory;
|
|
22
|
-
await new Promise<void>((resolve, reject) => {
|
|
23
|
-
setTimeout(async () => {
|
|
24
|
-
try {
|
|
25
|
-
// Create blank context - inline runtime should not inherit test context
|
|
26
|
-
new GGLocatorScope("GGInlineRunner").enter();
|
|
27
|
-
this.controlClient = new GGTestRuntimeWorker(this.config);
|
|
28
|
-
// Pass factory to avoid dynamic import() which causes duplicate
|
|
29
|
-
// module loading in Vite/vitest environments
|
|
30
|
-
await this.controlClient.start(factory);
|
|
31
|
-
resolve();
|
|
32
|
-
} catch (err) {
|
|
33
|
-
reject(err);
|
|
34
|
-
}
|
|
35
|
-
}, 0);
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async stopRuntime(): Promise<void> {
|
|
40
|
-
await this.controlClient?.stopRuntime();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async shutdown(): Promise<void> {
|
|
44
|
-
await this.controlClient?.shutdown();
|
|
45
|
-
this.controlClient = undefined;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
import type {RuntimeRunner} from "./RuntimeRunner";
|
|
2
|
+
import {GGTestRuntimeWorker} from "../GGTestRuntimeWorker";
|
|
3
|
+
import {GGTestEnvConfig} from "../GGTestRuntime";
|
|
4
|
+
import {GGLocatorScope} from "@grest-ts/locator";
|
|
5
|
+
|
|
6
|
+
export class InlineRunner implements RuntimeRunner {
|
|
7
|
+
|
|
8
|
+
private controlClient?: GGTestRuntimeWorker;
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly config: GGTestEnvConfig,
|
|
12
|
+
private readonly runtimeFactory?: () => any
|
|
13
|
+
) {
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async start(): Promise<void> {
|
|
17
|
+
// Use setTimeout + enterBlank() to create a fully isolated async context.
|
|
18
|
+
// This gives the inline runtime its own context tree where GGLog.init()
|
|
19
|
+
// creates an independent log context, similar to how Worker/Isolated modes
|
|
20
|
+
// naturally get separate contexts by running in different threads/processes.
|
|
21
|
+
const factory = this.runtimeFactory;
|
|
22
|
+
await new Promise<void>((resolve, reject) => {
|
|
23
|
+
setTimeout(async () => {
|
|
24
|
+
try {
|
|
25
|
+
// Create blank context - inline runtime should not inherit test context
|
|
26
|
+
new GGLocatorScope("GGInlineRunner").enter();
|
|
27
|
+
this.controlClient = new GGTestRuntimeWorker(this.config);
|
|
28
|
+
// Pass factory to avoid dynamic import() which causes duplicate
|
|
29
|
+
// module loading in Vite/vitest environments
|
|
30
|
+
await this.controlClient.start(factory);
|
|
31
|
+
resolve();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
reject(err);
|
|
34
|
+
}
|
|
35
|
+
}, 0);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async stopRuntime(): Promise<void> {
|
|
40
|
+
await this.controlClient?.stopRuntime();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async shutdown(): Promise<void> {
|
|
44
|
+
await this.controlClient?.shutdown();
|
|
45
|
+
this.controlClient = undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|