| 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 | * @jest-environment node |
| 9 | */ |
| 10 | |
| 11 | 'use strict'; |
| 12 | |
| 13 | describe('React hooks DevTools integration', () => { |
| 14 | let React; |
| 15 | let ReactDebugTools; |
| 16 | let ReactTestRenderer; |
| 17 | let act; |
| 18 | let overrideHookState; |
| 19 | let scheduleUpdate; |
| 20 | let scheduleRetry; |
| 21 | let setSuspenseHandler; |
| 22 | let waitForAll; |
| 23 | |
| 24 | global.IS_REACT_ACT_ENVIRONMENT = true; |
| 25 | |
| 26 | beforeEach(() => { |
| 27 | global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { |
| 28 | inject: injected => { |
| 29 | overrideHookState = injected.overrideHookState; |
| 30 | scheduleUpdate = injected.scheduleUpdate; |
| 31 | scheduleRetry = injected.scheduleRetry; |
| 32 | setSuspenseHandler = injected.setSuspenseHandler; |
| 33 | }, |
| 34 | supportsFiber: true, |
| 35 | onCommitFiberRoot: () => {}, |
| 36 | onCommitFiberUnmount: () => {}, |
| 37 | }; |
| 38 | |
| 39 | jest.resetModules(); |
| 40 | |
| 41 | React = require('react'); |
| 42 | ReactDebugTools = require('react-debug-tools'); |
| 43 | ReactTestRenderer = require('react-test-renderer'); |
| 44 | |
| 45 | const InternalTestUtils = require('internal-test-utils'); |
| 46 | waitForAll = InternalTestUtils.waitForAll; |
| 47 | |
| 48 | act = require('internal-test-utils').act; |
| 49 | }); |
| 50 | |
| 51 | it('should support editing useState hooks', async () => { |
| 52 | let setCountFn; |
| 53 | |
| 54 | function MyComponent() { |
| 55 | const [count, setCount] = React.useState(0); |
| 56 | setCountFn = setCount; |
| 57 | return <div>count:{count}</div>; |
| 58 | } |
| 59 | |
| 60 | let renderer; |
| 61 | await act(() => { |
| 62 | renderer = ReactTestRenderer.create(<MyComponent />, { |
| 63 | unstable_isConcurrent: true, |
| 64 | }); |
| 65 | }); |
| 66 | expect(renderer.toJSON()).toEqual({ |
| 67 | type: 'div', |
| 68 | props: {}, |
| 69 | children: ['count:', '0'], |
| 70 | }); |
| 71 | |
| 72 | const fiber = renderer.root.findByType(MyComponent)._currentFiber(); |
| 73 | const tree = ReactDebugTools.inspectHooksOfFiber(fiber); |
| 74 | const stateHook = tree[0]; |
| 75 | expect(stateHook.isStateEditable).toBe(true); |
| 76 | |
| 77 | if (__DEV__) { |
| 78 | await act(() => overrideHookState(fiber, stateHook.id, [], 10)); |
| 79 | expect(renderer.toJSON()).toEqual({ |
| 80 | type: 'div', |
| 81 | props: {}, |
| 82 | children: ['count:', '10'], |
| 83 | }); |
| 84 | |
| 85 | await act(() => setCountFn(count => count + 1)); |
| 86 | expect(renderer.toJSON()).toEqual({ |
| 87 | type: 'div', |
| 88 | props: {}, |
| 89 | children: ['count:', '11'], |
| 90 | }); |
| 91 | } |
| 92 | }); |
| 93 | |
| 94 | it('should support editable useReducer hooks', async () => { |
| 95 | const initialData = {foo: 'abc', bar: 123}; |
| 96 | |
| 97 | function reducer(state, action) { |
| 98 | switch (action.type) { |
| 99 | case 'swap': |
| 100 | return {foo: state.bar, bar: state.foo}; |
| 101 | default: |
| 102 | throw new Error(); |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | let dispatchFn; |
| 107 | function MyComponent() { |
| 108 | const [state, dispatch] = React.useReducer(reducer, initialData); |
| 109 | dispatchFn = dispatch; |
| 110 | return ( |
| 111 | <div> |
| 112 | foo:{state.foo}, bar:{state.bar} |
| 113 | </div> |
| 114 | ); |
| 115 | } |
| 116 | |
| 117 | let renderer; |
| 118 | await act(() => { |
| 119 | renderer = ReactTestRenderer.create(<MyComponent />, { |
| 120 | unstable_isConcurrent: true, |
| 121 | }); |
| 122 | }); |
| 123 | expect(renderer.toJSON()).toEqual({ |
| 124 | type: 'div', |
| 125 | props: {}, |
| 126 | children: ['foo:', 'abc', ', bar:', '123'], |
| 127 | }); |
| 128 | |
| 129 | const fiber = renderer.root.findByType(MyComponent)._currentFiber(); |
| 130 | const tree = ReactDebugTools.inspectHooksOfFiber(fiber); |
| 131 | const reducerHook = tree[0]; |
| 132 | expect(reducerHook.isStateEditable).toBe(true); |
| 133 | |
| 134 | if (__DEV__) { |
| 135 | await act(() => overrideHookState(fiber, reducerHook.id, ['foo'], 'def')); |
| 136 | expect(renderer.toJSON()).toEqual({ |
| 137 | type: 'div', |
| 138 | props: {}, |
| 139 | children: ['foo:', 'def', ', bar:', '123'], |
| 140 | }); |
| 141 | |
| 142 | await act(() => dispatchFn({type: 'swap'})); |
| 143 | expect(renderer.toJSON()).toEqual({ |
| 144 | type: 'div', |
| 145 | props: {}, |
| 146 | children: ['foo:', '123', ', bar:', 'def'], |
| 147 | }); |
| 148 | } |
| 149 | }); |
| 150 | |
| 151 | // This test case is based on an open source bug report: |
| 152 | // https://github.com/facebookincubator/redux-react-hook/issues/34#issuecomment-466693787 |
| 153 | it('should handle interleaved stateful hooks (e.g. useState) and non-stateful hooks (e.g. useContext)', async () => { |
| 154 | const MyContext = React.createContext(1); |
| 155 | |
| 156 | let setStateFn; |
| 157 | function useCustomHook() { |
| 158 | const context = React.useContext(MyContext); |
| 159 | const [state, setState] = React.useState({count: context}); |
| 160 | React.useDebugValue(state.count); |
| 161 | setStateFn = setState; |
| 162 | return state.count; |
| 163 | } |
| 164 | |
| 165 | function MyComponent() { |
| 166 | const count = useCustomHook(); |
| 167 | return <div>count:{count}</div>; |
| 168 | } |
| 169 | |
| 170 | let renderer; |
| 171 | await act(() => { |
| 172 | renderer = ReactTestRenderer.create(<MyComponent />, { |
| 173 | unstable_isConcurrent: true, |
| 174 | }); |
| 175 | }); |
| 176 | expect(renderer.toJSON()).toEqual({ |
| 177 | type: 'div', |
| 178 | props: {}, |
| 179 | children: ['count:', '1'], |
| 180 | }); |
| 181 | |
| 182 | const fiber = renderer.root.findByType(MyComponent)._currentFiber(); |
| 183 | const tree = ReactDebugTools.inspectHooksOfFiber(fiber); |
| 184 | const stateHook = tree[0].subHooks[1]; |
| 185 | expect(stateHook.isStateEditable).toBe(true); |
| 186 | |
| 187 | if (__DEV__) { |
| 188 | await act(() => overrideHookState(fiber, stateHook.id, ['count'], 10)); |
| 189 | expect(renderer.toJSON()).toEqual({ |
| 190 | type: 'div', |
| 191 | props: {}, |
| 192 | children: ['count:', '10'], |
| 193 | }); |
| 194 | await act(() => setStateFn(state => ({count: state.count + 1}))); |
| 195 | expect(renderer.toJSON()).toEqual({ |
| 196 | type: 'div', |
| 197 | props: {}, |
| 198 | children: ['count:', '11'], |
| 199 | }); |
| 200 | } |
| 201 | }); |
| 202 | |
| 203 | it('should support overriding suspense in legacy mode', async () => { |
| 204 | if (__DEV__) { |
| 205 | // Lock the first render |
| 206 | setSuspenseHandler(() => true); |
| 207 | } |
| 208 | |
| 209 | function MyComponent() { |
| 210 | return 'Done'; |
| 211 | } |
| 212 | let renderer; |
| 213 | await act(() => { |
| 214 | renderer = ReactTestRenderer.create( |
| 215 | <div> |
| 216 | <React.Suspense fallback={'Loading'}> |
| 217 | <MyComponent /> |
| 218 | </React.Suspense> |
| 219 | </div>, |
| 220 | {unstable_isConcurrent: true}, |
| 221 | ); |
| 222 | }); |
| 223 | const fiber = renderer.root._currentFiber().child; |
| 224 | if (__DEV__) { |
| 225 | // First render was locked |
| 226 | expect(renderer.toJSON().children).toEqual(['Loading']); |
| 227 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 228 | expect(renderer.toJSON().children).toEqual(['Loading']); |
| 229 | |
| 230 | // Release the lock |
| 231 | setSuspenseHandler(() => false); |
| 232 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 233 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 234 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 235 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 236 | |
| 237 | // Lock again |
| 238 | setSuspenseHandler(() => true); |
| 239 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 240 | expect(renderer.toJSON().children).toEqual(['Loading']); |
| 241 | |
| 242 | // Release the lock again |
| 243 | setSuspenseHandler(() => false); |
| 244 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 245 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 246 | |
| 247 | // Ensure it checks specific fibers. |
| 248 | setSuspenseHandler(f => f === fiber || f === fiber.alternate); |
| 249 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 250 | expect(renderer.toJSON().children).toEqual(['Loading']); |
| 251 | setSuspenseHandler(f => f !== fiber && f !== fiber.alternate); |
| 252 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 253 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 254 | } else { |
| 255 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 256 | } |
| 257 | }); |
| 258 | |
| 259 | it('should support overriding suspense in concurrent mode', async () => { |
| 260 | if (__DEV__) { |
| 261 | // Lock the first render |
| 262 | setSuspenseHandler(() => true); |
| 263 | } |
| 264 | |
| 265 | function MyComponent() { |
| 266 | return 'Done'; |
| 267 | } |
| 268 | |
| 269 | const renderer = await act(() => |
| 270 | ReactTestRenderer.create( |
| 271 | <div> |
| 272 | <React.Suspense fallback={'Loading'}> |
| 273 | <MyComponent /> |
| 274 | </React.Suspense> |
| 275 | </div>, |
| 276 | {unstable_isConcurrent: true}, |
| 277 | ), |
| 278 | ); |
| 279 | |
| 280 | await waitForAll([]); |
| 281 | // Ensure we timeout any suspense time. |
| 282 | jest.advanceTimersByTime(1000); |
| 283 | const fiber = renderer.root._currentFiber().child; |
| 284 | if (__DEV__) { |
| 285 | // First render was locked |
| 286 | expect(renderer.toJSON().children).toEqual(['Loading']); |
| 287 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 288 | expect(renderer.toJSON().children).toEqual(['Loading']); |
| 289 | |
| 290 | // Release the lock |
| 291 | setSuspenseHandler(() => false); |
| 292 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 293 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 294 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 295 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 296 | |
| 297 | // Lock again |
| 298 | setSuspenseHandler(() => true); |
| 299 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 300 | expect(renderer.toJSON().children).toEqual(['Loading']); |
| 301 | |
| 302 | // Release the lock again |
| 303 | setSuspenseHandler(() => false); |
| 304 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 305 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 306 | |
| 307 | // Ensure it checks specific fibers. |
| 308 | setSuspenseHandler(f => f === fiber || f === fiber.alternate); |
| 309 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 310 | expect(renderer.toJSON().children).toEqual(['Loading']); |
| 311 | setSuspenseHandler(f => f !== fiber && f !== fiber.alternate); |
| 312 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 313 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 314 | } else { |
| 315 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 316 | } |
| 317 | |
| 318 | if (scheduleRetry) { |
| 319 | // Lock again, synchronously |
| 320 | setSuspenseHandler(() => true); |
| 321 | await act(() => scheduleUpdate(fiber)); // Re-render |
| 322 | expect(renderer.toJSON().children).toEqual(['Loading']); |
| 323 | |
| 324 | // Release the lock again but this time using retry lane |
| 325 | setSuspenseHandler(() => false); |
| 326 | await act(() => scheduleRetry(fiber)); // Re-render |
| 327 | expect(renderer.toJSON().children).toEqual(['Done']); |
| 328 | } |
| 329 | }); |
| 330 | }); |