main
js 962 lines 41 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 * @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&param=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 });