main
js 147 lines 4.46 KB
Raw
1 let React;
2 let ReactDOM;
3 let ReactDOMClient;
4 let Scheduler;
5 let act;
6 let assertLog;
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)', () => {
14 beforeEach(() => {
15 jest.resetModules();
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 ReactDOM = require('react-dom');
41 ReactDOMClient = require('react-dom/client');
42 Scheduler = require('scheduler');
43 act = require('internal-test-utils').act;
44
45 const InternalTestUtils = require('internal-test-utils');
46 assertLog = InternalTestUtils.assertLog;
47 });
48
49 function Text({text}) {
50 Scheduler.log(text);
51 return text;
52 }
53
54 function getVisibleChildren(element: Element): React$Node {
55 const children = [];
56 let node: any = element.firstChild;
57 while (node) {
58 if (node.nodeType === 1) {
59 if (
60 ((node.tagName !== 'SCRIPT' && node.tagName !== 'script') ||
61 node.hasAttribute('data-meaningful')) &&
62 node.tagName !== 'TEMPLATE' &&
63 node.tagName !== 'template' &&
64 !node.hasAttribute('hidden') &&
65 !node.hasAttribute('aria-hidden')
66 ) {
67 const props: any = {};
68 const attributes = node.attributes;
69 for (let i = 0; i < attributes.length; i++) {
70 if (
71 attributes[i].name === 'id' &&
72 attributes[i].value.includes(':')
73 ) {
74 // We assume this is a React added ID that's a non-visual implementation detail.
75 continue;
76 }
77 props[attributes[i].name] = attributes[i].value;
78 }
79 props.children = getVisibleChildren(node);
80 children.push(
81 require('react').createElement(node.tagName.toLowerCase(), props),
82 );
83 }
84 } else if (node.nodeType === 3) {
85 children.push(node.data);
86 }
87 node = node.nextSibling;
88 }
89 return children.length === 0
90 ? undefined
91 : children.length === 1
92 ? children[0]
93 : children;
94 }
95
96 it('completely exhausts synchronous work queue even if something throws', async () => {
97 function Throws({error}) {
98 throw error;
99 }
100
101 const container1 = document.createElement('div');
102 const root1 = ReactDOMClient.createRoot(container1);
103 const container2 = document.createElement('div');
104 const root2 = ReactDOMClient.createRoot(container2);
105 const container3 = document.createElement('div');
106 const root3 = ReactDOMClient.createRoot(container3);
107
108 await act(async () => {
109 root1.render(<Text text="Hi" />);
110 root2.render(<Text text="Andrew" />);
111 root3.render(<Text text="!" />);
112 });
113 assertLog(['Hi', 'Andrew', '!']);
114
115 const aahh = new Error('AAHH!');
116 const nooo = new Error('Noooooooooo!');
117
118 // Override the global queueMicrotask so we can test the behavior.
119 overrideQueueMicrotask = true;
120 let error;
121 try {
122 await act(() => {
123 ReactDOM.flushSync(() => {
124 root1.render(<Throws error={aahh} />);
125 root2.render(<Throws error={nooo} />);
126 root3.render(<Text text="aww" />);
127 });
128 });
129 } catch (e) {
130 error = e;
131 }
132
133 // The update to root 3 should have finished synchronously, even though the
134 // earlier updates errored.
135 assertLog(['aww']);
136 // Roots 1 and 2 were unmounted.
137 expect(getVisibleChildren(container1)).toEqual(undefined);
138 expect(getVisibleChildren(container2)).toEqual(undefined);
139 expect(getVisibleChildren(container3)).toEqual('aww');
140
141 // In modern environments, React would throw an AggregateError. Because
142 // AggregateError is not available, React throws the first error, then
143 // throws the remaining errors in separate tasks.
144 expect(error).toBe(aahh);
145 await flushFakeMicrotasks();
146 });
147 });