@samitouri / QOS-React-2 / commits / e0bbc26623

Improve tests that deal with microtasks (#26493)

I rewrote some of our tests that deal with microtasks with the aim of making them less coupled to implementation details. This is related to an upcoming change to move update processing into a microtask.

Andrew Clark committed Mar 27, 2023 at 23:17 UTC e0bbc2662301e0d6c9a0cd601c22f7f27e1f56b2
4 files changed +92 -13
packages/internal-test-utils/ReactInternalTestUtils.js
+24
@@ -218,6 +218,30 @@ ${diff(expectedLog, actualLog)}
218 throw error;
219 }
220
221 +export async function waitForDiscrete(expectedLog) {
222 + assertYieldsWereCleared(SchedulerMock);
223 +
224 + // Create the error object before doing any async work, to get a better
225 + // stack trace.
226 + const error = new Error();
227 + Error.captureStackTrace(error, waitForDiscrete);
228 +
229 + // Wait until end of current task/microtask.
230 + await waitForMicrotasks();
231 +
232 + const actualLog = SchedulerMock.unstable_clearLog();
233 + if (equals(actualLog, expectedLog)) {
234 + return;
235 + }
236 +
237 + error.message = `
238 +Expected sequence of events did not occur.
239 +
240 +${diff(expectedLog, actualLog)}
241 +`;
242 + throw error;
243 +}
244 +
245 export function assertLog(expectedLog) {
246 const actualLog = SchedulerMock.unstable_clearLog();
247 if (equals(actualLog, expectedLog)) {
packages/react-dom/src/__tests__/ReactDOMSafariMicrotaskBug-test.js
+35 -11
@@ -13,25 +13,33 @@ let React;
13
14 let ReactDOMClient;
15 let act;
16 +let assertLog;
17 +let Scheduler;
18
19 describe('ReactDOMSafariMicrotaskBug-test', () => {
20 let container;
19 - let flushMicrotasksPrematurely;
21 + let overrideQueueMicrotask;
22 + let flushFakeMicrotasks;
23
24 beforeEach(() => {
25 // In Safari, microtasks don't always run on clean stack.
26 // This setup crudely approximates it.
27 // In reality, the sync flush happens when an iframe is added to the page.
28 // https://github.com/facebook/react/issues/22459
26 - let queue = [];
27 - window.queueMicrotask = function (cb) {
28 - queue.push(cb);
29 + const originalQueueMicrotask = queueMicrotask;
30 + overrideQueueMicrotask = false;
31 + const fakeMicrotaskQueue = [];
32 + global.queueMicrotask = cb => {
33 + if (overrideQueueMicrotask) {
34 + fakeMicrotaskQueue.push(cb);
35 + } else {
36 + originalQueueMicrotask(cb);
37 + }
38 };
30 - flushMicrotasksPrematurely = function () {
31 - while (queue.length > 0) {
32 - const prevQueue = queue;
33 - queue = [];
34 - prevQueue.forEach(cb => cb());
39 + flushFakeMicrotasks = () => {
40 + while (fakeMicrotaskQueue.length > 0) {
41 + const cb = fakeMicrotaskQueue.shift();
42 + cb();
43 }
44 };
45
@@ -40,6 +48,8 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
48 React = require('react');
49 ReactDOMClient = require('react-dom/client');
50 act = require('internal-test-utils').act;
51 + assertLog = require('internal-test-utils').assertLog;
52 + Scheduler = require('scheduler');
53
54 document.body.appendChild(container);
55 });
@@ -55,10 +65,14 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
65 return (
66 <div
67 ref={() => {
68 + overrideQueueMicrotask = true;
69 if (!ran) {
70 ran = true;
71 setState(1);
61 - flushMicrotasksPrematurely();
72 + flushFakeMicrotasks();
73 + Scheduler.log(
74 + 'Content at end of ref callback: ' + container.textContent,
75 + );
76 }
77 }}>
78 {state}
@@ -69,6 +83,7 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
83 await act(() => {
84 root.render(<Foo />);
85 });
86 + assertLog(['Content at end of ref callback: 0']);
87 expect(container.textContent).toBe('1');
88 });
89
@@ -78,8 +93,12 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
93 return (
94 <button
95 onClick={() => {
96 + overrideQueueMicrotask = true;
97 setState(1);
82 - flushMicrotasksPrematurely();
98 + flushFakeMicrotasks();
99 + Scheduler.log(
100 + 'Content at end of click handler: ' + container.textContent,
101 + );
102 }}>
103 {state}
104 </button>
@@ -95,6 +114,11 @@ describe('ReactDOMSafariMicrotaskBug-test', () => {
114 new MouseEvent('click', {bubbles: true}),
115 );
116 });
117 + // This causes the update to flush earlier than usual. This isn't the ideal
118 + // behavior but we use this test to document it. The bug is Safari's, not
119 + // ours, so we just do our best to not crash even though the behavior isn't
120 + // completely correct.
121 + assertLog(['Content at end of click handler: 1']);
122 expect(container.textContent).toBe('1');
123 });
124 });
packages/react-dom/src/events/plugins/__tests__/ChangeEventPlugin-test.js
+3 -2
@@ -16,6 +16,7 @@ let ReactFeatureFlags;
16 let Scheduler;
17 let act;
18 let waitForAll;
19 +let waitForDiscrete;
20 let assertLog;
21
22 const setUntrackedChecked = Object.getOwnPropertyDescriptor(
@@ -65,6 +66,7 @@ describe('ChangeEventPlugin', () => {
66
67 const InternalTestUtils = require('internal-test-utils');
68 waitForAll = InternalTestUtils.waitForAll;
69 + waitForDiscrete = InternalTestUtils.waitForDiscrete;
70 assertLog = InternalTestUtils.assertLog;
71
72 container = document.createElement('div');
@@ -730,8 +732,7 @@ describe('ChangeEventPlugin', () => {
732 );
733
734 // Flush microtask queue.
733 - await null;
734 - assertLog(['render: ']);
735 + await waitForDiscrete(['render: ']);
736 expect(input.value).toBe('');
737 });
738
packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js
+30
@@ -5,6 +5,9 @@ let act;
5 let assertLog;
6 let waitForThrow;
7
8 +let overrideQueueMicrotask;
9 +let flushFakeMicrotasks;
10 +
11 // TODO: Migrate tests to React DOM instead of React Noop
12
13 describe('ReactFlushSync (AggregateError not available)', () => {
@@ -13,6 +16,26 @@ describe('ReactFlushSync (AggregateError not available)', () => {
16
17 global.AggregateError = undefined;
18
19 + // When AggregateError is not available, the errors are rethrown in a
20 + // microtask. This is an implementation detail but we want to test it here
21 + // so override the global one.
22 + const originalQueueMicrotask = queueMicrotask;
23 + overrideQueueMicrotask = false;
24 + const fakeMicrotaskQueue = [];
25 + global.queueMicrotask = cb => {
26 + if (overrideQueueMicrotask) {
27 + fakeMicrotaskQueue.push(cb);
28 + } else {
29 + originalQueueMicrotask(cb);
30 + }
31 + };
32 + flushFakeMicrotasks = () => {
33 + while (fakeMicrotaskQueue.length > 0) {
34 + const cb = fakeMicrotaskQueue.shift();
35 + cb();
36 + }
37 + };
38 +
39 React = require('react');
40 ReactNoop = require('react-noop-renderer');
41 Scheduler = require('scheduler');
@@ -47,6 +70,8 @@ describe('ReactFlushSync (AggregateError not available)', () => {
70 const aahh = new Error('AAHH!');
71 const nooo = new Error('Noooooooooo!');
72
73 + // Override the global queueMicrotask so we can test the behavior.
74 + overrideQueueMicrotask = true;
75 let error;
76 try {
77 ReactNoop.flushSync(() => {
@@ -70,10 +95,15 @@ describe('ReactFlushSync (AggregateError not available)', () => {
95 // AggregateError is not available, React throws the first error, then
96 // throws the remaining errors in separate tasks.
97 expect(error).toBe(aahh);
98 +
99 // TODO: Currently the remaining error is rethrown in an Immediate Scheduler
100 // task, but this may change to a timer or microtask in the future. The
101 // exact mechanism is an implementation detail; they just need to be logged
102 // in the order the occurred.
103 +
104 + // This will start throwing if we change it to rethrow in a microtask.
105 + flushFakeMicrotasks();
106 +
107 await waitForThrow(nooo);
108 });
109 });