@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.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -0
  3. package/dist/src/GGBundleTest.d.ts +8 -0
  4. package/dist/src/GGBundleTest.d.ts.map +1 -0
  5. package/dist/src/GGBundleTest.js +75 -0
  6. package/dist/src/GGBundleTest.js.map +1 -0
  7. package/dist/src/GGTest.d.ts +131 -0
  8. package/dist/src/GGTest.d.ts.map +1 -0
  9. package/dist/src/GGTest.js +245 -0
  10. package/dist/src/GGTest.js.map +1 -0
  11. package/dist/src/GGTestContext.d.ts +36 -0
  12. package/dist/src/GGTestContext.d.ts.map +1 -0
  13. package/dist/src/GGTestContext.js +63 -0
  14. package/dist/src/GGTestContext.js.map +1 -0
  15. package/dist/src/GGTestRunner.d.ts +108 -0
  16. package/dist/src/GGTestRunner.d.ts.map +1 -0
  17. package/dist/src/GGTestRunner.js +242 -0
  18. package/dist/src/GGTestRunner.js.map +1 -0
  19. package/dist/src/GGTestRuntime.d.ts +103 -0
  20. package/dist/src/GGTestRuntime.d.ts.map +1 -0
  21. package/dist/src/GGTestRuntime.js +219 -0
  22. package/dist/src/GGTestRuntime.js.map +1 -0
  23. package/dist/src/GGTestRuntimeWorker.d.ts +41 -0
  24. package/dist/src/GGTestRuntimeWorker.d.ts.map +1 -0
  25. package/dist/src/GGTestRuntimeWorker.js +136 -0
  26. package/dist/src/GGTestRuntimeWorker.js.map +1 -0
  27. package/dist/src/GGTestSharedRef.d.ts +35 -0
  28. package/dist/src/GGTestSharedRef.d.ts.map +1 -0
  29. package/dist/src/GGTestSharedRef.js +126 -0
  30. package/dist/src/GGTestSharedRef.js.map +1 -0
  31. package/dist/src/GGTestkitExtensionsDiscovery.d.ts +21 -0
  32. package/dist/src/GGTestkitExtensionsDiscovery.d.ts.map +1 -0
  33. package/dist/src/GGTestkitExtensionsDiscovery.js +24 -0
  34. package/dist/src/GGTestkitExtensionsDiscovery.js.map +1 -0
  35. package/dist/src/IGGLocalDiscoveryServer.d.ts +16 -0
  36. package/dist/src/IGGLocalDiscoveryServer.d.ts.map +1 -0
  37. package/dist/src/IGGLocalDiscoveryServer.js +2 -0
  38. package/dist/src/IGGLocalDiscoveryServer.js.map +1 -0
  39. package/dist/src/callOn/GGCallOnSelector.d.ts +42 -0
  40. package/dist/src/callOn/GGCallOnSelector.d.ts.map +1 -0
  41. package/dist/src/callOn/GGCallOnSelector.js +35 -0
  42. package/dist/src/callOn/GGCallOnSelector.js.map +1 -0
  43. package/dist/src/callOn/GGContractClass.implement.d.ts +8 -0
  44. package/dist/src/callOn/GGContractClass.implement.d.ts.map +1 -0
  45. package/dist/src/callOn/GGContractClass.implement.js +31 -0
  46. package/dist/src/callOn/GGContractClass.implement.js.map +1 -0
  47. package/dist/src/callOn/GGTestActionForLocatorOnCall.d.ts +28 -0
  48. package/dist/src/callOn/GGTestActionForLocatorOnCall.d.ts.map +1 -0
  49. package/dist/src/callOn/GGTestActionForLocatorOnCall.js +118 -0
  50. package/dist/src/callOn/GGTestActionForLocatorOnCall.js.map +1 -0
  51. package/dist/src/callOn/TestableIPC.d.ts +72 -0
  52. package/dist/src/callOn/TestableIPC.d.ts.map +1 -0
  53. package/dist/src/callOn/TestableIPC.js +34 -0
  54. package/dist/src/callOn/TestableIPC.js.map +1 -0
  55. package/dist/src/callOn/callOn.d.ts +113 -0
  56. package/dist/src/callOn/callOn.d.ts.map +1 -0
  57. package/dist/src/callOn/callOn.js +122 -0
  58. package/dist/src/callOn/callOn.js.map +1 -0
  59. package/dist/src/callOn/registerOnCallHandler.d.ts +13 -0
  60. package/dist/src/callOn/registerOnCallHandler.d.ts.map +1 -0
  61. package/dist/src/callOn/registerOnCallHandler.js +111 -0
  62. package/dist/src/callOn/registerOnCallHandler.js.map +1 -0
  63. package/dist/src/index-node.d.ts +35 -0
  64. package/dist/src/index-node.d.ts.map +1 -0
  65. package/dist/src/index-node.js +50 -0
  66. package/dist/src/index-node.js.map +1 -0
  67. package/dist/src/mockable/GGMockable.d.ts +19 -0
  68. package/dist/src/mockable/GGMockable.d.ts.map +1 -0
  69. package/dist/src/mockable/GGMockable.js +2 -0
  70. package/dist/src/mockable/GGMockable.js.map +1 -0
  71. package/dist/src/mockable/GGMockableCall.d.ts +2 -0
  72. package/dist/src/mockable/GGMockableCall.d.ts.map +1 -0
  73. package/dist/src/mockable/GGMockableCall.js +41 -0
  74. package/dist/src/mockable/GGMockableCall.js.map +1 -0
  75. package/dist/src/mockable/GGMockableIPC.d.ts +17 -0
  76. package/dist/src/mockable/GGMockableIPC.d.ts.map +1 -0
  77. package/dist/src/mockable/GGMockableIPC.js +8 -0
  78. package/dist/src/mockable/GGMockableIPC.js.map +1 -0
  79. package/dist/src/mockable/GGMockableInterceptor.d.ts +24 -0
  80. package/dist/src/mockable/GGMockableInterceptor.d.ts.map +1 -0
  81. package/dist/src/mockable/GGMockableInterceptor.js +32 -0
  82. package/dist/src/mockable/GGMockableInterceptor.js.map +1 -0
  83. package/dist/src/mockable/GGMockableInterceptorsServer.d.ts +12 -0
  84. package/dist/src/mockable/GGMockableInterceptorsServer.d.ts.map +1 -0
  85. package/dist/src/mockable/GGMockableInterceptorsServer.js +55 -0
  86. package/dist/src/mockable/GGMockableInterceptorsServer.js.map +1 -0
  87. package/dist/src/mockable/mockable.d.ts +46 -0
  88. package/dist/src/mockable/mockable.d.ts.map +1 -0
  89. package/dist/src/mockable/mockable.js +47 -0
  90. package/dist/src/mockable/mockable.js.map +1 -0
  91. package/dist/src/runner/InlineRunner.d.ts +12 -0
  92. package/dist/src/runner/InlineRunner.d.ts.map +1 -0
  93. package/dist/src/runner/InlineRunner.js +42 -0
  94. package/dist/src/runner/InlineRunner.js.map +1 -0
  95. package/dist/src/runner/IsolatedRunner.d.ts +17 -0
  96. package/dist/src/runner/IsolatedRunner.d.ts.map +1 -0
  97. package/dist/src/runner/IsolatedRunner.js +155 -0
  98. package/dist/src/runner/IsolatedRunner.js.map +1 -0
  99. package/dist/src/runner/RuntimeRunner.d.ts +14 -0
  100. package/dist/src/runner/RuntimeRunner.d.ts.map +1 -0
  101. package/dist/src/runner/RuntimeRunner.js +2 -0
  102. package/dist/src/runner/RuntimeRunner.js.map +1 -0
  103. package/dist/src/runner/WorkerRunner.d.ts +17 -0
  104. package/dist/src/runner/WorkerRunner.d.ts.map +1 -0
  105. package/dist/src/runner/WorkerRunner.js +155 -0
  106. package/dist/src/runner/WorkerRunner.js.map +1 -0
  107. package/dist/src/runner/isolated-loader.mjs +91 -0
  108. package/dist/src/runner/worker-loader.mjs +49 -0
  109. package/dist/src/testers/GGCallInterceptor.d.ts +71 -0
  110. package/dist/src/testers/GGCallInterceptor.d.ts.map +1 -0
  111. package/dist/src/testers/GGCallInterceptor.js +170 -0
  112. package/dist/src/testers/GGCallInterceptor.js.map +1 -0
  113. package/dist/src/testers/GGMockWith.d.ts +30 -0
  114. package/dist/src/testers/GGMockWith.d.ts.map +1 -0
  115. package/dist/src/testers/GGMockWith.js +70 -0
  116. package/dist/src/testers/GGMockWith.js.map +1 -0
  117. package/dist/src/testers/GGSpyWith.d.ts +40 -0
  118. package/dist/src/testers/GGSpyWith.d.ts.map +1 -0
  119. package/dist/src/testers/GGSpyWith.js +90 -0
  120. package/dist/src/testers/GGSpyWith.js.map +1 -0
  121. package/dist/src/testers/GGTestAction.d.ts +126 -0
  122. package/dist/src/testers/GGTestAction.d.ts.map +1 -0
  123. package/dist/src/testers/GGTestAction.js +245 -0
  124. package/dist/src/testers/GGTestAction.js.map +1 -0
  125. package/dist/src/testers/GGTestComponent.d.ts +15 -0
  126. package/dist/src/testers/GGTestComponent.d.ts.map +1 -0
  127. package/dist/src/testers/GGTestComponent.js +2 -0
  128. package/dist/src/testers/GGTestComponent.js.map +1 -0
  129. package/dist/src/testers/GGTestSelector.d.ts +54 -0
  130. package/dist/src/testers/GGTestSelector.d.ts.map +1 -0
  131. package/dist/src/testers/GGTestSelector.js +179 -0
  132. package/dist/src/testers/GGTestSelector.js.map +1 -0
  133. package/dist/src/testers/IGGTestInterceptor.d.ts +8 -0
  134. package/dist/src/testers/IGGTestInterceptor.d.ts.map +1 -0
  135. package/dist/src/testers/IGGTestInterceptor.js +2 -0
  136. package/dist/src/testers/IGGTestInterceptor.js.map +1 -0
  137. package/dist/src/testers/IGGTestWith.d.ts +13 -0
  138. package/dist/src/testers/IGGTestWith.d.ts.map +1 -0
  139. package/dist/src/testers/IGGTestWith.js +2 -0
  140. package/dist/src/testers/IGGTestWith.js.map +1 -0
  141. package/dist/src/testers/RuntimeSelector.d.ts +117 -0
  142. package/dist/src/testers/RuntimeSelector.d.ts.map +1 -0
  143. package/dist/src/testers/RuntimeSelector.js +2 -0
  144. package/dist/src/testers/RuntimeSelector.js.map +1 -0
  145. package/dist/src/tsconfig.json +17 -0
  146. package/dist/src/utils/GGExpectations.d.ts +18 -0
  147. package/dist/src/utils/GGExpectations.d.ts.map +1 -0
  148. package/dist/src/utils/GGExpectations.js +59 -0
  149. package/dist/src/utils/GGExpectations.js.map +1 -0
  150. package/dist/src/utils/GGTestError.d.ts +13 -0
  151. package/dist/src/utils/GGTestError.d.ts.map +1 -0
  152. package/dist/src/utils/GGTestError.js +26 -0
  153. package/dist/src/utils/GGTestError.js.map +1 -0
  154. package/dist/src/utils/captureStack.d.ts +9 -0
  155. package/dist/src/utils/captureStack.d.ts.map +1 -0
  156. package/dist/src/utils/captureStack.js +51 -0
  157. package/dist/src/utils/captureStack.js.map +1 -0
  158. package/dist/tsconfig.publish.tsbuildinfo +1 -0
  159. package/package.json +66 -0
  160. package/src/GGBundleTest.ts +89 -0
  161. package/src/GGTest.ts +318 -0
  162. package/src/GGTestContext.ts +74 -0
  163. package/src/GGTestRunner.ts +308 -0
  164. package/src/GGTestRuntime.ts +265 -0
  165. package/src/GGTestRuntimeWorker.ts +159 -0
  166. package/src/GGTestSharedRef.ts +116 -0
  167. package/src/GGTestkitExtensionsDiscovery.ts +26 -0
  168. package/src/IGGLocalDiscoveryServer.ts +16 -0
  169. package/src/callOn/GGCallOnSelector.ts +61 -0
  170. package/src/callOn/GGContractClass.implement.ts +43 -0
  171. package/src/callOn/GGTestActionForLocatorOnCall.ts +134 -0
  172. package/src/callOn/TestableIPC.ts +81 -0
  173. package/src/callOn/callOn.ts +224 -0
  174. package/src/callOn/registerOnCallHandler.ts +123 -0
  175. package/src/index-node.ts +64 -0
  176. package/src/mockable/GGMockable.ts +22 -0
  177. package/src/mockable/GGMockableCall.ts +45 -0
  178. package/src/mockable/GGMockableIPC.ts +20 -0
  179. package/src/mockable/GGMockableInterceptor.ts +44 -0
  180. package/src/mockable/GGMockableInterceptorsServer.ts +69 -0
  181. package/src/mockable/mockable.ts +71 -0
  182. package/src/runner/InlineRunner.ts +47 -0
  183. package/src/runner/IsolatedRunner.ts +179 -0
  184. package/src/runner/RuntimeRunner.ts +15 -0
  185. package/src/runner/WorkerRunner.ts +179 -0
  186. package/src/runner/isolated-loader.mjs +91 -0
  187. package/src/runner/worker-loader.mjs +49 -0
  188. package/src/testers/GGCallInterceptor.ts +224 -0
  189. package/src/testers/GGMockWith.ts +92 -0
  190. package/src/testers/GGSpyWith.ts +115 -0
  191. package/src/testers/GGTestAction.ts +333 -0
  192. package/src/testers/GGTestComponent.ts +16 -0
  193. package/src/testers/GGTestSelector.ts +223 -0
  194. package/src/testers/IGGTestInterceptor.ts +11 -0
  195. package/src/testers/IGGTestWith.ts +15 -0
  196. package/src/testers/RuntimeSelector.ts +151 -0
  197. package/src/tsconfig.json +17 -0
  198. package/src/utils/GGExpectations.ts +78 -0
  199. package/src/utils/GGTestError.ts +37 -0
  200. package/src/utils/captureStack.ts +54 -0
@@ -0,0 +1,333 @@
1
+ import {IGGTestWith} from "./IGGTestWith";
2
+ import {IGGTestInterceptor} from "./IGGTestInterceptor";
3
+ import {GGLog} from "@grest-ts/logger";
4
+ import {LOG_COLORS} from "@grest-ts/logger-console";
5
+ import {captureStackSourceFile} from "../utils/captureStack";
6
+ import {GGContext} from "@grest-ts/context";
7
+ import {GG_TRACE} from "@grest-ts/trace";
8
+ import {Raw} from "@grest-ts/schema";
9
+ import {DeepPartial} from "@grest-ts/common";
10
+ import {GGExpectations} from "../utils/GGExpectations";
11
+
12
+ interface WaitForInterceptor {
13
+ interceptor: IGGTestInterceptor;
14
+ timeout: number;
15
+ }
16
+
17
+ /**
18
+ * Configuration for a test action's logging behavior.
19
+ */
20
+ export interface GGTestActionConfig {
21
+ /** If true, the action has no response to log (e.g., fire-and-forget WebSocket message) */
22
+ noResponse: boolean;
23
+ /** Data used for logging the action */
24
+ logData: {
25
+ /** Description shown in logs, e.g., "[POST /api/users]" or "[Config MyConfig.timeout]" */
26
+ message: string;
27
+ /** Optional context to display (e.g., auth, language) */
28
+ context?: any;
29
+ /** Optional request payload to log when executing */
30
+ request?: any
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Base class for test actions that can be awaited and chained with expectations.
36
+ *
37
+ * Test actions are PromiseLike objects that execute when awaited. They support
38
+ * attaching interceptors (mocks, log expectations, spies) that are validated
39
+ * during execution.
40
+ *
41
+ * @example
42
+ * // Simple action - just await to execute
43
+ * const user = await client.getUser(123);
44
+ *
45
+ * // With sync expectation - mock must be called during the action
46
+ * await client.createUser({name: "Alice"})
47
+ * .with(mockEmailService.sendWelcome.toBeCalledOnce());
48
+ *
49
+ * // With async expectation - waits for event after action completes
50
+ * await client.triggerAsyncJob()
51
+ * .waitFor(t.logs.expect("Job completed"), 10000);
52
+ *
53
+ * @typeParam T - The type returned when the action completes
54
+ */
55
+ export abstract class GGTestAction<T> implements Promise<T> {
56
+
57
+ readonly [Symbol.toStringTag]: string = "GGTestAction";
58
+
59
+ protected readonly ctx: GGContext;
60
+ private readonly config: GGTestActionConfig;
61
+ protected readonly interceptors: IGGTestInterceptor[] = [];
62
+ protected readonly _waitForInterceptors: WaitForInterceptor[] = [];
63
+ private readonly definedInSourceFile: string;
64
+
65
+ protected readonly responseExpectations: GGExpectations<any> = new GGExpectations()
66
+
67
+ constructor(ctx: GGContext, config: GGTestActionConfig) {
68
+ if (ctx === undefined) throw new Error("No ctx provided!")
69
+ this.ctx = ctx;
70
+ this.config = config;
71
+ this.definedInSourceFile = captureStackSourceFile();
72
+ new GGContext("Test").run(() => {
73
+ GG_TRACE.init();
74
+ const separator = "-".repeat(100);
75
+ GGLog.info(this, separator)
76
+ GGLog.info(this,
77
+ this.logMsg("new", this.config.logData.message)
78
+ + (this.config.logData.context ? "\n" + LOG_COLORS.bgOrange + LOG_COLORS.black
79
+ + "Context: " + LOG_COLORS.reset + LOG_COLORS.orange + JSON.stringify(this.config.logData.context) + LOG_COLORS.reset : "")
80
+ + "\n" + this.definedInSourceFile
81
+ );
82
+ });
83
+
84
+ }
85
+
86
+ public toEqual(expectedData: Raw<T>): this {
87
+ this.responseExpectations.toEqual(expectedData as T);
88
+ return this;
89
+ }
90
+
91
+ public toMatchObject(expectedData: DeepPartial<Raw<T>>): this {
92
+ this.responseExpectations.toMatchObject(expectedData as T);
93
+ return this;
94
+ }
95
+
96
+ public toBeUndefined(): this {
97
+ this.responseExpectations.toBeUndefined();
98
+ return this;
99
+ }
100
+
101
+ public toHaveLength(length: number): this {
102
+ this.responseExpectations.toHaveLength(length);
103
+ return this;
104
+ }
105
+
106
+ public arrayToContain<Item extends T extends Array<infer R> ? R : never>(...items: Partial<Raw<Item>>[]): this {
107
+ this.responseExpectations.arrayToContain(...items);
108
+ return this;
109
+ }
110
+
111
+ public arrayToContainEqual<Item extends T extends Array<infer R> ? R : never>(...items: Partial<Raw<Item>>[]): this {
112
+ this.responseExpectations.arrayToContainEqual(...items);
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * Attach expectations that must be satisfied during action execution.
118
+ *
119
+ * Interceptors are registered before the action runs and validated immediately after.
120
+ * If an expectation fails (e.g., mock not called), the test fails before checking
121
+ * the action's response - this surfaces the root cause of failures first.
122
+ *
123
+ * @example
124
+ * await client.createOrder({items: [...]})
125
+ * .with(mockInventory.reserve.toBeCalledOnce())
126
+ * .with(mockPayment.charge.toMatchObject({amount: 100}));
127
+ */
128
+ public with(...expectations: IGGTestWith[]): this {
129
+ for (const expectation of expectations) {
130
+ // Check if this expectation requires async processing
131
+ if (expectation.requiresWaitFor?.()) {
132
+ throw new Error(
133
+ `This interceptor cannot be used with .with() because it handles async events ` +
134
+ `that occur after the HTTP response.\n\n` +
135
+ `SQS message processing happens asynchronously - the message is delivered to the ` +
136
+ `subscriber queue after the HTTP request completes.\n\n` +
137
+ `To verify SQS events, use one of these approaches:\n` +
138
+ ` 1. Use .waitFor() with log verification:\n` +
139
+ ` .waitFor(t.worker.logs.expect(/expected log message/))\n\n` +
140
+ ` 2. Use .waitFor() with the SQS interceptor (waits for async processing):\n` +
141
+ ` .waitFor(SqsResource.spy.toMatchObject({...}))\n`
142
+ );
143
+ }
144
+ this.interceptors.push(expectation.createInterceptor());
145
+ }
146
+ return this;
147
+ }
148
+
149
+ /**
150
+ * Attach an expectation that may be satisfied after the action completes.
151
+ *
152
+ * Use this for async side effects - when the action triggers something that
153
+ * happens later (e.g., background job, delayed log, async notification).
154
+ * The test will poll until the expectation is satisfied or timeout is reached.
155
+ *
156
+ * @param expectation - The expectation to wait for
157
+ * @param timeout - Max time to wait in ms (default: 5000)
158
+ *
159
+ * @example
160
+ * // Action returns immediately, but logs after 100ms
161
+ * await client.triggerBackgroundJob({id: 123})
162
+ * .waitFor(t.logs.expect("Job 123 completed"), 10000);
163
+ */
164
+ public waitFor(expectation: IGGTestWith, timeout: number = 5000): this {
165
+ const interceptor = expectation.createInterceptor();
166
+ this.interceptors.push(interceptor);
167
+ this._waitForInterceptors.push({
168
+ interceptor,
169
+ timeout
170
+ });
171
+ return this;
172
+ }
173
+
174
+ public then<TResult1 = T, TResult2 = never>(
175
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
176
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
177
+ ): Promise<TResult1 | TResult2> {
178
+ return this.execute().then(onfulfilled, onrejected);
179
+ }
180
+
181
+ public catch<TResult = never>(
182
+ onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
183
+ ): Promise<T | TResult> {
184
+ return this.execute().catch(onrejected);
185
+ }
186
+
187
+ public finally(onfinally?: (() => void) | null): Promise<T> {
188
+ return this.execute().finally(onfinally);
189
+ }
190
+
191
+ protected async execute(): Promise<T> {
192
+ return this.ctx.run(async () => {
193
+ GG_TRACE.init();
194
+ GGLog.info(this,
195
+ this.logMsg("execute", this.config.logData.message),
196
+ this.config.logData.request
197
+ )
198
+ let rawResult: tActionRawData = undefined;
199
+ try {
200
+ await Promise.all(this.interceptors.map(i => i.register()));
201
+ rawResult = await this.executeAction()
202
+ await new Promise(resolve => setTimeout(resolve, 25));
203
+ } finally {
204
+ await Promise.all(this.interceptors.map(i => i.unregister()));
205
+ }
206
+
207
+ const waitForSet = new Set(this._waitForInterceptors.map(w => w.interceptor));
208
+
209
+ // Validate non-wait interceptors immediately
210
+ for (const interceptor of this.interceptors) {
211
+ if (!waitForSet.has(interceptor)) {
212
+ await interceptor.validate();
213
+ }
214
+ }
215
+
216
+ // Check for validation errors from non-wait interceptors
217
+ for (const interceptor of this.interceptors) {
218
+ if (!waitForSet.has(interceptor)) {
219
+ const error = interceptor.getMockValidationError();
220
+ if (error) {
221
+ throw error;
222
+ }
223
+ }
224
+ }
225
+
226
+ let result: any = undefined;
227
+ if (!this.config.noResponse) {
228
+ result = await this.processRawResponse(rawResult);
229
+ this.responseExpectations.check(result);
230
+ }
231
+
232
+ if (this._waitForInterceptors.length > 0) {
233
+ await this._waitForAllInterceptors();
234
+ }
235
+
236
+ // Validate wait interceptors after waiting
237
+ for (const {interceptor} of this._waitForInterceptors) {
238
+ await interceptor.validate();
239
+ const error = interceptor.getMockValidationError();
240
+ if (error) {
241
+ throw error;
242
+ }
243
+ }
244
+
245
+ GGLog.info(this,
246
+ this.logMsg("finished", this.config.logData.message),
247
+ this.config.noResponse ? "Result: (void)" : rawResult
248
+ )
249
+
250
+ return result;
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Execute the core action (e.g., make HTTP request, send WebSocket message).
256
+ * Subclasses implement this with their specific action logic.
257
+ *
258
+ * @returns The raw response data (before parsing/transformation)
259
+ */
260
+ protected abstract executeAction(): Promise<tActionRawData>;
261
+
262
+ /**
263
+ * Process and validate the raw response from executeAction().
264
+ * Called AFTER mock validation, so mock errors surface first.
265
+ *
266
+ * Subclasses implement this to:
267
+ * - Parse the raw response into the expected type T
268
+ * - Check response expectations (e.g., toMatchObject, toEqual)
269
+ *
270
+ * @param result - Raw response from executeAction()
271
+ * @returns The parsed/validated result of type T
272
+ */
273
+ protected processRawResponse(result: tActionRawData): Promise<T> {
274
+ return result;
275
+ }
276
+
277
+ private async _waitForAllInterceptors(): Promise<void> {
278
+ const pending = new Map(
279
+ this._waitForInterceptors.map(w => [w.interceptor, w.timeout])
280
+ );
281
+
282
+ const checkInterval = 20;
283
+ const actionEndTime = Date.now();
284
+
285
+ return new Promise((resolve, reject) => {
286
+ const check = () => {
287
+ const elapsed = Date.now() - actionEndTime;
288
+
289
+ for (const [interceptor, timeout] of pending) {
290
+ if (interceptor.isCalled()) {
291
+ pending.delete(interceptor);
292
+ } else if (elapsed > timeout) {
293
+ reject(new Error(
294
+ `[Test Failed] Timeout waiting for interceptor after ${timeout}ms`
295
+ ));
296
+ return;
297
+ }
298
+ }
299
+
300
+ if (pending.size === 0) {
301
+ resolve();
302
+ return;
303
+ }
304
+
305
+ setTimeout(check, checkInterval);
306
+ };
307
+
308
+ check();
309
+ });
310
+ }
311
+
312
+ private logMsg(type: "new" | "execute" | "finished", msg: string): string {
313
+ let pref = "";
314
+ let color: string = "";
315
+ if (type === "new") {
316
+ pref = "New test action "
317
+ color = LOG_COLORS.reset + LOG_COLORS.bgBlack + LOG_COLORS.white
318
+ } else if (type === "execute") {
319
+ pref = "Executing test action to "
320
+ color = LOG_COLORS.reset + LOG_COLORS.bgGray + LOG_COLORS.black
321
+ } else if (type === "finished") {
322
+ pref = "Finished test action to "
323
+ color = LOG_COLORS.reset + LOG_COLORS.bgGray + LOG_COLORS.black
324
+ }
325
+ return color + pref + msg + LOG_COLORS.reset;
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Branded type for raw action response data.
331
+ * Used to distinguish raw responses from parsed results in the type system.
332
+ */
333
+ export type tActionRawData = any & { tActionRawData: never }
@@ -0,0 +1,16 @@
1
+ import type {GGTestRunner} from "../GGTestRunner";
2
+
3
+ /**
4
+ * Interface for test components that can be registered with GGTestRunner.
5
+ * Components must accept GGTestRunner as their constructor argument.
6
+ */
7
+ export interface GGTestComponent {
8
+ start?(): Promise<void>;
9
+ afterEach?(): Promise<void>;
10
+ teardown?(): Promise<void>;
11
+ }
12
+
13
+ /**
14
+ * Constructor type for components. All components must accept GGTestRunner as constructor argument.
15
+ */
16
+ export type GGTestComponentType<T extends GGTestComponent> = new (runner: GGTestRunner) => T;
@@ -0,0 +1,223 @@
1
+ import {RuntimeConstructor, RuntimeResult, Selector, SelectorExtensions, ObjectResult, StartResult, RuntimeInput} from "./RuntimeSelector";
2
+ import {GGTestRuntime} from "../GGTestRuntime";
3
+ import type {GGTestRunner} from "../GGTestRunner";
4
+
5
+ /**
6
+ * Internal implementation class for Selector.
7
+ * Works with GGTestRuntime instances directly.
8
+ */
9
+ export class GGTestSelector<T extends RuntimeConstructor[]> {
10
+
11
+ private static readonly extensions = new Map<string, typeof GGTestSelectorExtension>();
12
+
13
+ public readonly runtimes: GGTestRuntime[];
14
+ private readonly extensionCache = new Map<string, unknown>();
15
+
16
+ constructor(runtimes: GGTestRuntime[]) {
17
+ this.runtimes = runtimes;
18
+ }
19
+
20
+ public get length(): number {
21
+ return this.runtimes.length;
22
+ }
23
+
24
+ public static addExtension(factory: typeof GGTestSelectorExtension & { PROPERTY_NAME: string }): void {
25
+ if (this.extensions.has(factory.PROPERTY_NAME)) {
26
+ throw new Error("Extension with name '" + factory.PROPERTY_NAME + "' is already registered!")
27
+ }
28
+ this.extensions.set(factory.PROPERTY_NAME, factory);
29
+ }
30
+
31
+ public static hasExtension(name: string): boolean {
32
+ return this.extensions.has(name);
33
+ }
34
+
35
+ public getExtension<K extends keyof SelectorExtensions<T>>(name: K): SelectorExtensions<T>[K] {
36
+ const cached = this.extensionCache.get(name as string);
37
+ if (cached !== undefined) {
38
+ return cached as SelectorExtensions<T>[K];
39
+ }
40
+
41
+ const extension = GGTestSelector.extensions.get(name);
42
+ if (!extension) {
43
+ throw new Error(`Extension '${String(name)}' is not registered. ` +
44
+ `Make sure the module providing this extension is imported.`);
45
+ }
46
+
47
+ const extensionInstance = new extension(this.runtimes);
48
+ this.extensionCache.set(name as string, extensionInstance);
49
+ return extensionInstance as SelectorExtensions<T>[K];
50
+ }
51
+
52
+ /**
53
+ * Stop all runtimes in this selector.
54
+ */
55
+ public async stop(): Promise<void> {
56
+ for (const runtime of this.runtimes) {
57
+ await runtime.stop();
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Shutdown all runtimes in this selector.
63
+ */
64
+ public async shutdown(): Promise<void> {
65
+ for (const runtime of this.runtimes) {
66
+ await runtime.shutdown();
67
+ }
68
+ }
69
+ }
70
+
71
+ export class GGTestSelectorExtension {
72
+
73
+ protected readonly runner: GGTestRunner
74
+
75
+ constructor(protected readonly runtimes: GGTestRuntime[]) {
76
+ this.runner = this.runtimes[0]?.runner
77
+ }
78
+
79
+ protected async forEachParallel(callback: (runtime: GGTestRuntime) => Promise<void>): Promise<void> {
80
+ const results = await Promise.allSettled(
81
+ this.runtimes.map(runtime => callback(runtime))
82
+ );
83
+ const errors = results
84
+ .filter((r): r is PromiseRejectedResult => r.status === 'rejected')
85
+ .map(r => r.reason);
86
+
87
+ if (errors.length > 0) {
88
+ throw new AggregateError(errors, `${errors.length} runtime(s) failed`);
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Create a proxied Selector that supports:
95
+ * - Indexed access: selector[0]
96
+ * - Extension access: selector.config, selector.logs
97
+ * - Lifecycle methods: selector.stop(), selector.shutdown()
98
+ * - Standard properties: selector.runtimes, selector.length
99
+ */
100
+ export function createSelector<T extends RuntimeConstructor[]>(runtimes: GGTestRuntime[]): Selector<T> {
101
+ const impl = new GGTestSelector<T>(runtimes);
102
+
103
+ return new Proxy(impl as unknown as Selector<T>, {
104
+ get(target, prop) {
105
+ const implTarget = target as unknown as GGTestSelector<T>;
106
+
107
+ // Handle numeric index access: selector[0]
108
+ if (typeof prop === 'string' && /^\d+$/.test(prop)) {
109
+ const index = parseInt(prop, 10);
110
+ if (index >= 0 && index < implTarget.runtimes.length) {
111
+ return createSelector([implTarget.runtimes[index]]);
112
+ }
113
+ return undefined;
114
+ }
115
+
116
+ // Handle extension access: selector.config, selector.logs, etc.
117
+ if (typeof prop === 'string' && GGTestSelector.hasExtension(prop)) {
118
+ return implTarget.getExtension(prop as keyof SelectorExtensions<T>);
119
+ }
120
+
121
+ // Handle methods and properties from impl
122
+ return Reflect.get(implTarget, prop);
123
+ }
124
+ }) as Selector<T>;
125
+ }
126
+
127
+ // ============================================================================
128
+ // Factory functions for different input shapes
129
+ // ============================================================================
130
+
131
+ /**
132
+ * Create result based on input shape.
133
+ * - Single runtime → Selector
134
+ * - Array of runtimes → Selector
135
+ * - Object → ObjectResult with named selectors
136
+ */
137
+ export function createStartResult<T extends RuntimeInput>(
138
+ input: T,
139
+ runtimes: GGTestRuntime[]
140
+ ): StartResult<T> {
141
+ // Object input: { main: MainRuntime, sub: SubRuntime }
142
+ if (isObjectInput(input)) {
143
+ return createObjectResult(input, runtimes) as StartResult<T>;
144
+ }
145
+
146
+ // Array or single runtime → Selector
147
+ return createSelector(runtimes) as StartResult<T>;
148
+ }
149
+
150
+ /**
151
+ * Check if input is an object (not a constructor or array).
152
+ */
153
+ function isObjectInput(input: RuntimeInput): input is Record<string, RuntimeConstructor | RuntimeConstructor[]> {
154
+ return typeof input === 'object' && !Array.isArray(input) && !('NAME' in input);
155
+ }
156
+
157
+ /**
158
+ * Create ObjectResult for object input.
159
+ */
160
+ function createObjectResult<T extends Record<string, RuntimeConstructor | RuntimeConstructor[]>>(
161
+ input: T,
162
+ runtimes: GGTestRuntime[]
163
+ ): ObjectResult<T> {
164
+ const result: Record<string, Selector<any>> = {};
165
+
166
+ // Group runtimes by the key they were registered under
167
+ // We need to track which runtimes belong to which key
168
+ for (const [key, value] of Object.entries(input)) {
169
+ const constructors = Array.isArray(value) ? value : [value];
170
+ const names = constructors.map(c => c.NAME);
171
+ const keyRuntimes = runtimes.filter(r => names.includes(r.name));
172
+ result[key] = createSelector(keyRuntimes);
173
+ }
174
+
175
+ // Add stop/shutdown that affects all runtimes
176
+ (result as any).stop = async () => {
177
+ for (const runtime of runtimes) {
178
+ await runtime.stop();
179
+ }
180
+ };
181
+
182
+ (result as any).shutdown = async () => {
183
+ for (const runtime of runtimes) {
184
+ await runtime.shutdown();
185
+ }
186
+ };
187
+
188
+ return result as ObjectResult<T>;
189
+ }
190
+
191
+ // ============================================================================
192
+ // Legacy factory (for backwards compatibility)
193
+ // ============================================================================
194
+
195
+ /**
196
+ * @deprecated Use createStartResult instead.
197
+ * Create a RuntimeResult for accessing runtimes via get() method.
198
+ */
199
+ export function createRuntimeSelector<T extends RuntimeConstructor[]>(runtimes: GGTestRuntime[]): RuntimeResult<T> {
200
+ return {
201
+ get(runtimeConstructor: RuntimeConstructor): Selector<any> {
202
+ const name = runtimeConstructor.NAME;
203
+ const filtered = runtimes.filter(r => r.name === name);
204
+ return createSelector(filtered);
205
+ },
206
+
207
+ all(): Selector<any> {
208
+ return createSelector(runtimes);
209
+ },
210
+
211
+ async stop(): Promise<void> {
212
+ for (const runtime of runtimes) {
213
+ await runtime.stop();
214
+ }
215
+ },
216
+
217
+ async shutdown(): Promise<void> {
218
+ for (const runtime of runtimes) {
219
+ await runtime.shutdown();
220
+ }
221
+ }
222
+ } as RuntimeResult<T>;
223
+ }
@@ -0,0 +1,11 @@
1
+ export interface IGGTestInterceptor {
2
+ register(): void | Promise<void>;
3
+
4
+ unregister(): void | Promise<void>;
5
+
6
+ validate(): void | Promise<void>;
7
+
8
+ getMockValidationError(): Error | undefined;
9
+
10
+ isCalled(): boolean;
11
+ }
@@ -0,0 +1,15 @@
1
+ import {IGGTestInterceptor} from "./IGGTestInterceptor";
2
+
3
+ export interface IGGTestWith {
4
+ createInterceptor(): IGGTestInterceptor;
5
+
6
+ /**
7
+ * Returns true if this expectation requires async processing and must be used with `.waitFor()`.
8
+ * When true, using with `.with()` will throw an error guiding the developer to use `.waitFor()`.
9
+ *
10
+ * Example: SQS message processing is async (happens after HTTP response), so SQS
11
+ * interceptors require `.waitFor()`.
12
+ */
13
+ requiresWaitFor?(): boolean;
14
+ }
15
+