20
21
import enqueueTask from './enqueueTask';
22
23
-let actingUpdatesScopeDepth = 0;
23
+let actingUpdatesScopeDepth: number = 0;
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> {
32
if (Scheduler.unstable_flushUntilNextPaint === undefined) {
33
throw Error(
34
'This version of `act` requires a special mock build of Scheduler.',
52
global.IS_REACT_ACT_ENVIRONMENT = false;
53
}
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) {
101
global.IS_REACT_ACT_ENVIRONMENT = previousIsActEnvironment;
102
}
53
- actingUpdatesScopeDepth--;
103
+ actingUpdatesScopeDepth = depth - 1;
104
55
- if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
105
+ if (actingUpdatesScopeDepth !== previousActingUpdatesScopeDepth) {
106
// if it's _less than_ previousActingUpdatesScopeDepth, then we can
107
// assume the 'other' one has warned
58
- throw new Error(
108
+ Scheduler.unstable_clearLog();
109
+ error.message =
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;
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;
114
}
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
-}