| 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 | * @flow |
| 8 | */ |
| 9 | |
| 10 | // Note that this test uses React components declared in the "__source__" directory. |
| 11 | // This is done to control if and how the code is transformed at runtime. |
| 12 | // Do not declare test components within this test file as it is very fragile. |
| 13 | |
| 14 | function expectHookNamesToEqual(map, expectedNamesArray) { |
| 15 | // Slightly hacky since it relies on the iterable order of values() |
| 16 | expect(Array.from(map.values())).toEqual(expectedNamesArray); |
| 17 | } |
| 18 | |
| 19 | function requireText(path, encoding) { |
| 20 | const {existsSync, readFileSync} = require('fs'); |
| 21 | if (existsSync(path)) { |
| 22 | return Promise.resolve(readFileSync(path, encoding)); |
| 23 | } else { |
| 24 | return Promise.reject(`File not found "${path}"`); |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | function initFetchMock() { |
| 29 | const fetchMock = require('jest-fetch-mock'); |
| 30 | fetchMock.enableMocks(); |
| 31 | fetchMock.mockIf(/.+$/, request => { |
| 32 | const url = request.url; |
| 33 | const isLoadingExternalSourceMap = /external\/.*\.map/.test(url); |
| 34 | if (isLoadingExternalSourceMap) { |
| 35 | // Assert that url contains correct query params |
| 36 | expect(url.includes('?foo=bar¶m=some_value')).toBe(true); |
| 37 | const fileSystemPath = url.split('?')[0]; |
| 38 | return requireText(fileSystemPath, 'utf8'); |
| 39 | } |
| 40 | return requireText(url, 'utf8'); |
| 41 | }); |
| 42 | return fetchMock; |
| 43 | } |
| 44 | |
| 45 | describe('parseHookNames', () => { |
| 46 | let fetchMock; |
| 47 | let inspectHooks; |
| 48 | let parseHookNames; |
| 49 | |
| 50 | beforeEach(() => { |
| 51 | jest.resetModules(); |
| 52 | |
| 53 | jest.mock('source-map-support', () => { |
| 54 | console.trace('source-map-support'); |
| 55 | }); |
| 56 | |
| 57 | fetchMock = initFetchMock(); |
| 58 | |
| 59 | inspectHooks = |
| 60 | require('react-debug-tools/src/ReactDebugHooks').inspectHooks; |
| 61 | |
| 62 | // Jest can't run the workerized version of this module. |
| 63 | const { |
| 64 | flattenHooksList, |
| 65 | loadSourceAndMetadata, |
| 66 | } = require('../parseHookNames/loadSourceAndMetadata'); |
| 67 | const parseSourceAndMetadata = |
| 68 | require('../parseHookNames/parseSourceAndMetadata').parseSourceAndMetadata; |
| 69 | parseHookNames = async hooksTree => { |
| 70 | const hooksList = flattenHooksList(hooksTree); |
| 71 | |
| 72 | // Runs in the UI thread so it can share Network cache: |
| 73 | const locationKeyToHookSourceAndMetadata = |
| 74 | await loadSourceAndMetadata(hooksList); |
| 75 | |
| 76 | // Runs in a Worker because it's CPU intensive: |
| 77 | return parseSourceAndMetadata( |
| 78 | hooksList, |
| 79 | locationKeyToHookSourceAndMetadata, |
| 80 | ); |
| 81 | }; |
| 82 | |
| 83 | // Jest (jest-runner?) configures Errors to automatically account for source maps. |
| 84 | // This changes behavior between our tests and the browser. |
| 85 | // Ideally we would clear the prepareStackTrace() method on the Error object, |
| 86 | // but Node falls back to looking for it on the main context's Error constructor, |
| 87 | // which may still be patched. |
| 88 | // To ensure we get the default behavior, override prepareStackTrace ourselves. |
| 89 | // NOTE: prepareStackTrace is called from the error.stack getter, but the getter |
| 90 | // has a recursion breaker which falls back to the default behavior. |
| 91 | Error.prepareStackTrace = (error, trace) => { |
| 92 | return error.stack; |
| 93 | }; |
| 94 | }); |
| 95 | |
| 96 | afterEach(() => { |
| 97 | fetch.resetMocks(); |
| 98 | }); |
| 99 | |
| 100 | async function getHookNamesForComponent(Component, props = {}) { |
| 101 | const hooksTree = inspectHooks(Component, props, undefined); |
| 102 | const hookNames = await parseHookNames(hooksTree); |
| 103 | return hookNames; |
| 104 | } |
| 105 | |
| 106 | it('should parse names for useState()', async () => { |
| 107 | const Component = |
| 108 | require('./__source__/__untransformed__/ComponentWithUseState').Component; |
| 109 | const hookNames = await getHookNamesForComponent(Component); |
| 110 | expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz', null]); |
| 111 | }); |
| 112 | |
| 113 | it('should parse names for useReducer()', async () => { |
| 114 | const Component = |
| 115 | require('./__source__/__untransformed__/ComponentWithUseReducer').Component; |
| 116 | const hookNames = await getHookNamesForComponent(Component); |
| 117 | expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz']); |
| 118 | }); |
| 119 | |
| 120 | it('should skip loading source files for unnamed hooks like useEffect', async () => { |
| 121 | const Component = |
| 122 | require('./__source__/__untransformed__/ComponentWithUseEffect').Component; |
| 123 | |
| 124 | // Since this component contains only unnamed hooks, the source code should not even be loaded. |
| 125 | fetchMock.mockIf(/.+$/, request => { |
| 126 | throw Error(`Unexpected file request for "${request.url}"`); |
| 127 | }); |
| 128 | |
| 129 | const hookNames = await getHookNamesForComponent(Component); |
| 130 | expectHookNamesToEqual(hookNames, []); // No hooks with names |
| 131 | }); |
| 132 | |
| 133 | it('should skip loading source files for unnamed hooks like useEffect (alternate)', async () => { |
| 134 | const Component = |
| 135 | require('./__source__/__untransformed__/ComponentWithExternalUseEffect').Component; |
| 136 | |
| 137 | fetchMock.mockIf(/.+$/, request => { |
| 138 | // Since the custom hook contains only unnamed hooks, the source code should not be loaded. |
| 139 | if (request.url.endsWith('useCustom.js')) { |
| 140 | throw Error(`Unexpected file request for "${request.url}"`); |
| 141 | } |
| 142 | return requireText(request.url, 'utf8'); |
| 143 | }); |
| 144 | |
| 145 | const hookNames = await getHookNamesForComponent(Component); |
| 146 | expectHookNamesToEqual(hookNames, ['count', null]); // No hooks with names |
| 147 | }); |
| 148 | |
| 149 | it('should parse names for custom hooks', async () => { |
| 150 | const Component = |
| 151 | require('./__source__/__untransformed__/ComponentWithNamedCustomHooks').Component; |
| 152 | const hookNames = await getHookNamesForComponent(Component); |
| 153 | expectHookNamesToEqual(hookNames, [ |
| 154 | 'foo', |
| 155 | null, // Custom hooks can have names, but not when using destructuring. |
| 156 | 'baz', |
| 157 | ]); |
| 158 | }); |
| 159 | |
| 160 | it('should parse names for code using hooks indirectly', async () => { |
| 161 | const Component = |
| 162 | require('./__source__/__untransformed__/ComponentUsingHooksIndirectly').Component; |
| 163 | const hookNames = await getHookNamesForComponent(Component); |
| 164 | expectHookNamesToEqual(hookNames, ['count', 'darkMode', 'isDarkMode']); |
| 165 | }); |
| 166 | |
| 167 | it('should parse names for code using nested hooks', async () => { |
| 168 | const Component = |
| 169 | require('./__source__/__untransformed__/ComponentWithNestedHooks').Component; |
| 170 | let InnerComponent; |
| 171 | const hookNames = await getHookNamesForComponent(Component, { |
| 172 | callback: innerComponent => { |
| 173 | InnerComponent = innerComponent; |
| 174 | }, |
| 175 | }); |
| 176 | const innerHookNames = await getHookNamesForComponent(InnerComponent); |
| 177 | expectHookNamesToEqual(hookNames, ['InnerComponent']); |
| 178 | expectHookNamesToEqual(innerHookNames, ['state']); |
| 179 | }); |
| 180 | |
| 181 | it('should return null for custom hooks without explicit names', async () => { |
| 182 | const Component = |
| 183 | require('./__source__/__untransformed__/ComponentWithUnnamedCustomHooks').Component; |
| 184 | const hookNames = await getHookNamesForComponent(Component); |
| 185 | expectHookNamesToEqual(hookNames, [ |
| 186 | null, // Custom hooks can have names, but this one does not even return a value. |
| 187 | null, // Custom hooks can have names, but not when using destructuring. |
| 188 | null, // Custom hooks can have names, but not when using destructuring. |
| 189 | ]); |
| 190 | }); |
| 191 | |
| 192 | // TODO Test that cache purge works |
| 193 | |
| 194 | // TODO Test that cached metadata is purged when Fast Refresh scheduled |
| 195 | |
| 196 | describe('inline, external and bundle source maps', () => { |
| 197 | it('should work for simple components', async () => { |
| 198 | async function testFor(path, name = 'Component') { |
| 199 | const Component = require(path)[name]; |
| 200 | const hookNames = await getHookNamesForComponent(Component); |
| 201 | expectHookNamesToEqual(hookNames, [ |
| 202 | 'count', // useState |
| 203 | ]); |
| 204 | } |
| 205 | |
| 206 | await testFor('./__source__/Example'); // original source (uncompiled) |
| 207 | await testFor('./__source__/__compiled__/inline/Example'); // inline source map |
| 208 | await testFor('./__source__/__compiled__/external/Example'); // external source map |
| 209 | await testFor('./__source__/__compiled__/inline/index-map/Example'); // inline index map source map |
| 210 | await testFor('./__source__/__compiled__/external/index-map/Example'); // external index map source map |
| 211 | await testFor('./__source__/__compiled__/bundle/index', 'Example'); // bundle source map |
| 212 | await testFor('./__source__/__compiled__/no-columns/Example'); // simulated Webpack 'cheap-module-source-map' |
| 213 | }); |
| 214 | |
| 215 | it('should work with more complex files and components', async () => { |
| 216 | async function testFor(path, name = undefined) { |
| 217 | const components = name != null ? require(path)[name] : require(path); |
| 218 | |
| 219 | let hookNames = await getHookNamesForComponent(components.List); |
| 220 | expectHookNamesToEqual(hookNames, [ |
| 221 | 'newItemText', // useState |
| 222 | 'items', // useState |
| 223 | 'uid', // useState |
| 224 | 'handleClick', // useCallback |
| 225 | 'handleKeyPress', // useCallback |
| 226 | 'handleChange', // useCallback |
| 227 | 'removeItem', // useCallback |
| 228 | 'toggleItem', // useCallback |
| 229 | ]); |
| 230 | |
| 231 | hookNames = await getHookNamesForComponent(components.ListItem, { |
| 232 | item: {}, |
| 233 | }); |
| 234 | expectHookNamesToEqual(hookNames, [ |
| 235 | 'handleDelete', // useCallback |
| 236 | 'handleToggle', // useCallback |
| 237 | ]); |
| 238 | } |
| 239 | |
| 240 | await testFor('./__source__/ToDoList'); // original source (uncompiled) |
| 241 | await testFor('./__source__/__compiled__/inline/ToDoList'); // inline source map |
| 242 | await testFor('./__source__/__compiled__/external/ToDoList'); // external source map |
| 243 | await testFor('./__source__/__compiled__/inline/index-map/ToDoList'); // inline index map source map |
| 244 | await testFor('./__source__/__compiled__/external/index-map/ToDoList'); // external index map source map |
| 245 | await testFor('./__source__/__compiled__/bundle', 'ToDoList'); // bundle source map |
| 246 | await testFor('./__source__/__compiled__/no-columns/ToDoList'); // simulated Webpack 'cheap-module-source-map' |
| 247 | }); |
| 248 | |
| 249 | it('should work for custom hook', async () => { |
| 250 | async function testFor(path, name = 'Component') { |
| 251 | const Component = require(path)[name]; |
| 252 | const hookNames = await getHookNamesForComponent(Component); |
| 253 | expectHookNamesToEqual(hookNames, [ |
| 254 | 'count', // useState() |
| 255 | 'isDarkMode', // useIsDarkMode() |
| 256 | 'isDarkMode', // useIsDarkMode -> useState() |
| 257 | null, // useFoo() |
| 258 | ]); |
| 259 | } |
| 260 | |
| 261 | await testFor('./__source__/ComponentWithCustomHook'); // original source (uncompiled) |
| 262 | await testFor('./__source__/__compiled__/inline/ComponentWithCustomHook'); // inline source map |
| 263 | await testFor( |
| 264 | './__source__/__compiled__/external/ComponentWithCustomHook', |
| 265 | ); // external source map |
| 266 | await testFor( |
| 267 | './__source__/__compiled__/inline/index-map/ComponentWithCustomHook', |
| 268 | ); // inline index map source map |
| 269 | await testFor( |
| 270 | './__source__/__compiled__/external/index-map/ComponentWithCustomHook', |
| 271 | ); // external index map source map |
| 272 | await testFor( |
| 273 | './__source__/__compiled__/bundle', |
| 274 | 'ComponentWithCustomHook', |
| 275 | ); // bundle source map |
| 276 | await testFor( |
| 277 | './__source__/__compiled__/no-columns/ComponentWithCustomHook', |
| 278 | ); // simulated Webpack 'cheap-module-source-map' |
| 279 | }); |
| 280 | |
| 281 | it('should work when code is using hooks indirectly', async () => { |
| 282 | async function testFor(path, name = 'Component') { |
| 283 | const Component = require(path)[name]; |
| 284 | const hookNames = await getHookNamesForComponent(Component); |
| 285 | expectHookNamesToEqual(hookNames, [ |
| 286 | 'count', // useState() |
| 287 | 'darkMode', // useDarkMode() |
| 288 | 'isDarkMode', // useState() |
| 289 | ]); |
| 290 | } |
| 291 | |
| 292 | await testFor( |
| 293 | './__source__/__compiled__/inline/ComponentUsingHooksIndirectly', |
| 294 | ); // inline source map |
| 295 | await testFor( |
| 296 | './__source__/__compiled__/external/ComponentUsingHooksIndirectly', |
| 297 | ); // external source map |
| 298 | await testFor( |
| 299 | './__source__/__compiled__/inline/index-map/ComponentUsingHooksIndirectly', |
| 300 | ); // inline index map source map |
| 301 | await testFor( |
| 302 | './__source__/__compiled__/external/index-map/ComponentUsingHooksIndirectly', |
| 303 | ); // external index map source map |
| 304 | await testFor( |
| 305 | './__source__/__compiled__/bundle', |
| 306 | 'ComponentUsingHooksIndirectly', |
| 307 | ); // bundle source map |
| 308 | await testFor( |
| 309 | './__source__/__compiled__/no-columns/ComponentUsingHooksIndirectly', |
| 310 | ); // simulated Webpack 'cheap-module-source-map' |
| 311 | }); |
| 312 | |
| 313 | it('should work when code is using nested hooks', async () => { |
| 314 | async function testFor(path, name = 'Component') { |
| 315 | const Component = require(path)[name]; |
| 316 | let InnerComponent; |
| 317 | const hookNames = await getHookNamesForComponent(Component, { |
| 318 | callback: innerComponent => { |
| 319 | InnerComponent = innerComponent; |
| 320 | }, |
| 321 | }); |
| 322 | const innerHookNames = await getHookNamesForComponent(InnerComponent); |
| 323 | expectHookNamesToEqual(hookNames, [ |
| 324 | 'InnerComponent', // useMemo() |
| 325 | ]); |
| 326 | expectHookNamesToEqual(innerHookNames, [ |
| 327 | 'state', // useState() |
| 328 | ]); |
| 329 | } |
| 330 | |
| 331 | await testFor( |
| 332 | './__source__/__compiled__/inline/ComponentWithNestedHooks', |
| 333 | ); // inline source map |
| 334 | await testFor( |
| 335 | './__source__/__compiled__/external/ComponentWithNestedHooks', |
| 336 | ); // external source map |
| 337 | await testFor( |
| 338 | './__source__/__compiled__/inline/index-map/ComponentWithNestedHooks', |
| 339 | ); // inline index map source map |
| 340 | await testFor( |
| 341 | './__source__/__compiled__/external/index-map/ComponentWithNestedHooks', |
| 342 | ); // external index map source map |
| 343 | await testFor( |
| 344 | './__source__/__compiled__/bundle', |
| 345 | 'ComponentWithNestedHooks', |
| 346 | ); // bundle source map |
| 347 | await testFor( |
| 348 | './__source__/__compiled__/no-columns/ComponentWithNestedHooks', |
| 349 | ); // simulated Webpack 'cheap-module-source-map' |
| 350 | }); |
| 351 | |
| 352 | it('should work for external hooks', async () => { |
| 353 | async function testFor(path, name = 'Component') { |
| 354 | const Component = require(path)[name]; |
| 355 | const hookNames = await getHookNamesForComponent(Component); |
| 356 | expectHookNamesToEqual(hookNames, [ |
| 357 | 'theme', // useTheme() |
| 358 | 'theme', // useContext() |
| 359 | ]); |
| 360 | } |
| 361 | |
| 362 | // We can't test the uncompiled source here, because it either needs to get transformed, |
| 363 | // which would break the source mapping, or the import statements will fail. |
| 364 | |
| 365 | await testFor( |
| 366 | './__source__/__compiled__/inline/ComponentWithExternalCustomHooks', |
| 367 | ); // inline source map |
| 368 | await testFor( |
| 369 | './__source__/__compiled__/external/ComponentWithExternalCustomHooks', |
| 370 | ); // external source map |
| 371 | await testFor( |
| 372 | './__source__/__compiled__/inline/index-map/ComponentWithExternalCustomHooks', |
| 373 | ); // inline index map source map |
| 374 | await testFor( |
| 375 | './__source__/__compiled__/external/index-map/ComponentWithExternalCustomHooks', |
| 376 | ); // external index map source map |
| 377 | await testFor( |
| 378 | './__source__/__compiled__/bundle', |
| 379 | 'ComponentWithExternalCustomHooks', |
| 380 | ); // bundle source map |
| 381 | await testFor( |
| 382 | './__source__/__compiled__/no-columns/ComponentWithExternalCustomHooks', |
| 383 | ); // simulated Webpack 'cheap-module-source-map' |
| 384 | }); |
| 385 | |
| 386 | it('should work when multiple hooks are on a line', async () => { |
| 387 | async function testFor(path, name = 'Component') { |
| 388 | const Component = require(path)[name]; |
| 389 | const hookNames = await getHookNamesForComponent(Component); |
| 390 | expectHookNamesToEqual(hookNames, [ |
| 391 | 'a', // useContext() |
| 392 | 'b', // useContext() |
| 393 | 'c', // useContext() |
| 394 | 'd', // useContext() |
| 395 | ]); |
| 396 | } |
| 397 | |
| 398 | await testFor( |
| 399 | './__source__/__compiled__/inline/ComponentWithMultipleHooksPerLine', |
| 400 | ); // inline source map |
| 401 | await testFor( |
| 402 | './__source__/__compiled__/external/ComponentWithMultipleHooksPerLine', |
| 403 | ); // external source map |
| 404 | await testFor( |
| 405 | './__source__/__compiled__/inline/index-map/ComponentWithMultipleHooksPerLine', |
| 406 | ); // inline index map source map |
| 407 | await testFor( |
| 408 | './__source__/__compiled__/external/index-map/ComponentWithMultipleHooksPerLine', |
| 409 | ); // external index map source map |
| 410 | await testFor( |
| 411 | './__source__/__compiled__/bundle', |
| 412 | 'ComponentWithMultipleHooksPerLine', |
| 413 | ); // bundle source map |
| 414 | |
| 415 | async function noColumntest(path, name = 'Component') { |
| 416 | const Component = require(path)[name]; |
| 417 | const hookNames = await getHookNamesForComponent(Component); |
| 418 | expectHookNamesToEqual(hookNames, [ |
| 419 | 'a', // useContext() |
| 420 | 'b', // useContext() |
| 421 | null, // useContext() |
| 422 | null, // useContext() |
| 423 | ]); |
| 424 | } |
| 425 | |
| 426 | // Note that this test is expected to only match the first two hooks |
| 427 | // because the 3rd and 4th hook are on the same line, |
| 428 | // and this type of source map doesn't have column numbers. |
| 429 | await noColumntest( |
| 430 | './__source__/__compiled__/no-columns/ComponentWithMultipleHooksPerLine', |
| 431 | ); // simulated Webpack 'cheap-module-source-map' |
| 432 | }); |
| 433 | |
| 434 | // TODO Inline require (e.g. require("react").useState()) isn't supported yet. |
| 435 | // Maybe this isn't an important use case to support, |
| 436 | // since inline requires are most likely to exist in compiled source (if at all). |
| 437 | // eslint-disable-next-line jest/no-disabled-tests |
| 438 | it.skip('should work for inline requires', async () => { |
| 439 | async function testFor(path, name = 'Component') { |
| 440 | const Component = require(path)[name]; |
| 441 | const hookNames = await getHookNamesForComponent(Component); |
| 442 | expectHookNamesToEqual(hookNames, [ |
| 443 | 'count', // useState() |
| 444 | ]); |
| 445 | } |
| 446 | |
| 447 | await testFor('./__source__/InlineRequire'); // original source (uncompiled) |
| 448 | await testFor('./__source__/__compiled__/inline/InlineRequire'); // inline source map |
| 449 | await testFor('./__source__/__compiled__/external/InlineRequire'); // external source map |
| 450 | await testFor('./__source__/__compiled__/inline/index-map/InlineRequire'); // inline index map source map |
| 451 | await testFor( |
| 452 | './__source__/__compiled__/external/index-map/InlineRequire', |
| 453 | ); // external index map source map |
| 454 | await testFor('./__source__/__compiled__/bundle', 'InlineRequire'); // bundle source map |
| 455 | await testFor('./__source__/__compiled__/no-columns/InlineRequire'); // simulated Webpack 'cheap-module-source-map' |
| 456 | }); |
| 457 | |
| 458 | it('should support sources that contain the string "sourceMappingURL="', async () => { |
| 459 | async function testFor(path, name = 'Component') { |
| 460 | const Component = require(path)[name]; |
| 461 | const hookNames = await getHookNamesForComponent(Component); |
| 462 | expectHookNamesToEqual(hookNames, [ |
| 463 | 'count', // useState() |
| 464 | ]); |
| 465 | } |
| 466 | |
| 467 | // We expect the inline sourceMappingURL to be invalid in this case; mute the warning. |
| 468 | console.warn = () => {}; |
| 469 | |
| 470 | await testFor('./__source__/ContainingStringSourceMappingURL'); // original source (uncompiled) |
| 471 | await testFor( |
| 472 | './__source__/__compiled__/inline/ContainingStringSourceMappingURL', |
| 473 | ); // inline source map |
| 474 | await testFor( |
| 475 | './__source__/__compiled__/external/ContainingStringSourceMappingURL', |
| 476 | ); // external source map |
| 477 | await testFor( |
| 478 | './__source__/__compiled__/inline/index-map/ContainingStringSourceMappingURL', |
| 479 | ); // inline index map source map |
| 480 | await testFor( |
| 481 | './__source__/__compiled__/external/index-map/ContainingStringSourceMappingURL', |
| 482 | ); // external index map source map |
| 483 | await testFor( |
| 484 | './__source__/__compiled__/bundle', |
| 485 | 'ContainingStringSourceMappingURL', |
| 486 | ); // bundle source map |
| 487 | await testFor( |
| 488 | './__source__/__compiled__/no-columns/ContainingStringSourceMappingURL', |
| 489 | ); // simulated Webpack 'cheap-module-source-map' |
| 490 | }); |
| 491 | }); |
| 492 | |
| 493 | describe('extended source maps', () => { |
| 494 | beforeEach(() => { |
| 495 | const babelParser = require('@babel/parser'); |
| 496 | const generateHookMapModule = require('../generateHookMap'); |
| 497 | jest.spyOn(babelParser, 'parse'); |
| 498 | jest.spyOn(generateHookMapModule, 'decodeHookMap'); |
| 499 | }); |
| 500 | |
| 501 | it('should work for simple components', async () => { |
| 502 | async function testFor(path, name = 'Component') { |
| 503 | const Component = require(path)[name]; |
| 504 | const hookNames = await getHookNamesForComponent(Component); |
| 505 | expectHookNamesToEqual(hookNames, [ |
| 506 | 'count', // useState |
| 507 | ]); |
| 508 | expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0); |
| 509 | expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled(); |
| 510 | } |
| 511 | |
| 512 | await testFor( |
| 513 | './__source__/__compiled__/inline/fb-sources-extended/Example', |
| 514 | ); // x_facebook_sources extended inline source map |
| 515 | await testFor( |
| 516 | './__source__/__compiled__/external/fb-sources-extended/Example', |
| 517 | ); // x_facebook_sources extended external source map |
| 518 | await testFor( |
| 519 | './__source__/__compiled__/inline/react-sources-extended/Example', |
| 520 | ); // x_react_sources extended inline source map |
| 521 | await testFor( |
| 522 | './__source__/__compiled__/external/react-sources-extended/Example', |
| 523 | ); // x_react_sources extended external source map |
| 524 | |
| 525 | // Using index map format for source maps |
| 526 | await testFor( |
| 527 | './__source__/__compiled__/inline/fb-sources-extended/index-map/Example', |
| 528 | ); // x_facebook_sources extended inline index map source map |
| 529 | await testFor( |
| 530 | './__source__/__compiled__/external/fb-sources-extended/index-map/Example', |
| 531 | ); // x_facebook_sources extended external index map source map |
| 532 | await testFor( |
| 533 | './__source__/__compiled__/inline/react-sources-extended/index-map/Example', |
| 534 | ); // x_react_sources extended inline index map source map |
| 535 | await testFor( |
| 536 | './__source__/__compiled__/external/react-sources-extended/index-map/Example', |
| 537 | ); // x_react_sources extended external index map source map |
| 538 | |
| 539 | // TODO test no-columns and bundle cases with extended source maps |
| 540 | }); |
| 541 | |
| 542 | it('should work with more complex files and components', async () => { |
| 543 | async function testFor(path, name = undefined) { |
| 544 | const components = name != null ? require(path)[name] : require(path); |
| 545 | |
| 546 | let hookNames = await getHookNamesForComponent(components.List); |
| 547 | expectHookNamesToEqual(hookNames, [ |
| 548 | 'newItemText', // useState |
| 549 | 'items', // useState |
| 550 | 'uid', // useState |
| 551 | 'handleClick', // useCallback |
| 552 | 'handleKeyPress', // useCallback |
| 553 | 'handleChange', // useCallback |
| 554 | 'removeItem', // useCallback |
| 555 | 'toggleItem', // useCallback |
| 556 | ]); |
| 557 | |
| 558 | hookNames = await getHookNamesForComponent(components.ListItem, { |
| 559 | item: {}, |
| 560 | }); |
| 561 | expectHookNamesToEqual(hookNames, [ |
| 562 | 'handleDelete', // useCallback |
| 563 | 'handleToggle', // useCallback |
| 564 | ]); |
| 565 | |
| 566 | expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0); |
| 567 | expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled(); |
| 568 | } |
| 569 | |
| 570 | await testFor( |
| 571 | './__source__/__compiled__/inline/fb-sources-extended/ToDoList', |
| 572 | ); // x_facebook_sources extended inline source map |
| 573 | await testFor( |
| 574 | './__source__/__compiled__/external/fb-sources-extended/ToDoList', |
| 575 | ); // x_facebook_sources extended external source map |
| 576 | await testFor( |
| 577 | './__source__/__compiled__/inline/react-sources-extended/ToDoList', |
| 578 | ); // x_react_sources extended inline source map |
| 579 | await testFor( |
| 580 | './__source__/__compiled__/external/react-sources-extended/ToDoList', |
| 581 | ); // x_react_sources extended external source map |
| 582 | |
| 583 | // Using index map format for source maps |
| 584 | await testFor( |
| 585 | './__source__/__compiled__/inline/fb-sources-extended/index-map/ToDoList', |
| 586 | ); // x_facebook_sources extended inline index map source map |
| 587 | await testFor( |
| 588 | './__source__/__compiled__/external/fb-sources-extended/index-map/ToDoList', |
| 589 | ); // x_facebook_sources extended external index map source map |
| 590 | await testFor( |
| 591 | './__source__/__compiled__/inline/react-sources-extended/index-map/ToDoList', |
| 592 | ); // x_react_sources extended inline index map source map |
| 593 | await testFor( |
| 594 | './__source__/__compiled__/external/react-sources-extended/index-map/ToDoList', |
| 595 | ); // x_react_sources extended external index map source map |
| 596 | |
| 597 | // TODO test no-columns and bundle cases with extended source maps |
| 598 | }); |
| 599 | |
| 600 | it('should work for custom hook', async () => { |
| 601 | async function testFor(path, name = 'Component') { |
| 602 | const Component = require(path)[name]; |
| 603 | const hookNames = await getHookNamesForComponent(Component); |
| 604 | expectHookNamesToEqual(hookNames, [ |
| 605 | 'count', // useState() |
| 606 | 'isDarkMode', // useIsDarkMode() |
| 607 | 'isDarkMode', // useIsDarkMode -> useState() |
| 608 | null, // isFoo() |
| 609 | ]); |
| 610 | expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0); |
| 611 | expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled(); |
| 612 | } |
| 613 | |
| 614 | await testFor( |
| 615 | './__source__/__compiled__/inline/fb-sources-extended/ComponentWithCustomHook', |
| 616 | ); // x_facebook_sources extended inline source map |
| 617 | await testFor( |
| 618 | './__source__/__compiled__/external/fb-sources-extended/ComponentWithCustomHook', |
| 619 | ); // x_facebook_sources extended external source map |
| 620 | await testFor( |
| 621 | './__source__/__compiled__/inline/react-sources-extended/ComponentWithCustomHook', |
| 622 | ); // x_react_sources extended inline source map |
| 623 | await testFor( |
| 624 | './__source__/__compiled__/external/react-sources-extended/ComponentWithCustomHook', |
| 625 | ); // x_react_sources extended external source map |
| 626 | |
| 627 | // Using index map format for source maps |
| 628 | await testFor( |
| 629 | './__source__/__compiled__/inline/fb-sources-extended/index-map/ComponentWithCustomHook', |
| 630 | ); // x_facebook_sources extended inline index map source map |
| 631 | await testFor( |
| 632 | './__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithCustomHook', |
| 633 | ); // x_facebook_sources extended external index map source map |
| 634 | await testFor( |
| 635 | './__source__/__compiled__/inline/react-sources-extended/index-map/ComponentWithCustomHook', |
| 636 | ); // x_react_sources extended inline index map source map |
| 637 | await testFor( |
| 638 | './__source__/__compiled__/external/react-sources-extended/index-map/ComponentWithCustomHook', |
| 639 | ); // x_react_sources extended external index map source map |
| 640 | |
| 641 | // TODO test no-columns and bundle cases with extended source maps |
| 642 | }); |
| 643 | |
| 644 | it('should work when code is using hooks indirectly', async () => { |
| 645 | async function testFor(path, name = 'Component') { |
| 646 | const Component = require(path)[name]; |
| 647 | const hookNames = await getHookNamesForComponent(Component); |
| 648 | expectHookNamesToEqual(hookNames, [ |
| 649 | 'count', // useState() |
| 650 | 'darkMode', // useDarkMode() |
| 651 | 'isDarkMode', // useState() |
| 652 | ]); |
| 653 | expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0); |
| 654 | expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled(); |
| 655 | } |
| 656 | |
| 657 | await testFor( |
| 658 | './__source__/__compiled__/inline/fb-sources-extended/ComponentUsingHooksIndirectly', |
| 659 | ); // x_facebook_sources extended inline source map |
| 660 | await testFor( |
| 661 | './__source__/__compiled__/external/fb-sources-extended/ComponentUsingHooksIndirectly', |
| 662 | ); // x_facebook_sources extended external source map |
| 663 | await testFor( |
| 664 | './__source__/__compiled__/inline/react-sources-extended/ComponentUsingHooksIndirectly', |
| 665 | ); // x_react_sources extended inline source map |
| 666 | await testFor( |
| 667 | './__source__/__compiled__/external/react-sources-extended/ComponentUsingHooksIndirectly', |
| 668 | ); // x_react_sources extended external source map |
| 669 | |
| 670 | // Using index map format for source maps |
| 671 | await testFor( |
| 672 | './__source__/__compiled__/inline/fb-sources-extended/index-map/ComponentUsingHooksIndirectly', |
| 673 | ); // x_facebook_sources extended inline index map source map |
| 674 | await testFor( |
| 675 | './__source__/__compiled__/external/fb-sources-extended/index-map/ComponentUsingHooksIndirectly', |
| 676 | ); // x_facebook_sources extended external index map source map |
| 677 | await testFor( |
| 678 | './__source__/__compiled__/inline/react-sources-extended/index-map/ComponentUsingHooksIndirectly', |
| 679 | ); // x_react_sources extended inline index map source map |
| 680 | await testFor( |
| 681 | './__source__/__compiled__/external/react-sources-extended/index-map/ComponentUsingHooksIndirectly', |
| 682 | ); // x_react_sources extended external index map source map |
| 683 | |
| 684 | // TODO test no-columns and bundle cases with extended source maps |
| 685 | }); |
| 686 | |
| 687 | it('should work when code is using nested hooks', async () => { |
| 688 | async function testFor(path, name = 'Component') { |
| 689 | const Component = require(path)[name]; |
| 690 | let InnerComponent; |
| 691 | const hookNames = await getHookNamesForComponent(Component, { |
| 692 | callback: innerComponent => { |
| 693 | InnerComponent = innerComponent; |
| 694 | }, |
| 695 | }); |
| 696 | const innerHookNames = await getHookNamesForComponent(InnerComponent); |
| 697 | expectHookNamesToEqual(hookNames, [ |
| 698 | 'InnerComponent', // useMemo() |
| 699 | ]); |
| 700 | expectHookNamesToEqual(innerHookNames, [ |
| 701 | 'state', // useState() |
| 702 | ]); |
| 703 | expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0); |
| 704 | expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled(); |
| 705 | } |
| 706 | |
| 707 | await testFor( |
| 708 | './__source__/__compiled__/inline/fb-sources-extended/ComponentWithNestedHooks', |
| 709 | ); // x_facebook_sources extended inline source map |
| 710 | await testFor( |
| 711 | './__source__/__compiled__/external/fb-sources-extended/ComponentWithNestedHooks', |
| 712 | ); // x_facebook_sources extended external source map |
| 713 | await testFor( |
| 714 | './__source__/__compiled__/inline/react-sources-extended/ComponentWithNestedHooks', |
| 715 | ); // x_react_sources extended inline source map |
| 716 | await testFor( |
| 717 | './__source__/__compiled__/external/react-sources-extended/ComponentWithNestedHooks', |
| 718 | ); // x_react_sources extended external source map |
| 719 | |
| 720 | // Using index map format for source maps |
| 721 | await testFor( |
| 722 | './__source__/__compiled__/inline/fb-sources-extended/index-map/ComponentWithNestedHooks', |
| 723 | ); // x_facebook_sources extended inline index map source map |
| 724 | await testFor( |
| 725 | './__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithNestedHooks', |
| 726 | ); // x_facebook_sources extended external index map source map |
| 727 | await testFor( |
| 728 | './__source__/__compiled__/inline/react-sources-extended/index-map/ComponentWithNestedHooks', |
| 729 | ); // x_react_sources extended inline index map source map |
| 730 | await testFor( |
| 731 | './__source__/__compiled__/external/react-sources-extended/index-map/ComponentWithNestedHooks', |
| 732 | ); // x_react_sources extended external index map source map |
| 733 | |
| 734 | // TODO test no-columns and bundle cases with extended source maps |
| 735 | }); |
| 736 | |
| 737 | it('should work for external hooks', async () => { |
| 738 | async function testFor(path, name = 'Component') { |
| 739 | const Component = require(path)[name]; |
| 740 | const hookNames = await getHookNamesForComponent(Component); |
| 741 | expectHookNamesToEqual(hookNames, [ |
| 742 | 'theme', // useTheme() |
| 743 | 'theme', // useContext() |
| 744 | ]); |
| 745 | expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0); |
| 746 | expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled(); |
| 747 | } |
| 748 | |
| 749 | // We can't test the uncompiled source here, because it either needs to get transformed, |
| 750 | // which would break the source mapping, or the import statements will fail. |
| 751 | |
| 752 | await testFor( |
| 753 | './__source__/__compiled__/inline/fb-sources-extended/ComponentWithExternalCustomHooks', |
| 754 | ); // x_facebook_sources extended inline source map |
| 755 | await testFor( |
| 756 | './__source__/__compiled__/external/fb-sources-extended/ComponentWithExternalCustomHooks', |
| 757 | ); // x_facebook_sources extended external source map |
| 758 | await testFor( |
| 759 | './__source__/__compiled__/inline/react-sources-extended/ComponentWithExternalCustomHooks', |
| 760 | ); // x_react_sources extended inline source map |
| 761 | await testFor( |
| 762 | './__source__/__compiled__/external/react-sources-extended/ComponentWithExternalCustomHooks', |
| 763 | ); // x_react_sources extended external source map |
| 764 | |
| 765 | // Using index map format for source maps |
| 766 | await testFor( |
| 767 | './__source__/__compiled__/inline/fb-sources-extended/index-map/ComponentWithExternalCustomHooks', |
| 768 | ); // x_facebook_sources extended inline index map source map |
| 769 | await testFor( |
| 770 | './__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithExternalCustomHooks', |
| 771 | ); // x_facebook_sources extended external index map source map |
| 772 | await testFor( |
| 773 | './__source__/__compiled__/inline/react-sources-extended/index-map/ComponentWithExternalCustomHooks', |
| 774 | ); // x_react_sources extended inline index map source map |
| 775 | await testFor( |
| 776 | './__source__/__compiled__/external/react-sources-extended/index-map/ComponentWithExternalCustomHooks', |
| 777 | ); // x_react_sources extended external index map source map |
| 778 | |
| 779 | // TODO test no-columns and bundle cases with extended source maps |
| 780 | }); |
| 781 | |
| 782 | it('should work when multiple hooks are on a line', async () => { |
| 783 | async function testFor(path, name = 'Component') { |
| 784 | const Component = require(path)[name]; |
| 785 | const hookNames = await getHookNamesForComponent(Component); |
| 786 | expectHookNamesToEqual(hookNames, [ |
| 787 | 'a', // useContext() |
| 788 | 'b', // useContext() |
| 789 | 'c', // useContext() |
| 790 | 'd', // useContext() |
| 791 | ]); |
| 792 | expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0); |
| 793 | expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled(); |
| 794 | } |
| 795 | |
| 796 | await testFor( |
| 797 | './__source__/__compiled__/inline/fb-sources-extended/ComponentWithMultipleHooksPerLine', |
| 798 | ); // x_facebook_sources extended inline source map |
| 799 | await testFor( |
| 800 | './__source__/__compiled__/external/fb-sources-extended/ComponentWithMultipleHooksPerLine', |
| 801 | ); // x_facebook_sources extended external source map |
| 802 | await testFor( |
| 803 | './__source__/__compiled__/inline/react-sources-extended/ComponentWithMultipleHooksPerLine', |
| 804 | ); // x_react_sources extended inline source map |
| 805 | await testFor( |
| 806 | './__source__/__compiled__/external/react-sources-extended/ComponentWithMultipleHooksPerLine', |
| 807 | ); // x_react_sources extended external source map |
| 808 | |
| 809 | // Using index map format for source maps |
| 810 | await testFor( |
| 811 | './__source__/__compiled__/inline/fb-sources-extended/index-map/ComponentWithMultipleHooksPerLine', |
| 812 | ); // x_facebook_sources extended inline index map source map |
| 813 | await testFor( |
| 814 | './__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithMultipleHooksPerLine', |
| 815 | ); // x_facebook_sources extended external index map source map |
| 816 | await testFor( |
| 817 | './__source__/__compiled__/inline/react-sources-extended/index-map/ComponentWithMultipleHooksPerLine', |
| 818 | ); // x_react_sources extended inline index map source map |
| 819 | await testFor( |
| 820 | './__source__/__compiled__/external/react-sources-extended/index-map/ComponentWithMultipleHooksPerLine', |
| 821 | ); // x_react_sources extended external index map source map |
| 822 | |
| 823 | // TODO test no-columns and bundle cases with extended source maps |
| 824 | }); |
| 825 | |
| 826 | // TODO Inline require (e.g. require("react").useState()) isn't supported yet. |
| 827 | // Maybe this isn't an important use case to support, |
| 828 | // since inline requires are most likely to exist in compiled source (if at all). |
| 829 | // eslint-disable-next-line jest/no-disabled-tests |
| 830 | it.skip('should work for inline requires', async () => { |
| 831 | async function testFor(path, name = 'Component') { |
| 832 | const Component = require(path)[name]; |
| 833 | const hookNames = await getHookNamesForComponent(Component); |
| 834 | expectHookNamesToEqual(hookNames, [ |
| 835 | 'count', // useState() |
| 836 | ]); |
| 837 | expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0); |
| 838 | expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled(); |
| 839 | } |
| 840 | |
| 841 | await testFor( |
| 842 | './__source__/__compiled__/inline/fb-sources-extended/InlineRequire', |
| 843 | ); // x_facebook_sources extended inline source map |
| 844 | await testFor( |
| 845 | './__source__/__compiled__/external/fb-sources-extended/InlineRequire', |
| 846 | ); // x_facebook_sources extended external source map |
| 847 | await testFor( |
| 848 | './__source__/__compiled__/inline/react-sources-extended/InlineRequire', |
| 849 | ); // x_react_sources extended inline source map |
| 850 | await testFor( |
| 851 | './__source__/__compiled__/external/react-sources-extended/InlineRequire', |
| 852 | ); // x_react_sources extended external source map |
| 853 | |
| 854 | // Using index map format for source maps |
| 855 | await testFor( |
| 856 | './__source__/__compiled__/inline/fb-sources-extended/index-map/InlineRequire', |
| 857 | ); // x_facebook_sources extended inline index map source map |
| 858 | await testFor( |
| 859 | './__source__/__compiled__/external/fb-sources-extended/index-map/InlineRequire', |
| 860 | ); // x_facebook_sources extended external index map source map |
| 861 | await testFor( |
| 862 | './__source__/__compiled__/inline/react-sources-extended/index-map/InlineRequire', |
| 863 | ); // x_react_sources extended inline index map source map |
| 864 | await testFor( |
| 865 | './__source__/__compiled__/external/react-sources-extended/index-map/InlineRequire', |
| 866 | ); // x_react_sources extended external index map source map |
| 867 | |
| 868 | // TODO test no-columns and bundle cases with extended source maps |
| 869 | }); |
| 870 | |
| 871 | it('should support sources that contain the string "sourceMappingURL="', async () => { |
| 872 | async function testFor(path, name = 'Component') { |
| 873 | const Component = require(path)[name]; |
| 874 | const hookNames = await getHookNamesForComponent(Component); |
| 875 | expectHookNamesToEqual(hookNames, [ |
| 876 | 'count', // useState() |
| 877 | ]); |
| 878 | expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0); |
| 879 | expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled(); |
| 880 | } |
| 881 | |
| 882 | // We expect the inline sourceMappingURL to be invalid in this case; mute the warning. |
| 883 | console.warn = () => {}; |
| 884 | |
| 885 | await testFor( |
| 886 | './__source__/__compiled__/inline/fb-sources-extended/ContainingStringSourceMappingURL', |
| 887 | ); // x_facebook_sources extended inline source map |
| 888 | await testFor( |
| 889 | './__source__/__compiled__/external/fb-sources-extended/ContainingStringSourceMappingURL', |
| 890 | ); // x_facebook_sources extended external source map |
| 891 | await testFor( |
| 892 | './__source__/__compiled__/inline/react-sources-extended/ContainingStringSourceMappingURL', |
| 893 | ); // x_react_sources extended inline source map |
| 894 | await testFor( |
| 895 | './__source__/__compiled__/external/react-sources-extended/ContainingStringSourceMappingURL', |
| 896 | ); // x_react_sources extended external source map |
| 897 | |
| 898 | // Using index map format for source maps |
| 899 | await testFor( |
| 900 | './__source__/__compiled__/inline/fb-sources-extended/index-map/ContainingStringSourceMappingURL', |
| 901 | ); // x_facebook_sources extended inline index map source map |
| 902 | await testFor( |
| 903 | './__source__/__compiled__/external/fb-sources-extended/index-map/ContainingStringSourceMappingURL', |
| 904 | ); // x_facebook_sources extended external index map source map |
| 905 | await testFor( |
| 906 | './__source__/__compiled__/inline/react-sources-extended/index-map/ContainingStringSourceMappingURL', |
| 907 | ); // x_react_sources extended inline index map source map |
| 908 | await testFor( |
| 909 | './__source__/__compiled__/external/react-sources-extended/index-map/ContainingStringSourceMappingURL', |
| 910 | ); // x_react_sources extended external index map source map |
| 911 | |
| 912 | // TODO test no-columns and bundle cases with extended source maps |
| 913 | }); |
| 914 | }); |
| 915 | }); |
| 916 | |
| 917 | describe('parseHookNames worker', () => { |
| 918 | let inspectHooks; |
| 919 | let parseHookNames; |
| 920 | let workerizedParseSourceAndMetadataMock; |
| 921 | |
| 922 | beforeEach(() => { |
| 923 | window.Worker = undefined; |
| 924 | |
| 925 | workerizedParseSourceAndMetadataMock = jest.fn(); |
| 926 | |
| 927 | initFetchMock(); |
| 928 | |
| 929 | jest.mock('../parseHookNames/parseSourceAndMetadata.worker.js', () => { |
| 930 | return { |
| 931 | __esModule: true, |
| 932 | default: () => ({ |
| 933 | parseSourceAndMetadata: workerizedParseSourceAndMetadataMock, |
| 934 | }), |
| 935 | }; |
| 936 | }); |
| 937 | |
| 938 | inspectHooks = |
| 939 | require('react-debug-tools/src/ReactDebugHooks').inspectHooks; |
| 940 | parseHookNames = require('../parseHookNames').parseHookNames; |
| 941 | }); |
| 942 | |
| 943 | async function getHookNamesForComponent(Component, props = {}) { |
| 944 | const hooksTree = inspectHooks(Component, props, undefined); |
| 945 | const hookNames = await parseHookNames(hooksTree); |
| 946 | return hookNames; |
| 947 | } |
| 948 | |
| 949 | it('should use worker', async () => { |
| 950 | const Component = |
| 951 | require('./__source__/__untransformed__/ComponentWithUseState').Component; |
| 952 | |
| 953 | window.Worker = true; |
| 954 | |
| 955 | // Reset module so mocked worker instance can be updated. |
| 956 | jest.resetModules(); |
| 957 | parseHookNames = require('../parseHookNames').parseHookNames; |
| 958 | |
| 959 | await getHookNamesForComponent(Component); |
| 960 | expect(workerizedParseSourceAndMetadataMock).toHaveBeenCalledTimes(1); |
| 961 | }); |
| 962 | }); |