@@ -20,9 +20,15 @@ import * as Scheduler from 'scheduler/unstable_mock';
2020
2121import enqueueTask from './enqueueTask' ;
2222
23- let actingUpdatesScopeDepth = 0 ;
23+ let actingUpdatesScopeDepth : number = 0 ;
2424
25- export function act < T > ( scope : ( ) = > Thenable < T > ) : Thenable < T > {
25+ async function waitForMicrotasks ( ) {
26+ return new Promise ( resolve => {
27+ enqueueTask ( ( ) => resolve ( ) ) ;
28+ } ) ;
29+ }
30+
31+ export async function act < T > ( scope : ( ) = > Thenable < T > ) : Thenable < T > {
2632 if ( Scheduler . unstable_flushUntilNextPaint === undefined ) {
2733 throw Error (
2834 'This version of `act` requires a special mock build of Scheduler.' ,
@@ -46,93 +52,64 @@ export function act<T>(scope: () => Thenable<T>): Thenable<T> {
4652 global. IS_REACT_ACT_ENVIRONMENT = false ;
4753 }
4854
49- const unwind = ( ) => {
50- if ( actingUpdatesScopeDepth === 1 ) {
55+ // Create the error object before doing any async work, to get a better
56+ // stack trace.
57+ const error = new Error ( ) ;
58+ Error . captureStackTrace ( error , act ) ;
59+
60+ // Call the provided scope function after an async gap. This is an extra
61+ // precaution to ensure that our tests do not accidentally rely on the act
62+ // scope adding work to the queue synchronously. We don't do this in the
63+ // public version of `act`, though we maybe should in the future.
64+ await waitForMicrotasks ( ) ;
65+
66+ try {
67+ const result = await scope ( ) ;
68+
69+ do {
70+ // Wait until end of current task/microtask.
71+ await waitForMicrotasks ( ) ;
72+
73+ // $FlowFixMe: Flow doesn't know about global Jest object
74+ if ( jest . isEnvironmentTornDown ( ) ) {
75+ error . message =
76+ 'The Jest environment was torn down before `act` completed. This ' +
77+ 'probably means you forgot to `await` an `act` call.' ;
78+ throw error ;
79+ }
80+
81+ if ( ! Scheduler . unstable_hasPendingWork ( ) ) {
82+ // $FlowFixMe: Flow doesn't know about global Jest object
83+ jest . runOnlyPendingTimers ( ) ;
84+ if ( Scheduler . unstable_hasPendingWork ( ) ) {
85+ // Committing a fallback scheduled additional work. Continue flushing.
86+ } else {
87+ // There's no pending work, even after both the microtask queue
88+ // and the timer queue are empty. Stop flushing.
89+ break ;
90+ }
91+ }
92+ // flushUntilNextPaint stops when React yields execution. Allow microtasks
93+ // queue to flush before continuing.
94+ Scheduler . unstable_flushUntilNextPaint ( ) ;
95+ } while ( true ) ;
96+
97+ return result ;
98+ } finally {
99+ const depth = actingUpdatesScopeDepth ;
100+ if ( depth === 1 ) {
51101 global . IS_REACT_ACT_ENVIRONMENT = previousIsActEnvironment ;
52102 }
53- actingUpdatesScopeDepth -- ;
103+ actingUpdatesScopeDepth = depth - 1 ;
54104
55- if ( actingUpdatesScopeDepth > previousActingUpdatesScopeDepth ) {
105+ if ( actingUpdatesScopeDepth !== previousActingUpdatesScopeDepth ) {
56106 // if it's _less than_ previousActingUpdatesScopeDepth, then we can
57107 // assume the 'other' one has warned
58- throw new Error (
108+ Scheduler . unstable_clearLog ( ) ;
109+ error . message =
59110 'You seem to have overlapping act() calls, this is not supported. ' +
60- 'Be sure to await previous act() calls before making a new one. ' ,
61- ) ;
62- }
63- } ;
64-
65- // TODO: This would be way simpler if we used async/await.
66- try {
67- const result = scope ( ) ;
68- if (
69- typeof result !== 'object' ||
70- result === null ||
71- typeof ( result : any ) . then !== 'function'
72- ) {
73- throw new Error (
74- 'The internal version of `act` used in the React repo must be passed ' +
75- "an async function, even if doesn't await anything. This is a " +
76- 'temporary limitation that will soon be fixed.' ,
77- ) ;
111+ 'Be sure to await previous act() calls before making a new one. ' ;
112+ throw error ;
78113 }
79- const thenableResult : Thenable < T > = (result: any);
80-
81- return {
82- then ( resolve : T => mixed , reject : mixed => mixed ) {
83- thenableResult . then (
84- returnValue => {
85- flushActWork (
86- ( ) => {
87- unwind ( ) ;
88- resolve ( returnValue ) ;
89- } ,
90- error => {
91- unwind ( ) ;
92- reject ( error ) ;
93- } ,
94- ) ;
95- } ,
96- error => {
97- unwind ( ) ;
98- reject ( error ) ;
99- } ,
100- ) ;
101- } ,
102- } ;
103- } catch ( error ) {
104- unwind ( ) ;
105- throw error ;
106114 }
107115}
108-
109- function flushActWork ( resolve : ( ) => void , reject : ( error : any ) => void ) {
110- if ( Scheduler . unstable_hasPendingWork ( ) ) {
111- try {
112- Scheduler . unstable_flushUntilNextPaint ( ) ;
113- } catch (error) {
114- reject ( error ) ;
115- return ;
116- }
117-
118- // If Scheduler yields while there's still work, it's so that we can
119- // unblock the main thread (e.g. for paint or for microtasks). Yield to
120- // the main thread and continue in a new task.
121- enqueueTask(() => flushActWork ( resolve , reject ) ) ;
122- return ;
123- }
124-
125- // Once the scheduler queue is empty, run all the timers. The purpose of this
126- // is to force any pending fallbacks to commit. The public version of act does
127- // this with dev-only React runtime logic, but since our internal act needs to
128- // work production builds of React, we have to cheat.
129- // $FlowFixMe: Flow doesn't know about global Jest object
130- jest . runOnlyPendingTimers ( ) ;
131- if ( Scheduler . unstable_hasPendingWork ( ) ) {
132- // Committing a fallback scheduled additional work. Continue flushing.
133- flushActWork ( resolve , reject ) ;
134- return ;
135- }
136-
137- resolve ( ) ;
138- }
0 commit comments