@@ -192,3 +192,90 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
192
192
}
193
193
}
194
194
}
195
+
196
+ export async function serverAct < T > ( scope : ( ) = > Thenable < T > ) : Thenable < T > {
197
+ // We require every `act` call to assert console logs
198
+ // with one of the assertion helpers. Fails if not empty.
199
+ assertConsoleLogsCleared ( ) ;
200
+
201
+ // $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
202
+ if ( ! jest . isMockFunction ( setTimeout ) ) {
203
+ throw Error (
204
+ "This version of `act` requires Jest's timer mocks " +
205
+ '(i.e. jest.useFakeTimers).' ,
206
+ ) ;
207
+ }
208
+
209
+ // Create the error object before doing any async work, to get a better
210
+ // stack trace.
211
+ const error = new Error ( ) ;
212
+ Error . captureStackTrace ( error , act ) ;
213
+
214
+ // Call the provided scope function after an async gap. This is an extra
215
+ // precaution to ensure that our tests do not accidentally rely on the act
216
+ // scope adding work to the queue synchronously. We don't do this in the
217
+ // public version of `act`, though we maybe should in the future.
218
+ await waitForMicrotasks ( ) ;
219
+
220
+ const errorHandlerNode = function ( err : mixed ) {
221
+ thrownErrors . push ( err ) ;
222
+ } ;
223
+ // We track errors that were logged globally as if they occurred in this scope and then rethrow them.
224
+ if ( typeof process === 'object ') {
225
+ // Node environment
226
+ process . on ( 'uncaughtException ', errorHandlerNode) ;
227
+ } else if (
228
+ typeof window === 'object' &&
229
+ typeof window . addEventListener === 'function'
230
+ ) {
231
+ throw new Error ( 'serverAct is not supported in JSDOM environments' ) ;
232
+ }
233
+
234
+ try {
235
+ const result = await scope ( ) ;
236
+
237
+ do {
238
+ // Wait until end of current task/microtask.
239
+ await waitForMicrotasks ( ) ;
240
+
241
+ // $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
242
+ if ( jest . isEnvironmentTornDown ( ) ) {
243
+ error . message =
244
+ 'The Jest environment was torn down before `act` completed. This ' +
245
+ 'probably means you forgot to `await` an `act` call.' ;
246
+ throw error ;
247
+ }
248
+
249
+ // $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
250
+ const j = jest ;
251
+ if ( j . getTimerCount ( ) > 0 ) {
252
+ // There's a pending timer. Flush it now. We only do this in order to
253
+ // force Suspense fallbacks to display; the fact that it's a timer
254
+ // is an implementation detail. If there are other timers scheduled,
255
+ // those will also fire now, too, which is not ideal. (The public
256
+ // version of `act` doesn't do this.) For this reason, we should try
257
+ // to avoid using timers in our internal tests.
258
+ j . runOnlyPendingTimers ( ) ;
259
+ // If a committing a fallback triggers another update, it might not
260
+ // get scheduled until a microtask. So wait one more time.
261
+ await waitForMicrotasks ( ) ;
262
+ } else {
263
+ break ;
264
+ }
265
+ } while ( true ) ;
266
+
267
+ if ( thrownErrors . length > 0 ) {
268
+ // Rethrow any errors logged by the global error handling.
269
+ const thrownError = aggregateErrors ( thrownErrors ) ;
270
+ thrownErrors . length = 0 ;
271
+ throw thrownError ;
272
+ }
273
+
274
+ return result ;
275
+ } finally {
276
+ if ( typeof process === 'object' ) {
277
+ // Node environment
278
+ process . off ( 'uncaughtException' , errorHandlerNode ) ;
279
+ }
280
+ }
281
+ }
0 commit comments