Skip to content

Commit

Permalink
Internal test helpers: Use Node's MessageChannel to queue task (#26345)
Browse files Browse the repository at this point in the history
To wait for the microtask queue to empty, our internal test helpers
schedule an arbitrary task using `setImmediate`. It doesn't matter what
kind of task it is, only that it's a separate task from the current one,
because by the time it fires, the microtasks for the current event will
have already been processed.

The issue with `setImmediate` is that Jest mocks it. Which can lead to
weird behavior.

I've changed it to instead use a message event, via the MessageChannel
implementation exposed by the `node:worker_threads` module.

We should consider doing this in the public implementation of `act`,
too.
  • Loading branch information
acdlite authored Mar 8, 2023
1 parent f36ab0e commit 8364377
Show file tree
Hide file tree
Showing 3 changed files with 12 additions and 39 deletions.
42 changes: 4 additions & 38 deletions packages/internal-test-utils/enqueueTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,10 @@
* @flow
*/

let didWarnAboutMessageChannel = false;
let enqueueTaskImpl = null;
const {MessageChannel} = require('node:worker_threads');

// Same as shared/enqeuueTask, but while that one used by the public
// implementation of `act`, this is only used by our internal testing helpers.
export default function enqueueTask(task: () => void): void {
if (enqueueTaskImpl === null) {
try {
// read require off the module object to get around the bundlers.
// we don't want them to detect a require and bundle a Node polyfill.
const requireString = ('require' + Math.random()).slice(0, 7);
const nodeRequire = module && module[requireString];
// assuming we're in node, let's try to get node's
// version of setImmediate, bypassing fake timers if any.
enqueueTaskImpl = nodeRequire.call(module, 'timers').setImmediate;
} catch (_err) {
// we're in a browser
// we can't use regular timers because they may still be faked
// so we try MessageChannel+postMessage instead
enqueueTaskImpl = function (callback: () => void) {
if (__DEV__) {
if (didWarnAboutMessageChannel === false) {
didWarnAboutMessageChannel = true;
if (typeof MessageChannel === 'undefined') {
console['error'](
'This browser does not have a MessageChannel implementation, ' +
'so enqueuing tasks via await act(async () => ...) will fail. ' +
'Please file an issue at https://github.com/facebook/react/issues ' +
'if you encounter this warning.',
);
}
}
}
const channel = new MessageChannel();
channel.port1.onmessage = callback;
channel.port2.postMessage(undefined);
};
}
}
return enqueueTaskImpl(task);
const channel = new MessageChannel();
channel.port1.onmessage = task;
channel.port2.postMessage(undefined);
}
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ Task 1 [Normal] │ █████████
taskId++;
const task = scheduleCallback(NormalPriority, () => {});
cancelCallback(task);
await waitForAll([]);
Scheduler.unstable_flushAll();
}

expect(console.error).toHaveBeenCalledTimes(1);
Expand Down
7 changes: 7 additions & 0 deletions scripts/flow/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,10 @@ declare module 'async_hooks' {
enterWith(store: T): void;
}
}

declare module 'node:worker_threads' {
declare class MessageChannel {
port1: MessagePort;
port2: MessagePort;
}
}

0 comments on commit 8364377

Please sign in to comment.