@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
package/src/GGTest.ts
CHANGED
|
@@ -1,318 +1,318 @@
|
|
|
1
|
-
import {GG_TEST_RESOURCE, GG_TEST_RUNNER, GGTestRunner, TestResource} from "./GGTestRunner";
|
|
2
|
-
import {RuntimeConstructor, RuntimeInput, StartResult, Selector} from "./testers/RuntimeSelector";
|
|
3
|
-
import {GGTestMode, GGTestRuntime, GGTestRuntimeConfig} from "./GGTestRuntime";
|
|
4
|
-
import {createStartResult} from "./testers/GGTestSelector";
|
|
5
|
-
import {IGGTestWith} from "./testers/IGGTestWith";
|
|
6
|
-
|
|
7
|
-
// -------------------------------------------------
|
|
8
|
-
// GGTest - Static API for test setup
|
|
9
|
-
// -------------------------------------------------
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Make a result awaitable by adding a then() method.
|
|
13
|
-
*/
|
|
14
|
-
type Awaitable<T> = T & PromiseLike<T>;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Static API for test setup operations.
|
|
18
|
-
* Use GGTest.startWorker(), GGTest.with(), etc. in test files.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // Single runtime
|
|
22
|
-
* const t = GGTest.startWorker(MainRuntime);
|
|
23
|
-
* t.logs.cursor();
|
|
24
|
-
*
|
|
25
|
-
* // Multiple instances
|
|
26
|
-
* const t = GGTest.startWorker([MainRuntime, MainRuntime]);
|
|
27
|
-
* t[0].logs.cursor();
|
|
28
|
-
*
|
|
29
|
-
* // Named runtimes
|
|
30
|
-
* const t = GGTest.startWorker({main: MainRuntime, sub: SubRuntime});
|
|
31
|
-
* t.main.logs.cursor();
|
|
32
|
-
* t.sub.config.update();
|
|
33
|
-
*/
|
|
34
|
-
export class GGTest {
|
|
35
|
-
|
|
36
|
-
// -------------------------------------------------
|
|
37
|
-
// Private state for runtime management
|
|
38
|
-
// -------------------------------------------------
|
|
39
|
-
|
|
40
|
-
// When GG_COVERAGE_MODE is set, force INLINE mode for all runtimes
|
|
41
|
-
// This is needed because v8 coverage doesn't capture worker_threads or child_process
|
|
42
|
-
private static readonly forceCoverageInline = process.env.GG_COVERAGE_MODE === '1';
|
|
43
|
-
|
|
44
|
-
// -------------------------------------------------
|
|
45
|
-
// Public static methods
|
|
46
|
-
// -------------------------------------------------
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get a resource wrapper for calling test operations.
|
|
50
|
-
* Returns an object with methods defined by the resource's [GG_TEST_RESOURCE].
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* GGTest.with(MyConfig.mysql).clone();
|
|
54
|
-
* GGTest.with(MyConfig.mysql).clone("seed.sql");
|
|
55
|
-
*/
|
|
56
|
-
public static with<T extends TestResource>(resource: T): T[typeof GG_TEST_RESOURCE] {
|
|
57
|
-
return resource[GG_TEST_RESOURCE];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Start runtimes in inline mode.
|
|
62
|
-
* Inline mode runs the service code in the same process as the test.
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* // Single runtime
|
|
66
|
-
* const t = GGTest.startInline(MainRuntime);
|
|
67
|
-
*
|
|
68
|
-
* // Array of runtimes (multiple instances)
|
|
69
|
-
* const t = GGTest.startInline([MainRuntime, MainRuntime]);
|
|
70
|
-
*
|
|
71
|
-
* // Object with named runtimes
|
|
72
|
-
* const t = GGTest.startInline({main: MainRuntime, sub: SubRuntime});
|
|
73
|
-
*/
|
|
74
|
-
public static startInline<const T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
|
|
75
|
-
return this.startWithMode(input, GGTestMode.INLINE);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Start runtimes in worker mode.
|
|
80
|
-
* Worker mode runs the service code in a worker thread.
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* // Single runtime
|
|
84
|
-
* const t = GGTest.startWorker(MainRuntime);
|
|
85
|
-
*
|
|
86
|
-
* // Array of runtimes (multiple instances)
|
|
87
|
-
* const t = GGTest.startWorker([MainRuntime, MainRuntime]);
|
|
88
|
-
*
|
|
89
|
-
* // Object with named runtimes
|
|
90
|
-
* const t = GGTest.startWorker({main: MainRuntime, sub: SubRuntime});
|
|
91
|
-
*/
|
|
92
|
-
public static startWorker<T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
|
|
93
|
-
return this.startWithMode(input, GGTestMode.WORKER);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Start runtimes in isolated mode.
|
|
98
|
-
* Isolated mode runs the service code in a separate process.
|
|
99
|
-
*
|
|
100
|
-
* @example
|
|
101
|
-
* // Single runtime
|
|
102
|
-
* const t = GGTest.startIsolated(MainRuntime);
|
|
103
|
-
*
|
|
104
|
-
* // Array of runtimes (multiple instances)
|
|
105
|
-
* const t = GGTest.startIsolated([MainRuntime, MainRuntime]);
|
|
106
|
-
*
|
|
107
|
-
* // Object with named runtimes
|
|
108
|
-
* const t = GGTest.startIsolated({main: MainRuntime, sub: SubRuntime});
|
|
109
|
-
*/
|
|
110
|
-
public static startIsolated<T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
|
|
111
|
-
return this.startWithMode(input, GGTestMode.ISOLATED);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Wait for an async expectation to be satisfied.
|
|
116
|
-
* Use this for standalone async expectations outside of action chains.
|
|
117
|
-
*
|
|
118
|
-
* Polls the expectation every 20ms until it's satisfied or timeout is reached.
|
|
119
|
-
*
|
|
120
|
-
* @param expectation - The expectation to wait for
|
|
121
|
-
* @param timeout - Max time to wait in ms (default: 5000)
|
|
122
|
-
*
|
|
123
|
-
* @example
|
|
124
|
-
* // Wait for a log after injecting an event
|
|
125
|
-
* await UserEventsPublisher.inject.registered({...});
|
|
126
|
-
* await GGTest.waitFor(t.logs.expect(/validation failed/i));
|
|
127
|
-
*
|
|
128
|
-
* @example
|
|
129
|
-
* // Wait for a metric to be incremented
|
|
130
|
-
* await GGTest.waitFor(t.main.metrics.expect(SomeMetric).inc({label: 'value'}));
|
|
131
|
-
*/
|
|
132
|
-
public static async waitFor(expectation: IGGTestWith, timeout: number = 5000): Promise<void> {
|
|
133
|
-
const interceptor = expectation.createInterceptor();
|
|
134
|
-
const checkInterval = 20;
|
|
135
|
-
const startTime = Date.now();
|
|
136
|
-
|
|
137
|
-
await interceptor.register();
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
await new Promise<void>((resolve, reject) => {
|
|
141
|
-
const check = () => {
|
|
142
|
-
const elapsed = Date.now() - startTime;
|
|
143
|
-
|
|
144
|
-
if (interceptor.isCalled()) {
|
|
145
|
-
resolve();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (elapsed > timeout) {
|
|
150
|
-
reject(new Error(
|
|
151
|
-
`[GGTest.waitFor] Timeout after ${timeout}ms waiting for expectation`
|
|
152
|
-
));
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
setTimeout(check, checkInterval);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
check();
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
await interceptor.validate();
|
|
163
|
-
const error = interceptor.getMockValidationError();
|
|
164
|
-
if (error) {
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
} finally {
|
|
168
|
-
await interceptor.unregister();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// -------------------------------------------------
|
|
173
|
-
// Private helpers
|
|
174
|
-
// -------------------------------------------------
|
|
175
|
-
|
|
176
|
-
private static startWithMode<T extends RuntimeInput>(
|
|
177
|
-
input: T,
|
|
178
|
-
mode: GGTestMode
|
|
179
|
-
): Awaitable<StartResult<T>> {
|
|
180
|
-
const test = GG_TEST_RUNNER.get();
|
|
181
|
-
const constructors = this.extractConstructors(input);
|
|
182
|
-
const runtimes = this.createRuntimes(test, constructors, {mode: this.getCoverageMode(mode)});
|
|
183
|
-
const result = createStartResult(input, runtimes);
|
|
184
|
-
|
|
185
|
-
if (test.started) {
|
|
186
|
-
// We're inside a test block - start immediately
|
|
187
|
-
// Cleanup is handled by the global afterEach registered in initInTestCleanup
|
|
188
|
-
const startupPromise = this.startRuntimesInTestBlock(runtimes);
|
|
189
|
-
return this.makeAwaitable(result, startupPromise);
|
|
190
|
-
} else {
|
|
191
|
-
// We're in describe block - normal flow, runtimes start in beforeAll
|
|
192
|
-
// Return immediately-resolved thenable for consistency
|
|
193
|
-
return this.makeAwaitable(result, Promise.resolve());
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Extract runtime constructors from the input in the order they should be created.
|
|
199
|
-
*/
|
|
200
|
-
private static extractConstructors(input: RuntimeInput): RuntimeConstructor[] {
|
|
201
|
-
// Single runtime
|
|
202
|
-
if ('NAME' in input) {
|
|
203
|
-
return [input as RuntimeConstructor];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Array of runtimes
|
|
207
|
-
if (Array.isArray(input)) {
|
|
208
|
-
return input;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Object: { main: MainRuntime, sub: [SubRuntime, SubRuntime] }
|
|
212
|
-
const constructors: RuntimeConstructor[] = [];
|
|
213
|
-
for (const value of Object.values(input)) {
|
|
214
|
-
if (Array.isArray(value)) {
|
|
215
|
-
constructors.push(...value);
|
|
216
|
-
} else {
|
|
217
|
-
constructors.push(value);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return constructors;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Start runtimes immediately (for in-test usage).
|
|
225
|
-
* Runtimes are automatically tracked by the runner for afterEach cleanup.
|
|
226
|
-
*/
|
|
227
|
-
private static async startRuntimesInTestBlock(runtimes: GGTestRuntime[]): Promise<void> {
|
|
228
|
-
for (const runtime of runtimes) {
|
|
229
|
-
await runtime.start();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Makes a result thenable by adding a then() method.
|
|
235
|
-
* This allows both sync and async usage patterns.
|
|
236
|
-
*
|
|
237
|
-
* IMPORTANT: The then() method removes itself after being called to prevent
|
|
238
|
-
* infinite thenable resolution. When onfulfilled returns the result,
|
|
239
|
-
* JavaScript would try to resolve it again if then() still existed.
|
|
240
|
-
*/
|
|
241
|
-
private static makeAwaitable<T>(result: T, startupPromise: Promise<void>): Awaitable<T> {
|
|
242
|
-
const awaitable = result as Awaitable<T>;
|
|
243
|
-
|
|
244
|
-
awaitable.then = <TResult1 = T, TResult2 = never>(
|
|
245
|
-
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
|
246
|
-
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
|
247
|
-
): Promise<TResult1 | TResult2> => {
|
|
248
|
-
// Remove then() to prevent infinite thenable resolution
|
|
249
|
-
delete (awaitable as any).then;
|
|
250
|
-
|
|
251
|
-
return startupPromise.then(
|
|
252
|
-
() => onfulfilled ? onfulfilled(result) : result as unknown as TResult1,
|
|
253
|
-
onrejected
|
|
254
|
-
);
|
|
255
|
-
};
|
|
256
|
-
return awaitable;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private static getCoverageMode(requestedMode: GGTestMode): GGTestMode {
|
|
260
|
-
if (this.forceCoverageInline && requestedMode !== GGTestMode.INLINE) {
|
|
261
|
-
return GGTestMode.INLINE;
|
|
262
|
-
}
|
|
263
|
-
return requestedMode;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private static createRuntimes(
|
|
267
|
-
test: GGTestRunner,
|
|
268
|
-
services: RuntimeConstructor[],
|
|
269
|
-
config: GGTestRuntimeConfig
|
|
270
|
-
): GGTestRuntime[] {
|
|
271
|
-
const createdRuntimes: GGTestRuntime[] = []
|
|
272
|
-
|
|
273
|
-
for (const service of services) {
|
|
274
|
-
const runtimeConstructor = service as RuntimeConstructor;
|
|
275
|
-
|
|
276
|
-
// Validate that runtime has NAME property
|
|
277
|
-
if (!runtimeConstructor.NAME) {
|
|
278
|
-
throw new Error(
|
|
279
|
-
`Runtime '${service.name}' must define a static NAME property. ` +
|
|
280
|
-
`Add 'public static readonly NAME = "yourname"' to the class.`
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Validate that runtime has SOURCE_MODULE_URL (set by GGRuntime.cli())
|
|
285
|
-
if (!runtimeConstructor.SOURCE_MODULE_URL) {
|
|
286
|
-
throw new Error(
|
|
287
|
-
`Runtime '${service.name}' has no source path. ` +
|
|
288
|
-
`Make sure the runtime file calls: ${service.name}.cli(import.meta.url)`
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const className = service.name;
|
|
293
|
-
const name = runtimeConstructor.NAME;
|
|
294
|
-
const sourcePath = runtimeConstructor.SOURCE_MODULE_URL;
|
|
295
|
-
|
|
296
|
-
const runtime = new GGTestRuntime(test, sourcePath, className, name, config);
|
|
297
|
-
// Store factory for inline mode — avoids dynamic import which causes
|
|
298
|
-
// duplicate module loading in Vite/vitest environments
|
|
299
|
-
runtime.runtimeFactory = () => new (runtimeConstructor as any)();
|
|
300
|
-
createdRuntimes.push(runtime)
|
|
301
|
-
}
|
|
302
|
-
return createdRuntimes;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// -------------------------------------------------
|
|
307
|
-
// Legacy exports for backwards compatibility
|
|
308
|
-
// -------------------------------------------------
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* @deprecated Use StartResult instead
|
|
312
|
-
*/
|
|
313
|
-
export type {RuntimeResult} from "./testers/RuntimeSelector";
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* @deprecated Use Awaitable<StartResult<T>> instead
|
|
317
|
-
*/
|
|
318
|
-
export type AwaitableRuntimeResult<R extends RuntimeConstructor[]> = Awaitable<Selector<R>>;
|
|
1
|
+
import {GG_TEST_RESOURCE, GG_TEST_RUNNER, GGTestRunner, TestResource} from "./GGTestRunner";
|
|
2
|
+
import {RuntimeConstructor, RuntimeInput, StartResult, Selector} from "./testers/RuntimeSelector";
|
|
3
|
+
import {GGTestMode, GGTestRuntime, GGTestRuntimeConfig} from "./GGTestRuntime";
|
|
4
|
+
import {createStartResult} from "./testers/GGTestSelector";
|
|
5
|
+
import {IGGTestWith} from "./testers/IGGTestWith";
|
|
6
|
+
|
|
7
|
+
// -------------------------------------------------
|
|
8
|
+
// GGTest - Static API for test setup
|
|
9
|
+
// -------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Make a result awaitable by adding a then() method.
|
|
13
|
+
*/
|
|
14
|
+
type Awaitable<T> = T & PromiseLike<T>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Static API for test setup operations.
|
|
18
|
+
* Use GGTest.startWorker(), GGTest.with(), etc. in test files.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Single runtime
|
|
22
|
+
* const t = GGTest.startWorker(MainRuntime);
|
|
23
|
+
* t.logs.cursor();
|
|
24
|
+
*
|
|
25
|
+
* // Multiple instances
|
|
26
|
+
* const t = GGTest.startWorker([MainRuntime, MainRuntime]);
|
|
27
|
+
* t[0].logs.cursor();
|
|
28
|
+
*
|
|
29
|
+
* // Named runtimes
|
|
30
|
+
* const t = GGTest.startWorker({main: MainRuntime, sub: SubRuntime});
|
|
31
|
+
* t.main.logs.cursor();
|
|
32
|
+
* t.sub.config.update();
|
|
33
|
+
*/
|
|
34
|
+
export class GGTest {
|
|
35
|
+
|
|
36
|
+
// -------------------------------------------------
|
|
37
|
+
// Private state for runtime management
|
|
38
|
+
// -------------------------------------------------
|
|
39
|
+
|
|
40
|
+
// When GG_COVERAGE_MODE is set, force INLINE mode for all runtimes
|
|
41
|
+
// This is needed because v8 coverage doesn't capture worker_threads or child_process
|
|
42
|
+
private static readonly forceCoverageInline = process.env.GG_COVERAGE_MODE === '1';
|
|
43
|
+
|
|
44
|
+
// -------------------------------------------------
|
|
45
|
+
// Public static methods
|
|
46
|
+
// -------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get a resource wrapper for calling test operations.
|
|
50
|
+
* Returns an object with methods defined by the resource's [GG_TEST_RESOURCE].
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* GGTest.with(MyConfig.mysql).clone();
|
|
54
|
+
* GGTest.with(MyConfig.mysql).clone("seed.sql");
|
|
55
|
+
*/
|
|
56
|
+
public static with<T extends TestResource>(resource: T): T[typeof GG_TEST_RESOURCE] {
|
|
57
|
+
return resource[GG_TEST_RESOURCE];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Start runtimes in inline mode.
|
|
62
|
+
* Inline mode runs the service code in the same process as the test.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* // Single runtime
|
|
66
|
+
* const t = GGTest.startInline(MainRuntime);
|
|
67
|
+
*
|
|
68
|
+
* // Array of runtimes (multiple instances)
|
|
69
|
+
* const t = GGTest.startInline([MainRuntime, MainRuntime]);
|
|
70
|
+
*
|
|
71
|
+
* // Object with named runtimes
|
|
72
|
+
* const t = GGTest.startInline({main: MainRuntime, sub: SubRuntime});
|
|
73
|
+
*/
|
|
74
|
+
public static startInline<const T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
|
|
75
|
+
return this.startWithMode(input, GGTestMode.INLINE);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Start runtimes in worker mode.
|
|
80
|
+
* Worker mode runs the service code in a worker thread.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // Single runtime
|
|
84
|
+
* const t = GGTest.startWorker(MainRuntime);
|
|
85
|
+
*
|
|
86
|
+
* // Array of runtimes (multiple instances)
|
|
87
|
+
* const t = GGTest.startWorker([MainRuntime, MainRuntime]);
|
|
88
|
+
*
|
|
89
|
+
* // Object with named runtimes
|
|
90
|
+
* const t = GGTest.startWorker({main: MainRuntime, sub: SubRuntime});
|
|
91
|
+
*/
|
|
92
|
+
public static startWorker<T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
|
|
93
|
+
return this.startWithMode(input, GGTestMode.WORKER);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Start runtimes in isolated mode.
|
|
98
|
+
* Isolated mode runs the service code in a separate process.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Single runtime
|
|
102
|
+
* const t = GGTest.startIsolated(MainRuntime);
|
|
103
|
+
*
|
|
104
|
+
* // Array of runtimes (multiple instances)
|
|
105
|
+
* const t = GGTest.startIsolated([MainRuntime, MainRuntime]);
|
|
106
|
+
*
|
|
107
|
+
* // Object with named runtimes
|
|
108
|
+
* const t = GGTest.startIsolated({main: MainRuntime, sub: SubRuntime});
|
|
109
|
+
*/
|
|
110
|
+
public static startIsolated<T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
|
|
111
|
+
return this.startWithMode(input, GGTestMode.ISOLATED);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Wait for an async expectation to be satisfied.
|
|
116
|
+
* Use this for standalone async expectations outside of action chains.
|
|
117
|
+
*
|
|
118
|
+
* Polls the expectation every 20ms until it's satisfied or timeout is reached.
|
|
119
|
+
*
|
|
120
|
+
* @param expectation - The expectation to wait for
|
|
121
|
+
* @param timeout - Max time to wait in ms (default: 5000)
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* // Wait for a log after injecting an event
|
|
125
|
+
* await UserEventsPublisher.inject.registered({...});
|
|
126
|
+
* await GGTest.waitFor(t.logs.expect(/validation failed/i));
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* // Wait for a metric to be incremented
|
|
130
|
+
* await GGTest.waitFor(t.main.metrics.expect(SomeMetric).inc({label: 'value'}));
|
|
131
|
+
*/
|
|
132
|
+
public static async waitFor(expectation: IGGTestWith, timeout: number = 5000): Promise<void> {
|
|
133
|
+
const interceptor = expectation.createInterceptor();
|
|
134
|
+
const checkInterval = 20;
|
|
135
|
+
const startTime = Date.now();
|
|
136
|
+
|
|
137
|
+
await interceptor.register();
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await new Promise<void>((resolve, reject) => {
|
|
141
|
+
const check = () => {
|
|
142
|
+
const elapsed = Date.now() - startTime;
|
|
143
|
+
|
|
144
|
+
if (interceptor.isCalled()) {
|
|
145
|
+
resolve();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (elapsed > timeout) {
|
|
150
|
+
reject(new Error(
|
|
151
|
+
`[GGTest.waitFor] Timeout after ${timeout}ms waiting for expectation`
|
|
152
|
+
));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
setTimeout(check, checkInterval);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
check();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await interceptor.validate();
|
|
163
|
+
const error = interceptor.getMockValidationError();
|
|
164
|
+
if (error) {
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
} finally {
|
|
168
|
+
await interceptor.unregister();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// -------------------------------------------------
|
|
173
|
+
// Private helpers
|
|
174
|
+
// -------------------------------------------------
|
|
175
|
+
|
|
176
|
+
private static startWithMode<T extends RuntimeInput>(
|
|
177
|
+
input: T,
|
|
178
|
+
mode: GGTestMode
|
|
179
|
+
): Awaitable<StartResult<T>> {
|
|
180
|
+
const test = GG_TEST_RUNNER.get();
|
|
181
|
+
const constructors = this.extractConstructors(input);
|
|
182
|
+
const runtimes = this.createRuntimes(test, constructors, {mode: this.getCoverageMode(mode)});
|
|
183
|
+
const result = createStartResult(input, runtimes);
|
|
184
|
+
|
|
185
|
+
if (test.started) {
|
|
186
|
+
// We're inside a test block - start immediately
|
|
187
|
+
// Cleanup is handled by the global afterEach registered in initInTestCleanup
|
|
188
|
+
const startupPromise = this.startRuntimesInTestBlock(runtimes);
|
|
189
|
+
return this.makeAwaitable(result, startupPromise);
|
|
190
|
+
} else {
|
|
191
|
+
// We're in describe block - normal flow, runtimes start in beforeAll
|
|
192
|
+
// Return immediately-resolved thenable for consistency
|
|
193
|
+
return this.makeAwaitable(result, Promise.resolve());
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Extract runtime constructors from the input in the order they should be created.
|
|
199
|
+
*/
|
|
200
|
+
private static extractConstructors(input: RuntimeInput): RuntimeConstructor[] {
|
|
201
|
+
// Single runtime
|
|
202
|
+
if ('NAME' in input) {
|
|
203
|
+
return [input as RuntimeConstructor];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Array of runtimes
|
|
207
|
+
if (Array.isArray(input)) {
|
|
208
|
+
return input;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Object: { main: MainRuntime, sub: [SubRuntime, SubRuntime] }
|
|
212
|
+
const constructors: RuntimeConstructor[] = [];
|
|
213
|
+
for (const value of Object.values(input)) {
|
|
214
|
+
if (Array.isArray(value)) {
|
|
215
|
+
constructors.push(...value);
|
|
216
|
+
} else {
|
|
217
|
+
constructors.push(value);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return constructors;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Start runtimes immediately (for in-test usage).
|
|
225
|
+
* Runtimes are automatically tracked by the runner for afterEach cleanup.
|
|
226
|
+
*/
|
|
227
|
+
private static async startRuntimesInTestBlock(runtimes: GGTestRuntime[]): Promise<void> {
|
|
228
|
+
for (const runtime of runtimes) {
|
|
229
|
+
await runtime.start();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Makes a result thenable by adding a then() method.
|
|
235
|
+
* This allows both sync and async usage patterns.
|
|
236
|
+
*
|
|
237
|
+
* IMPORTANT: The then() method removes itself after being called to prevent
|
|
238
|
+
* infinite thenable resolution. When onfulfilled returns the result,
|
|
239
|
+
* JavaScript would try to resolve it again if then() still existed.
|
|
240
|
+
*/
|
|
241
|
+
private static makeAwaitable<T>(result: T, startupPromise: Promise<void>): Awaitable<T> {
|
|
242
|
+
const awaitable = result as Awaitable<T>;
|
|
243
|
+
|
|
244
|
+
awaitable.then = <TResult1 = T, TResult2 = never>(
|
|
245
|
+
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
|
246
|
+
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
|
247
|
+
): Promise<TResult1 | TResult2> => {
|
|
248
|
+
// Remove then() to prevent infinite thenable resolution
|
|
249
|
+
delete (awaitable as any).then;
|
|
250
|
+
|
|
251
|
+
return startupPromise.then(
|
|
252
|
+
() => onfulfilled ? onfulfilled(result) : result as unknown as TResult1,
|
|
253
|
+
onrejected
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
return awaitable;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private static getCoverageMode(requestedMode: GGTestMode): GGTestMode {
|
|
260
|
+
if (this.forceCoverageInline && requestedMode !== GGTestMode.INLINE) {
|
|
261
|
+
return GGTestMode.INLINE;
|
|
262
|
+
}
|
|
263
|
+
return requestedMode;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private static createRuntimes(
|
|
267
|
+
test: GGTestRunner,
|
|
268
|
+
services: RuntimeConstructor[],
|
|
269
|
+
config: GGTestRuntimeConfig
|
|
270
|
+
): GGTestRuntime[] {
|
|
271
|
+
const createdRuntimes: GGTestRuntime[] = []
|
|
272
|
+
|
|
273
|
+
for (const service of services) {
|
|
274
|
+
const runtimeConstructor = service as RuntimeConstructor;
|
|
275
|
+
|
|
276
|
+
// Validate that runtime has NAME property
|
|
277
|
+
if (!runtimeConstructor.NAME) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`Runtime '${service.name}' must define a static NAME property. ` +
|
|
280
|
+
`Add 'public static readonly NAME = "yourname"' to the class.`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Validate that runtime has SOURCE_MODULE_URL (set by GGRuntime.cli())
|
|
285
|
+
if (!runtimeConstructor.SOURCE_MODULE_URL) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`Runtime '${service.name}' has no source path. ` +
|
|
288
|
+
`Make sure the runtime file calls: ${service.name}.cli(import.meta.url)`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const className = service.name;
|
|
293
|
+
const name = runtimeConstructor.NAME;
|
|
294
|
+
const sourcePath = runtimeConstructor.SOURCE_MODULE_URL;
|
|
295
|
+
|
|
296
|
+
const runtime = new GGTestRuntime(test, sourcePath, className, name, config);
|
|
297
|
+
// Store factory for inline mode — avoids dynamic import which causes
|
|
298
|
+
// duplicate module loading in Vite/vitest environments
|
|
299
|
+
runtime.runtimeFactory = () => new (runtimeConstructor as any)();
|
|
300
|
+
createdRuntimes.push(runtime)
|
|
301
|
+
}
|
|
302
|
+
return createdRuntimes;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// -------------------------------------------------
|
|
307
|
+
// Legacy exports for backwards compatibility
|
|
308
|
+
// -------------------------------------------------
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @deprecated Use StartResult instead
|
|
312
|
+
*/
|
|
313
|
+
export type {RuntimeResult} from "./testers/RuntimeSelector";
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @deprecated Use Awaitable<StartResult<T>> instead
|
|
317
|
+
*/
|
|
318
|
+
export type AwaitableRuntimeResult<R extends RuntimeConstructor[]> = Awaitable<Selector<R>>;
|