15
import * as Scheduler from 'scheduler';
16
17
// Keep in sync with ReactDOM.js, and ReactTestUtils.js:
18
-const [
19
- /* eslint-disable no-unused-vars */
20
- getInstanceFromNode,
21
- getNodeFromInstance,
22
- getFiberCurrentPropsFromNode,
23
- enqueueStateRestore,
24
- restoreStateIfNeeded,
25
- /* eslint-enable no-unused-vars */
26
- flushPassiveEffects,
27
- IsThisRendererActing,
28
-] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
18
+const EventInternals =
19
+ ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
20
+// const getInstanceFromNode = EventInternals[0];
21
+// const getNodeFromInstance = EventInternals[1];
22
+// const getFiberCurrentPropsFromNode = EventInternals[2];
23
+// const enqueueStateRestore = EventInternals[3];
24
+// const restoreStateIfNeeded = EventInternals[4];
25
+// const flushPassiveEffects = EventInternals[5];
26
+const IsThisRendererActing = EventInternals[6];
27
28
const batchedUpdates = ReactDOM.unstable_batchedUpdates;
29
30
const {IsSomeRendererActing} = ReactSharedInternals;
31
34
-// this implementation should be exactly the same in
35
-// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
36
-
37
-const isSchedulerMocked =
38
- typeof Scheduler.unstable_flushAllWithoutAsserting === 'function';
39
-const flushWork =
40
- Scheduler.unstable_flushAllWithoutAsserting ||
41
- function() {
42
- let didFlushWork = false;
43
- while (flushPassiveEffects()) {
44
- didFlushWork = true;
45
- }
46
-
47
- return didFlushWork;
48
- };
49
-
50
-function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
51
- try {
52
- flushWork();
53
- enqueueTask(() => {
54
- if (flushWork()) {
55
- flushWorkAndMicroTasks(onDone);
56
- } else {
57
- onDone();
58
- }
59
- });
60
- } catch (err) {
61
- onDone(err);
62
- }
63
-}
64
-
65
-// we track the 'depth' of the act() calls with this counter,
66
-// so we can tell if any async act() calls try to run in parallel.
32
+// This version of `act` is only used by our tests. Unlike the public version
33
+// of `act`, it's designed to work identically in both production and
34
+// development. It may have slightly different behavior from the public
35
+// version, too, since our constraints in our test suite are not the same as
36
+// those of developers using React — we're testing React itself, as opposed to
37
+// building an app with React.
38
39
let actingUpdatesScopeDepth = 0;
69
-let didWarnAboutUsingActInProd = false;
40
71
-function act(callback: () => Thenable<mixed>): Thenable<void> {
72
- if (!__DEV__) {
73
- if (didWarnAboutUsingActInProd === false) {
74
- didWarnAboutUsingActInProd = true;
75
- // eslint-disable-next-line react-internal/no-production-logging
76
- console.error(
77
- 'act(...) is not supported in production builds of React, and might not behave as expected.',
78
- );
79
- }
41
+export function unstable_concurrentAct(scope: () => Thenable<mixed> | void) {
42
+ if (Scheduler.unstable_flushAllWithoutAsserting === undefined) {
43
+ throw Error(
44
+ 'This version of `act` requires a special mock build of Scheduler.',
45
+ );
46
+ }
47
+ if (setTimeout._isMockFunction !== true) {
48
+ throw Error(
49
+ "This version of `act` requires Jest's timer mocks " +
50
+ '(i.e. jest.useFakeTimers).',
51
+ );
52
}
81
- const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
82
- actingUpdatesScopeDepth++;
53
54
+ const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
55
const previousIsSomeRendererActing = IsSomeRendererActing.current;
56
const previousIsThisRendererActing = IsThisRendererActing.current;
57
IsSomeRendererActing.current = true;
58
IsThisRendererActing.current = true;
59
+ actingUpdatesScopeDepth++;
60
89
- function onDone() {
61
+ const unwind = () => {
62
actingUpdatesScopeDepth--;
63
IsSomeRendererActing.current = previousIsSomeRendererActing;
64
IsThisRendererActing.current = previousIsThisRendererActing;
65
+
66
if (__DEV__) {
67
if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
95
- // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
68
+ // if it's _less than_ previousActingUpdatesScopeDepth, then we can
69
+ // assume the 'other' one has warned
70
console.error(
71
'You seem to have overlapping act() calls, this is not supported. ' +
72
'Be sure to await previous act() calls before making a new one. ',
73
);
74
}
75
}
102
- }
76
+ };
77
104
- let result;
78
+ // TODO: This would be way simpler if 1) we required a promise to be
79
+ // returned and 2) we could use async/await. Since it's only our used in
80
+ // our test suite, we should be able to.
81
try {
106
- result = batchedUpdates(callback);
107
- } catch (error) {
108
- // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth
109
- onDone();
110
- throw error;
111
- }
112
-
113
- if (
114
- result !== null &&
115
- typeof result === 'object' &&
116
- typeof result.then === 'function'
117
- ) {
118
- // setup a boolean that gets set to true only
119
- // once this act() call is await-ed
120
- let called = false;
121
- if (__DEV__) {
122
- if (typeof Promise !== 'undefined') {
123
- //eslint-disable-next-line no-undef
124
- Promise.resolve()
125
- .then(() => {})
126
- .then(() => {
127
- if (called === false) {
128
- console.error(
129
- 'You called act(async () => ...) without await. ' +
130
- 'This could lead to unexpected testing behaviour, interleaving multiple act ' +
131
- 'calls and mixing their scopes. You should - await act(async () => ...);',
82
+ const thenable = batchedUpdates(scope);
83
+ if (
84
+ typeof thenable === 'object' &&
85
+ thenable !== null &&
86
+ typeof thenable.then === 'function'
87
+ ) {
88
+ return {
89
+ then(resolve: () => void, reject: (error: mixed) => void) {
90
+ thenable.then(
91
+ () => {
92
+ flushActWork(
93
+ () => {
94
+ unwind();
95
+ resolve();
96
+ },
97
+ error => {
98
+ unwind();
99
+ reject(error);
100
+ },
101
);
133
- }
134
- });
135
- }
136
- }
137
-
138
- // in the async case, the returned thenable runs the callback, flushes
139
- // effects and microtasks in a loop until flushPassiveEffects() === false,
140
- // and cleans up
141
- return {
142
- then(resolve, reject) {
143
- called = true;
144
- result.then(
145
- () => {
146
- if (
147
- actingUpdatesScopeDepth > 1 ||
148
- (isSchedulerMocked === true &&
149
- previousIsSomeRendererActing === true)
150
- ) {
151
- onDone();
152
- resolve();
153
- return;
154
- }
155
- // we're about to exit the act() scope,
156
- // now's the time to flush tasks/effects
157
- flushWorkAndMicroTasks((err: ?Error) => {
158
- onDone();
159
- if (err) {
160
- reject(err);
161
- } else {
162
- resolve();
163
- }
164
- });
165
- },
166
- err => {
167
- onDone();
168
- reject(err);
169
- },
170
- );
171
- },
172
- };
173
- } else {
174
- if (__DEV__) {
175
- if (result !== undefined) {
176
- console.error(
177
- 'The callback passed to act(...) function ' +
178
- 'must return undefined, or a Promise. You returned %s',
179
- result,
180
- );
102
+ },
103
+ error => {
104
+ unwind();
105
+ reject(error);
106
+ },
107
+ );
108
+ },
109
+ };
110
+ } else {
111
+ try {
112
+ // TODO: Let's not support non-async scopes at all in our tests. Need to
113
+ // migrate existing tests.
114
+ let didFlushWork;
115
+ do {
116
+ didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
117
+ } while (didFlushWork);
118
+ } finally {
119
+ unwind();
120
}
121
}
122
+ } catch (error) {
123
+ unwind();
124
+ throw error;
125
+ }
126
+}
127
184
- // flush effects until none remain, and cleanup
128
+function flushActWork(resolve, reject) {
129
+ // TODO: Run timers to flush suspended fallbacks
130
+ // jest.runOnlyPendingTimers();
131
+ enqueueTask(() => {
132
try {
186
- if (
187
- actingUpdatesScopeDepth === 1 &&
188
- (isSchedulerMocked === false || previousIsSomeRendererActing === false)
189
- ) {
190
- // we're about to exit the act() scope,
191
- // now's the time to flush effects
192
- flushWork();
133
+ const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
134
+ if (didFlushWork) {
135
+ flushActWork(resolve, reject);
136
+ } else {
137
+ resolve();
138
}
194
- onDone();
195
- } catch (err) {
196
- onDone();
197
- throw err;
139
+ } catch (error) {
140
+ reject(error);
141
}
199
-
200
- // in the sync case, the returned thenable only warns *if* await-ed
201
- return {
202
- then(resolve) {
203
- if (__DEV__) {
204
- console.error(
205
- 'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
206
- );
207
- }
208
- resolve();
209
- },
210
- };
211
- }
142
+ });
143
}
213
-
214
-export default act;