main
js 143 lines 4.06 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 describe('ReactStartTransitionMultipleRenderers', () => {
13 let act;
14 let container;
15 let React;
16 let ReactDOMClient;
17 let Scheduler;
18 let assertLog;
19 let startTransition;
20 let useOptimistic;
21 let textCache;
22
23 beforeEach(() => {
24 jest.resetModules();
25 React = require('react');
26 ReactDOMClient = require('react-dom/client');
27 Scheduler = require('scheduler');
28 act = require('internal-test-utils').act;
29 assertLog = require('internal-test-utils').assertLog;
30 startTransition = React.startTransition;
31 useOptimistic = React.useOptimistic;
32 container = document.createElement('div');
33 document.body.appendChild(container);
34
35 textCache = new Map();
36 });
37
38 function resolveText(text) {
39 const record = textCache.get(text);
40 if (record === undefined) {
41 const newRecord = {
42 status: 'resolved',
43 value: text,
44 };
45 textCache.set(text, newRecord);
46 } else if (record.status === 'pending') {
47 const thenable = record.value;
48 record.status = 'resolved';
49 record.value = text;
50 thenable.pings.forEach(t => t(text));
51 }
52 }
53
54 function getText(text) {
55 const record = textCache.get(text);
56 if (record === undefined) {
57 const thenable = {
58 pings: [],
59 then(resolve) {
60 if (newRecord.status === 'pending') {
61 thenable.pings.push(resolve);
62 } else {
63 Promise.resolve().then(() => resolve(newRecord.value));
64 }
65 },
66 };
67 const newRecord = {
68 status: 'pending',
69 value: thenable,
70 };
71 textCache.set(text, newRecord);
72 return thenable;
73 } else {
74 switch (record.status) {
75 case 'pending':
76 return record.value;
77 case 'rejected':
78 return Promise.reject(record.value);
79 case 'resolved':
80 return Promise.resolve(record.value);
81 }
82 }
83 }
84
85 function Text({text}) {
86 Scheduler.log(text);
87 return text;
88 }
89
90 afterEach(() => {
91 document.body.removeChild(container);
92 });
93
94 // This test imports multiple reconcilers. Because of how the renderers are
95 // built, it only works when running tests using the actual build artifacts,
96 // not the source files.
97 // @gate !source
98 it('React.startTransition works across multiple renderers', async () => {
99 const ReactNoop = require('react-noop-renderer');
100
101 const setIsPendings = new Set();
102
103 function App() {
104 const [isPending, setIsPending] = useOptimistic(false);
105 setIsPendings.add(setIsPending);
106 return <Text text={isPending ? 'Pending' : 'Not pending'} />;
107 }
108
109 const noopRoot = ReactNoop.createRoot(null);
110 const domRoot = ReactDOMClient.createRoot(container);
111
112 // Run the same component in two separate renderers.
113 await act(() => {
114 noopRoot.render(<App />);
115 domRoot.render(<App />);
116 });
117 assertLog(['Not pending', 'Not pending']);
118 expect(container.textContent).toEqual('Not pending');
119 expect(noopRoot).toMatchRenderedOutput('Not pending');
120
121 await act(() => {
122 startTransition(async () => {
123 // Wait until after an async gap before setting the optimistic state.
124 await getText('Wait before setting isPending');
125 setIsPendings.forEach(setIsPending => setIsPending(true));
126
127 // The optimistic state should not be reverted until the
128 // action completes.
129 await getText('Wait until end of async action');
130 });
131 });
132
133 await act(() => resolveText('Wait before setting isPending'));
134 assertLog(['Pending', 'Pending']);
135 expect(container.textContent).toEqual('Pending');
136 expect(noopRoot).toMatchRenderedOutput('Pending');
137
138 await act(() => resolveText('Wait until end of async action'));
139 assertLog(['Not pending', 'Not pending']);
140 expect(container.textContent).toEqual('Not pending');
141 expect(noopRoot).toMatchRenderedOutput('Not pending');
142 });
143 });