@@ -20,9 +20,15 @@ import * as Scheduler from 'scheduler/unstable_mock';
20
20
21
21
import enqueueTask from './enqueueTask' ;
22
22
23
- let actingUpdatesScopeDepth = 0 ;
23
+ let actingUpdatesScopeDepth : number = 0 ;
24
24
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 > {
26
32
if ( Scheduler . unstable_flushUntilNextPaint === undefined ) {
27
33
throw Error (
28
34
'This version of `act` requires a special mock build of Scheduler.' ,
@@ -46,93 +52,64 @@ export function act<T>(scope: () => Thenable<T>): Thenable<T> {
46
52
global. IS_REACT_ACT_ENVIRONMENT = false ;
47
53
}
48
54
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 ) {
51
101
global . IS_REACT_ACT_ENVIRONMENT = previousIsActEnvironment ;
52
102
}
53
- actingUpdatesScopeDepth -- ;
103
+ actingUpdatesScopeDepth = depth - 1 ;
54
104
55
- if ( actingUpdatesScopeDepth > previousActingUpdatesScopeDepth ) {
105
+ if ( actingUpdatesScopeDepth !== previousActingUpdatesScopeDepth ) {
56
106
// if it's _less than_ previousActingUpdatesScopeDepth, then we can
57
107
// assume the 'other' one has warned
58
- throw new Error (
108
+ Scheduler . unstable_clearLog ( ) ;
109
+ error . message =
59
110
'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 ;
78
113
}
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 ;
106
114
}
107
115
}
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