@samitouri / QOS-React-2 / commits / bec7599067

Migrate conditional tests to gate pragma (#18585)

* Migrate conditional tests to gate pragma I searched through the codebase for this pattern: ```js describe('test suite', () => { if (!__EXPERIMENTAL__) { // or some other condition test("empty test so Jest doesn't complain", () => {}); return; } // Unless we're in experimental mode, none of the tests in this block // will run. }) ``` and converted them to the `@gate` pragma instead. The reason this pattern isn't preferred is because you end up disabling more tests than you need to. * Add flag for www release channels Using a heuristic where I check a flag that is known to only be enabled in www. I left a TODO to instead set the release channel explicitly in each test config.

Andrew Clark committed Apr 13, 2020 at 14:45 UTC bec7599067dddb41aa7c475e5bfa0a36ddf404a8
30 files changed +3500 -2928
packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js
+2 -5
@@ -25,11 +25,7 @@ describe('ReactHooksInspection', () => {
25 ReactDebugTools = require('react-debug-tools');
26 });
27
28 - if (!__EXPERIMENTAL__) {
29 - it("empty test so Jest doesn't complain", () => {});
30 - return;
31 - }
32 -
28 + // @gate experimental
29 it('should inspect a simple useResponder hook', () => {
30 const TestResponder = React.DEPRECATED_createResponder('TestResponder', {});
31
@@ -51,6 +47,7 @@ describe('ReactHooksInspection', () => {
47 ]);
48 });
49
50 + // @gate experimental
51 it('should inspect a simple ReactDOM.useEvent hook', () => {
52 let clickHandle;
53 let ref;
packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js
+1
@@ -27,6 +27,7 @@ describe('StoreStressConcurrent', () => {
27 print = require('./storeSerializer').print;
28 });
29
30 + // TODO: Remove this in favor of @gate pragma
31 if (!__EXPERIMENTAL__) {
32 it("empty test so Jest doesn't complain", () => {});
33 return;
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+128 -106
@@ -87,11 +87,7 @@ describe('ReactDOMServerPartialHydration', () => {
87 SuspenseList = React.SuspenseList;
88 });
89
90 - if (!__EXPERIMENTAL__) {
91 - it("empty test so Jest doesn't complain", () => {});
92 - return;
93 - }
94 -
90 + // @gate experimental
91 it('hydrates a parent even if a child Suspense boundary is blocked', async () => {
92 let suspend = false;
93 let resolve;
@@ -150,6 +146,7 @@ describe('ReactDOMServerPartialHydration', () => {
146 expect(ref.current).toBe(span);
147 });
148
149 + // @gate experimental
150 it('calls the hydration callbacks after hydration or deletion', async () => {
151 let suspend = false;
152 let resolve;
@@ -240,6 +237,7 @@ describe('ReactDOMServerPartialHydration', () => {
237 expect(deleted.length).toBe(1);
238 });
239
240 + // @gate experimental
241 it('calls the onDeleted hydration callback if the parent gets deleted', async () => {
242 let suspend = false;
243 const promise = new Promise(() => {});
@@ -297,6 +295,7 @@ describe('ReactDOMServerPartialHydration', () => {
295 expect(deleted.length).toBe(1);
296 });
297
298 + // @gate experimental || www
299 it('warns and replaces the boundary content in legacy mode', async () => {
300 let suspend = false;
301 let resolve;
@@ -368,6 +367,7 @@ describe('ReactDOMServerPartialHydration', () => {
367 expect(container.textContent).toBe('Hello');
368 });
369
370 + // @gate experimental
371 it('can insert siblings before the dehydrated boundary', () => {
372 let suspend = false;
373 const promise = new Promise(() => {});
@@ -426,6 +426,7 @@ describe('ReactDOMServerPartialHydration', () => {
426 expect(container.firstChild.firstChild.textContent).toBe('First');
427 });
428
429 + // @gate experimental
430 it('can delete the dehydrated boundary before it is hydrated', () => {
431 let suspend = false;
432 const promise = new Promise(() => {});
@@ -482,6 +483,7 @@ describe('ReactDOMServerPartialHydration', () => {
483 expect(container.firstChild.children[1].textContent).toBe('After');
484 });
485
486 + // @gate experimental
487 it('blocks updates to hydrate the content first if props have changed', async () => {
488 let suspend = false;
489 let resolve;
@@ -553,6 +555,7 @@ describe('ReactDOMServerPartialHydration', () => {
555 expect(span.className).toBe('hi');
556 });
557
558 + // @gate experimental
559 it('shows the fallback if props have changed before hydration completes and is still suspended', async () => {
560 let suspend = false;
561 let resolve;
@@ -622,6 +625,7 @@ describe('ReactDOMServerPartialHydration', () => {
625 expect(container.textContent).toBe('Hi');
626 });
627
628 + // @gate experimental
629 it('shows the fallback of the outer if fallback is missing', async () => {
630 // This is the same exact test as above but with a nested Suspense without a fallback.
631 // This should be a noop.
@@ -695,6 +699,7 @@ describe('ReactDOMServerPartialHydration', () => {
699 expect(container.textContent).toBe('Hi');
700 });
701
702 + // @gate experimental
703 it('clears nested suspense boundaries if they did not hydrate yet', async () => {
704 let suspend = false;
705 let resolve;
@@ -767,6 +772,7 @@ describe('ReactDOMServerPartialHydration', () => {
772 expect(container.textContent).toBe('Hi Hi');
773 });
774
775 + // @gate experimental
776 it('hydrates first if props changed but we are able to resolve within a timeout', async () => {
777 let suspend = false;
778 let resolve;
@@ -847,6 +853,7 @@ describe('ReactDOMServerPartialHydration', () => {
853 expect(span.className).toBe('hi');
854 });
855
856 + // @gate experimental
857 it('blocks the update to hydrate first if context has changed', async () => {
858 let suspend = false;
859 let resolve;
@@ -931,6 +938,7 @@ describe('ReactDOMServerPartialHydration', () => {
938 expect(span.className).toBe('hi');
939 });
940
941 + // @gate experimental
942 it('shows the fallback if context has changed before hydration completes and is still suspended', async () => {
943 let suspend = false;
944 let resolve;
@@ -1014,6 +1022,7 @@ describe('ReactDOMServerPartialHydration', () => {
1022 expect(container.textContent).toBe('Hi');
1023 });
1024
1025 + // @gate experimental
1026 it('replaces the fallback with client content if it is not rendered by the server', async () => {
1027 let suspend = false;
1028 const promise = new Promise(resolvePromise => {});
@@ -1062,6 +1071,7 @@ describe('ReactDOMServerPartialHydration', () => {
1071 expect(ref.current).toBe(span);
1072 });
1073
1074 + // @gate experimental
1075 it('replaces the fallback within the suspended time if there is a nested suspense', async () => {
1076 let suspend = false;
1077 const promise = new Promise(resolvePromise => {});
@@ -1121,6 +1131,7 @@ describe('ReactDOMServerPartialHydration', () => {
1131 expect(ref.current).toBe(span);
1132 });
1133
1134 + // @gate experimental
1135 it('replaces the fallback within the suspended time if there is a nested suspense in a nested suspense', async () => {
1136 let suspend = false;
1137 const promise = new Promise(resolvePromise => {});
@@ -1182,6 +1193,7 @@ describe('ReactDOMServerPartialHydration', () => {
1193 expect(ref.current).toBe(span);
1194 });
1195
1196 + // @gate experimental
1197 it('waits for pending content to come in from the server and then hydrates it', async () => {
1198 let suspend = false;
1199 const promise = new Promise(resolvePromise => {});
@@ -1269,6 +1281,7 @@ describe('ReactDOMServerPartialHydration', () => {
1281 expect(ref.current).toBe(span);
1282 });
1283
1284 + // @gate experimental
1285 it('handles an error on the client if the server ends up erroring', async () => {
1286 let suspend = false;
1287 const promise = new Promise(resolvePromise => {});
@@ -1363,6 +1376,7 @@ describe('ReactDOMServerPartialHydration', () => {
1376 expect(ref.current).toBe(div);
1377 });
1378
1379 + // @gate experimental
1380 it('shows inserted items in a SuspenseList before content is hydrated', async () => {
1381 let suspend = false;
1382 let resolve;
@@ -1448,6 +1462,7 @@ describe('ReactDOMServerPartialHydration', () => {
1462 expect(ref.current).toBe(spanB);
1463 });
1464
1465 + // @gate experimental
1466 it('shows is able to hydrate boundaries even if others in a list are pending', async () => {
1467 let suspend = false;
1468 let resolve;
@@ -1522,6 +1537,7 @@ describe('ReactDOMServerPartialHydration', () => {
1537 expect(container.textContent).toBe('ALoading B');
1538 });
1539
1540 + // @gate experimental
1541 it('shows inserted items before pending in a SuspenseList as fallbacks', async () => {
1542 let suspend = false;
1543 let resolve;
@@ -1613,6 +1629,7 @@ describe('ReactDOMServerPartialHydration', () => {
1629 expect(container.textContent).toBe('ABC');
1630 });
1631
1632 + // @gate experimental
1633 it('can client render nested boundaries', async () => {
1634 let suspend = false;
1635 const promise = new Promise(() => {});
@@ -1667,6 +1684,7 @@ describe('ReactDOMServerPartialHydration', () => {
1684 expect(container.lastChild.data).toBe('unrelated comment');
1685 });
1686
1687 + // @gate experimental
1688 it('can hydrate TWO suspense boundaries', async () => {
1689 const ref1 = React.createRef();
1690 const ref2 = React.createRef();
@@ -1706,6 +1724,7 @@ describe('ReactDOMServerPartialHydration', () => {
1724 expect(ref2.current).toBe(span2);
1725 });
1726
1727 + // @gate experimental
1728 it('regenerates if it cannot hydrate before changes to props/context expire', async () => {
1729 let suspend = false;
1730 const promise = new Promise(resolvePromise => {});
@@ -1787,6 +1806,7 @@ describe('ReactDOMServerPartialHydration', () => {
1806 expect(newSpan.className).toBe('hi');
1807 });
1808
1809 + // @gate experimental
1810 it('does not invoke an event on a hydrated node until it commits', async () => {
1811 let suspend = false;
1812 let resolve;
@@ -1868,6 +1888,7 @@ describe('ReactDOMServerPartialHydration', () => {
1888 document.body.removeChild(container);
1889 });
1890
1891 + // @gate experimental
1892 it('does not invoke an event on a hydrated EventResponder until it commits', async () => {
1893 let suspend = false;
1894 let resolve;
@@ -1949,6 +1970,7 @@ describe('ReactDOMServerPartialHydration', () => {
1970 document.body.removeChild(container);
1971 });
1972
1973 + // @gate experimental
1974 it('invokes discrete events on nested suspense boundaries in a root (legacy system)', async () => {
1975 let suspend = false;
1976 let resolve;
@@ -2028,6 +2050,7 @@ describe('ReactDOMServerPartialHydration', () => {
2050 document.body.removeChild(container);
2051 });
2052
2053 + // @gate experimental
2054 it('invokes discrete events on nested suspense boundaries in a root (responder system)', async () => {
2055 let suspend = false;
2056 let resolve;
@@ -2110,6 +2133,7 @@ describe('ReactDOMServerPartialHydration', () => {
2133 document.body.removeChild(container);
2134 });
2135
2136 + // @gate experimental
2137 it('does not invoke the parent of dehydrated boundary event', async () => {
2138 let suspend = false;
2139 let resolve;
@@ -2184,6 +2208,7 @@ describe('ReactDOMServerPartialHydration', () => {
2208 document.body.removeChild(container);
2209 });
2210
2211 + // @gate experimental
2212 it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => {
2213 let suspend = false;
2214 let resolve;
@@ -2261,6 +2286,7 @@ describe('ReactDOMServerPartialHydration', () => {
2286 document.body.removeChild(parentContainer);
2287 });
2288
2289 + // @gate experimental
2290 it('blocks only on the last continuous event (legacy system)', async () => {
2291 let suspend1 = false;
2292 let resolve1;
@@ -2365,129 +2391,125 @@ describe('ReactDOMServerPartialHydration', () => {
2391 document.body.removeChild(container);
2392 });
2393
2368 - if (__EXPERIMENTAL__) {
2369 - it('blocks only on the last continuous event (Responder system)', async () => {
2370 - useHover = require('react-interactions/events/hover').useHover;
2394 + // @gate experimental
2395 + it('blocks only on the last continuous event (Responder system)', async () => {
2396 + useHover = require('react-interactions/events/hover').useHover;
2397
2372 - let suspend1 = false;
2373 - let resolve1;
2374 - const promise1 = new Promise(
2375 - resolvePromise => (resolve1 = resolvePromise),
2376 - );
2377 - let suspend2 = false;
2378 - let resolve2;
2379 - const promise2 = new Promise(
2380 - resolvePromise => (resolve2 = resolvePromise),
2381 - );
2398 + let suspend1 = false;
2399 + let resolve1;
2400 + const promise1 = new Promise(resolvePromise => (resolve1 = resolvePromise));
2401 + let suspend2 = false;
2402 + let resolve2;
2403 + const promise2 = new Promise(resolvePromise => (resolve2 = resolvePromise));
2404
2383 - function First({text}) {
2384 - if (suspend1) {
2385 - throw promise1;
2386 - } else {
2387 - return 'Hello';
2388 - }
2405 + function First({text}) {
2406 + if (suspend1) {
2407 + throw promise1;
2408 + } else {
2409 + return 'Hello';
2410 }
2411 + }
2412
2391 - function Second({text}) {
2392 - if (suspend2) {
2393 - throw promise2;
2394 - } else {
2395 - return 'World';
2396 - }
2413 + function Second({text}) {
2414 + if (suspend2) {
2415 + throw promise2;
2416 + } else {
2417 + return 'World';
2418 }
2419 + }
2420
2399 - const ops = [];
2400 -
2401 - function App() {
2402 - const listener1 = useHover({
2403 - onHoverStart() {
2404 - ops.push('Hover Start First');
2405 - },
2406 - onHoverEnd() {
2407 - ops.push('Hover End First');
2408 - },
2409 - });
2410 - const listener2 = useHover({
2411 - onHoverStart() {
2412 - ops.push('Hover Start Second');
2413 - },
2414 - onHoverEnd() {
2415 - ops.push('Hover End Second');
2416 - },
2417 - });
2418 - return (
2419 - <div>
2420 - <Suspense fallback="Loading First...">
2421 - <span DEPRECATED_flareListeners={listener1} />
2422 - {/* We suspend after to test what happens when we eager
2421 + const ops = [];
2422 +
2423 + function App() {
2424 + const listener1 = useHover({
2425 + onHoverStart() {
2426 + ops.push('Hover Start First');
2427 + },
2428 + onHoverEnd() {
2429 + ops.push('Hover End First');
2430 + },
2431 + });
2432 + const listener2 = useHover({
2433 + onHoverStart() {
2434 + ops.push('Hover Start Second');
2435 + },
2436 + onHoverEnd() {
2437 + ops.push('Hover End Second');
2438 + },
2439 + });
2440 + return (
2441 + <div>
2442 + <Suspense fallback="Loading First...">
2443 + <span DEPRECATED_flareListeners={listener1} />
2444 + {/* We suspend after to test what happens when we eager
2445 attach the listener. */}
2424 - <First />
2425 - </Suspense>
2426 - <Suspense fallback="Loading Second...">
2427 - <span DEPRECATED_flareListeners={listener2}>
2428 - <Second />
2429 - </span>
2430 - </Suspense>
2431 - </div>
2432 - );
2433 - }
2446 + <First />
2447 + </Suspense>
2448 + <Suspense fallback="Loading Second...">
2449 + <span DEPRECATED_flareListeners={listener2}>
2450 + <Second />
2451 + </span>
2452 + </Suspense>
2453 + </div>
2454 + );
2455 + }
2456
2435 - const finalHTML = ReactDOMServer.renderToString(<App />);
2436 - const container = document.createElement('div');
2437 - container.innerHTML = finalHTML;
2457 + const finalHTML = ReactDOMServer.renderToString(<App />);
2458 + const container = document.createElement('div');
2459 + container.innerHTML = finalHTML;
2460
2439 - // We need this to be in the document since we'll dispatch events on it.
2440 - document.body.appendChild(container);
2461 + // We need this to be in the document since we'll dispatch events on it.
2462 + document.body.appendChild(container);
2463
2442 - const appDiv = container.getElementsByTagName('div')[0];
2443 - const firstSpan = appDiv.getElementsByTagName('span')[0];
2444 - const secondSpan = appDiv.getElementsByTagName('span')[1];
2445 - expect(firstSpan.textContent).toBe('');
2446 - expect(secondSpan.textContent).toBe('World');
2464 + const appDiv = container.getElementsByTagName('div')[0];
2465 + const firstSpan = appDiv.getElementsByTagName('span')[0];
2466 + const secondSpan = appDiv.getElementsByTagName('span')[1];
2467 + expect(firstSpan.textContent).toBe('');
2468 + expect(secondSpan.textContent).toBe('World');
2469
2448 - // On the client we don't have all data yet but we want to start
2449 - // hydrating anyway.
2450 - suspend1 = true;
2451 - suspend2 = true;
2452 - const root = ReactDOM.createRoot(container, {hydrate: true});
2453 - root.render(<App />);
2470 + // On the client we don't have all data yet but we want to start
2471 + // hydrating anyway.
2472 + suspend1 = true;
2473 + suspend2 = true;
2474 + const root = ReactDOM.createRoot(container, {hydrate: true});
2475 + root.render(<App />);
2476
2455 - Scheduler.unstable_flushAll();
2456 - jest.runAllTimers();
2477 + Scheduler.unstable_flushAll();
2478 + jest.runAllTimers();
2479
2458 - dispatchMouseEvent(appDiv, null);
2459 - dispatchMouseEvent(firstSpan, appDiv);
2460 - dispatchMouseEvent(secondSpan, firstSpan);
2480 + dispatchMouseEvent(appDiv, null);
2481 + dispatchMouseEvent(firstSpan, appDiv);
2482 + dispatchMouseEvent(secondSpan, firstSpan);
2483
2462 - // Neither target is yet hydrated.
2463 - expect(ops).toEqual([]);
2484 + // Neither target is yet hydrated.
2485 + expect(ops).toEqual([]);
2486
2465 - // Resolving the second promise so that rendering can complete.
2466 - suspend2 = false;
2467 - resolve2();
2468 - await promise2;
2487 + // Resolving the second promise so that rendering can complete.
2488 + suspend2 = false;
2489 + resolve2();
2490 + await promise2;
2491
2470 - Scheduler.unstable_flushAll();
2471 - jest.runAllTimers();
2492 + Scheduler.unstable_flushAll();
2493 + jest.runAllTimers();
2494
2473 - // We've unblocked the current hover target so we should be
2474 - // able to replay it now.
2475 - expect(ops).toEqual(['Hover Start Second']);
2495 + // We've unblocked the current hover target so we should be
2496 + // able to replay it now.
2497 + expect(ops).toEqual(['Hover Start Second']);
2498
2477 - // Resolving the first promise has no effect now.
2478 - suspend1 = false;
2479 - resolve1();
2480 - await promise1;
2499 + // Resolving the first promise has no effect now.
2500 + suspend1 = false;
2501 + resolve1();
2502 + await promise1;
2503
2482 - Scheduler.unstable_flushAll();
2483 - jest.runAllTimers();
2504 + Scheduler.unstable_flushAll();
2505 + jest.runAllTimers();
2506
2485 - expect(ops).toEqual(['Hover Start Second']);
2507 + expect(ops).toEqual(['Hover Start Second']);
2508
2487 - document.body.removeChild(container);
2488 - });
2489 - }
2509 + document.body.removeChild(container);
2510 + });
2511
2512 + // @gate experimental
2513 it('finishes normal pri work before continuing to hydrate a retry', async () => {
2514 let suspend = false;
2515 let resolve;
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+195 -192
@@ -106,11 +106,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
106 Suspense = React.Suspense;
107 });
108
109 - if (!__EXPERIMENTAL__) {
110 - it("empty test so Jest doesn't complain", () => {});
111 - return;
112 - }
113 -
109 + // @gate experimental
110 it('hydrates the target boundary synchronously during a click', async () => {
111 function Child({text}) {
112 Scheduler.unstable_yieldValue(text);
@@ -173,6 +169,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
169 document.body.removeChild(container);
170 });
171
172 + // @gate experimental
173 it('hydrates at higher pri if sync did not work first time', async () => {
174 let suspend = false;
175 let resolve;
@@ -257,6 +254,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
254 document.body.removeChild(container);
255 });
256
257 + // @gate experimental
258 it('hydrates at higher pri for secondary discrete events', async () => {
259 let suspend = false;
260 let resolve;
@@ -349,247 +347,249 @@ describe('ReactDOMServerSelectiveHydration', () => {
347 document.body.removeChild(container);
348 });
349
352 - if (__EXPERIMENTAL__) {
353 - it('hydrates the target boundary synchronously during a click (flare)', async () => {
354 - const usePress = require('react-interactions/events/press').usePress;
355 -
356 - function Child({text}) {
357 - Scheduler.unstable_yieldValue(text);
358 - const listener = usePress({
359 - onPress() {
360 - Scheduler.unstable_yieldValue('Clicked ' + text);
361 - },
362 - });
350 + // @gate experimental
351 + it('hydrates the target boundary synchronously during a click (flare)', async () => {
352 + const usePress = require('react-interactions/events/press').usePress;
353
364 - return <span DEPRECATED_flareListeners={listener}>{text}</span>;
365 - }
354 + function Child({text}) {
355 + Scheduler.unstable_yieldValue(text);
356 + const listener = usePress({
357 + onPress() {
358 + Scheduler.unstable_yieldValue('Clicked ' + text);
359 + },
360 + });
361
367 - function App() {
368 - Scheduler.unstable_yieldValue('App');
369 - return (
370 - <div>
371 - <Suspense fallback="Loading...">
372 - <Child text="A" />
373 - </Suspense>
374 - <Suspense fallback="Loading...">
375 - <Child text="B" />
376 - </Suspense>
377 - </div>
378 - );
379 - }
362 + return <span DEPRECATED_flareListeners={listener}>{text}</span>;
363 + }
364
381 - const finalHTML = ReactDOMServer.renderToString(<App />);
365 + function App() {
366 + Scheduler.unstable_yieldValue('App');
367 + return (
368 + <div>
369 + <Suspense fallback="Loading...">
370 + <Child text="A" />
371 + </Suspense>
372 + <Suspense fallback="Loading...">
373 + <Child text="B" />
374 + </Suspense>
375 + </div>
376 + );
377 + }
378
383 - expect(Scheduler).toHaveYielded(['App', 'A', 'B']);
379 + const finalHTML = ReactDOMServer.renderToString(<App />);
380
385 - const container = document.createElement('div');
386 - // We need this to be in the document since we'll dispatch events on it.
387 - document.body.appendChild(container);
381 + expect(Scheduler).toHaveYielded(['App', 'A', 'B']);
382
389 - container.innerHTML = finalHTML;
383 + const container = document.createElement('div');
384 + // We need this to be in the document since we'll dispatch events on it.
385 + document.body.appendChild(container);
386
391 - const root = ReactDOM.createRoot(container, {hydrate: true});
392 - root.render(<App />);
387 + container.innerHTML = finalHTML;
388
394 - // Nothing has been hydrated so far.
395 - expect(Scheduler).toHaveYielded([]);
389 + const root = ReactDOM.createRoot(container, {hydrate: true});
390 + root.render(<App />);
391
397 - const span = container.getElementsByTagName('span')[1];
392 + // Nothing has been hydrated so far.
393 + expect(Scheduler).toHaveYielded([]);
394
399 - const target = createEventTarget(span);
395 + const span = container.getElementsByTagName('span')[1];
396
401 - // This should synchronously hydrate the root App and the second suspense
402 - // boundary.
403 - const preventDefault = jest.fn();
404 - target.virtualclick({preventDefault});
397 + const target = createEventTarget(span);
398
406 - // The event should have been canceled because we called preventDefault.
407 - expect(preventDefault).toHaveBeenCalled();
399 + // This should synchronously hydrate the root App and the second suspense
400 + // boundary.
401 + const preventDefault = jest.fn();
402 + target.virtualclick({preventDefault});
403
409 - // We rendered App, B and then invoked the event without rendering A.
410 - expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']);
404 + // The event should have been canceled because we called preventDefault.
405 + expect(preventDefault).toHaveBeenCalled();
406
412 - // After continuing the scheduler, we finally hydrate A.
413 - expect(Scheduler).toFlushAndYield(['A']);
407 + // We rendered App, B and then invoked the event without rendering A.
408 + expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']);
409
415 - document.body.removeChild(container);
416 - });
410 + // After continuing the scheduler, we finally hydrate A.
411 + expect(Scheduler).toFlushAndYield(['A']);
412
418 - it('hydrates at higher pri if sync did not work first time (flare)', async () => {
419 - const usePress = require('react-interactions/events/press').usePress;
420 - let suspend = false;
421 - let resolve;
422 - const promise = new Promise(resolvePromise => (resolve = resolvePromise));
413 + document.body.removeChild(container);
414 + });
415
424 - function Child({text}) {
425 - if ((text === 'A' || text === 'D') && suspend) {
426 - throw promise;
427 - }
428 - Scheduler.unstable_yieldValue(text);
416 + // @gate experimental
417 + it('hydrates at higher pri if sync did not work first time (flare)', async () => {
418 + const usePress = require('react-interactions/events/press').usePress;
419 + let suspend = false;
420 + let resolve;
421 + const promise = new Promise(resolvePromise => (resolve = resolvePromise));
422
430 - const listener = usePress({
431 - onPress() {
432 - Scheduler.unstable_yieldValue('Clicked ' + text);
433 - },
434 - });
435 - return <span DEPRECATED_flareListeners={listener}>{text}</span>;
423 + function Child({text}) {
424 + if ((text === 'A' || text === 'D') && suspend) {
425 + throw promise;
426 }
427 + Scheduler.unstable_yieldValue(text);
428
438 - function App() {
439 - Scheduler.unstable_yieldValue('App');
440 - return (
441 - <div>
442 - <Suspense fallback="Loading...">
443 - <Child text="A" />
444 - </Suspense>
445 - <Suspense fallback="Loading...">
446 - <Child text="B" />
447 - </Suspense>
448 - <Suspense fallback="Loading...">
449 - <Child text="C" />
450 - </Suspense>
451 - <Suspense fallback="Loading...">
452 - <Child text="D" />
453 - </Suspense>
454 - </div>
455 - );
456 - }
429 + const listener = usePress({
430 + onPress() {
431 + Scheduler.unstable_yieldValue('Clicked ' + text);
432 + },
433 + });
434 + return <span DEPRECATED_flareListeners={listener}>{text}</span>;
435 + }
436
458 - const finalHTML = ReactDOMServer.renderToString(<App />);
437 + function App() {
438 + Scheduler.unstable_yieldValue('App');
439 + return (
440 + <div>
441 + <Suspense fallback="Loading...">
442 + <Child text="A" />
443 + </Suspense>
444 + <Suspense fallback="Loading...">
445 + <Child text="B" />
446 + </Suspense>
447 + <Suspense fallback="Loading...">
448 + <Child text="C" />
449 + </Suspense>
450 + <Suspense fallback="Loading...">
451 + <Child text="D" />
452 + </Suspense>
453 + </div>
454 + );
455 + }
456
460 - expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
457 + const finalHTML = ReactDOMServer.renderToString(<App />);
458
462 - const container = document.createElement('div');
463 - // We need this to be in the document since we'll dispatch events on it.
464 - document.body.appendChild(container);
459 + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
460
466 - container.innerHTML = finalHTML;
461 + const container = document.createElement('div');
462 + // We need this to be in the document since we'll dispatch events on it.
463 + document.body.appendChild(container);
464
468 - const spanD = container.getElementsByTagName('span')[3];
465 + container.innerHTML = finalHTML;
466
470 - suspend = true;
467 + const spanD = container.getElementsByTagName('span')[3];
468
472 - // A and D will be suspended. We'll click on D which should take
473 - // priority, after we unsuspend.
474 - const root = ReactDOM.createRoot(container, {hydrate: true});
475 - root.render(<App />);
469 + suspend = true;
470
477 - // Nothing has been hydrated so far.
478 - expect(Scheduler).toHaveYielded([]);
471 + // A and D will be suspended. We'll click on D which should take
472 + // priority, after we unsuspend.
473 + const root = ReactDOM.createRoot(container, {hydrate: true});
474 + root.render(<App />);
475
480 - // This click target cannot be hydrated yet because it's suspended.
481 - const result = dispatchClickEvent(spanD);
476 + // Nothing has been hydrated so far.
477 + expect(Scheduler).toHaveYielded([]);
478
483 - expect(Scheduler).toHaveYielded(['App']);
479 + // This click target cannot be hydrated yet because it's suspended.
480 + const result = dispatchClickEvent(spanD);
481
485 - expect(result).toBe(true);
482 + expect(Scheduler).toHaveYielded(['App']);
483
487 - // Continuing rendering will render B next.
488 - expect(Scheduler).toFlushAndYield(['B', 'C']);
484 + expect(result).toBe(true);
485
490 - suspend = false;
491 - resolve();
492 - await promise;
486 + // Continuing rendering will render B next.
487 + expect(Scheduler).toFlushAndYield(['B', 'C']);
488
494 - // After the click, we should prioritize D and the Click first,
495 - // and only after that render A and C.
496 - expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']);
489 + suspend = false;
490 + resolve();
491 + await promise;
492
498 - document.body.removeChild(container);
499 - });
493 + // After the click, we should prioritize D and the Click first,
494 + // and only after that render A and C.
495 + expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']);
496
501 - it('hydrates at higher pri for secondary discrete events (flare)', async () => {
502 - const usePress = require('react-interactions/events/press').usePress;
503 - let suspend = false;
504 - let resolve;
505 - const promise = new Promise(resolvePromise => (resolve = resolvePromise));
497 + document.body.removeChild(container);
498 + });
499
507 - function Child({text}) {
508 - if ((text === 'A' || text === 'D') && suspend) {
509 - throw promise;
510 - }
511 - Scheduler.unstable_yieldValue(text);
500 + // @gate experimental
501 + it('hydrates at higher pri for secondary discrete events (flare)', async () => {
502 + const usePress = require('react-interactions/events/press').usePress;
503 + let suspend = false;
504 + let resolve;
505 + const promise = new Promise(resolvePromise => (resolve = resolvePromise));
506
513 - const listener = usePress({
514 - onPress() {
515 - Scheduler.unstable_yieldValue('Clicked ' + text);
516 - },
517 - });
518 - return <span DEPRECATED_flareListeners={listener}>{text}</span>;
507 + function Child({text}) {
508 + if ((text === 'A' || text === 'D') && suspend) {
509 + throw promise;
510 }
511 + Scheduler.unstable_yieldValue(text);
512
521 - function App() {
522 - Scheduler.unstable_yieldValue('App');
523 - return (
524 - <div>
525 - <Suspense fallback="Loading...">
526 - <Child text="A" />
527 - </Suspense>
528 - <Suspense fallback="Loading...">
529 - <Child text="B" />
530 - </Suspense>
531 - <Suspense fallback="Loading...">
532 - <Child text="C" />
533 - </Suspense>
534 - <Suspense fallback="Loading...">
535 - <Child text="D" />
536 - </Suspense>
537 - </div>
538 - );
539 - }
513 + const listener = usePress({
514 + onPress() {
515 + Scheduler.unstable_yieldValue('Clicked ' + text);
516 + },
517 + });
518 + return <span DEPRECATED_flareListeners={listener}>{text}</span>;
519 + }
520
541 - const finalHTML = ReactDOMServer.renderToString(<App />);
521 + function App() {
522 + Scheduler.unstable_yieldValue('App');
523 + return (
524 + <div>
525 + <Suspense fallback="Loading...">
526 + <Child text="A" />
527 + </Suspense>
528 + <Suspense fallback="Loading...">
529 + <Child text="B" />
530 + </Suspense>
531 + <Suspense fallback="Loading...">
532 + <Child text="C" />
533 + </Suspense>
534 + <Suspense fallback="Loading...">
535 + <Child text="D" />
536 + </Suspense>
537 + </div>
538 + );
539 + }
540
543 - expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
541 + const finalHTML = ReactDOMServer.renderToString(<App />);
542
545 - const container = document.createElement('div');
546 - // We need this to be in the document since we'll dispatch events on it.
547 - document.body.appendChild(container);
543 + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
544
549 - container.innerHTML = finalHTML;
545 + const container = document.createElement('div');
546 + // We need this to be in the document since we'll dispatch events on it.
547 + document.body.appendChild(container);
548
551 - const spanA = container.getElementsByTagName('span')[0];
552 - const spanC = container.getElementsByTagName('span')[2];
553 - const spanD = container.getElementsByTagName('span')[3];
549 + container.innerHTML = finalHTML;
550
555 - suspend = true;
551 + const spanA = container.getElementsByTagName('span')[0];
552 + const spanC = container.getElementsByTagName('span')[2];
553 + const spanD = container.getElementsByTagName('span')[3];
554
557 - // A and D will be suspended. We'll click on D which should take
558 - // priority, after we unsuspend.
559 - const root = ReactDOM.createRoot(container, {hydrate: true});
560 - root.render(<App />);
555 + suspend = true;
556
562 - // Nothing has been hydrated so far.
563 - expect(Scheduler).toHaveYielded([]);
557 + // A and D will be suspended. We'll click on D which should take
558 + // priority, after we unsuspend.
559 + const root = ReactDOM.createRoot(container, {hydrate: true});
560 + root.render(<App />);
561
565 - // This click target cannot be hydrated yet because the first is Suspended.
566 - dispatchClickEvent(spanA);
567 - dispatchClickEvent(spanC);
568 - dispatchClickEvent(spanD);
562 + // Nothing has been hydrated so far.
563 + expect(Scheduler).toHaveYielded([]);
564
570 - expect(Scheduler).toHaveYielded(['App']);
565 + // This click target cannot be hydrated yet because the first is Suspended.
566 + dispatchClickEvent(spanA);
567 + dispatchClickEvent(spanC);
568 + dispatchClickEvent(spanD);
569
572 - suspend = false;
573 - resolve();
574 - await promise;
570 + expect(Scheduler).toHaveYielded(['App']);
571
576 - // We should prioritize hydrating A, C and D first since we clicked in
577 - // them. Only after they're done will we hydrate B.
578 - expect(Scheduler).toFlushAndYield([
579 - 'A',
580 - 'Clicked A',
581 - 'C',
582 - 'Clicked C',
583 - 'D',
584 - 'Clicked D',
585 - // B should render last since it wasn't clicked.
586 - 'B',
587 - ]);
572 + suspend = false;
573 + resolve();
574 + await promise;
575
589 - document.body.removeChild(container);
590 - });
591 - }
576 + // We should prioritize hydrating A, C and D first since we clicked in
577 + // them. Only after they're done will we hydrate B.
578 + expect(Scheduler).toFlushAndYield([
579 + 'A',
580 + 'Clicked A',
581 + 'C',
582 + 'Clicked C',
583 + 'D',
584 + 'Clicked D',
585 + // B should render last since it wasn't clicked.
586 + 'B',
587 + ]);
588 +
589 + document.body.removeChild(container);
590 + });
591
592 + // @gate experimental
593 it('hydrates the hovered targets as higher priority for continuous events', async () => {
594 let suspend = false;
595 let resolve;
@@ -690,6 +690,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
690 document.body.removeChild(container);
691 });
692
693 + // @gate experimental
694 it('hydrates the last target path first for continuous events', async () => {
695 let suspend = false;
696 let resolve;
@@ -775,6 +776,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
776 document.body.removeChild(container);
777 });
778
779 + // @gate experimental
780 it('hydrates the last explicitly hydrated target at higher priority', async () => {
781 function Child({text}) {
782 Scheduler.unstable_yieldValue(text);
@@ -823,6 +825,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
825 expect(Scheduler).toFlushAndYield(['App', 'C', 'B', 'A']);
826 });
827
828 + // @gate experimental
829 it('hydrates before an update even if hydration moves away from it', async () => {
830 function Child({text}) {
831 Scheduler.unstable_yieldValue(text);
packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+34 -32
@@ -44,11 +44,6 @@ describe('ReactDOMServerSuspense', () => {
44 resetModules();
45 });
46
47 - if (!__EXPERIMENTAL__) {
48 - it("empty test so Jest doesn't complain", () => {});
49 - return;
50 - }
51 -
47 function Text(props) {
48 return <div>{props.text}</div>;
49 }
@@ -57,6 +52,7 @@ describe('ReactDOMServerSuspense', () => {
52 throw new Promise(() => {});
53 }
54
55 + // @gate experimental || www
56 it('should render the children when no promise is thrown', async () => {
57 const c = await serverRender(
58 <div>
@@ -71,6 +67,7 @@ describe('ReactDOMServerSuspense', () => {
67 expect(e.textContent).toBe('Children');
68 });
69
70 + // @gate experimental || www
71 it('should render the fallback when a promise thrown', async () => {
72 const c = await serverRender(
73 <div>
@@ -85,6 +82,7 @@ describe('ReactDOMServerSuspense', () => {
82 expect(e.textContent).toBe('Fallback');
83 });
84
85 + // @gate experimental || www
86 it('should work with nested suspense components', async () => {
87 const c = await serverRender(
88 <div>
@@ -105,6 +103,7 @@ describe('ReactDOMServerSuspense', () => {
103 );
104 });
105
106 + // @gate experimental
107 it('server renders a SuspenseList component and its children', async () => {
108 const example = (
109 <React.SuspenseList>
@@ -137,33 +136,36 @@ describe('ReactDOMServerSuspense', () => {
136 expect(divB).toBe(divB2);
137 });
138
140 - itThrowsWhenRendering(
141 - 'a suspending component outside a Suspense node',
142 - async render => {
143 - await render(
144 - <div>
145 - <React.Suspense />
146 - <AsyncText text="Children" />
147 - <React.Suspense />
148 - </div>,
149 - 1,
150 - );
151 - },
152 - 'Add a <Suspense fallback=...> component higher in the tree',
153 - );
154 -
155 - itThrowsWhenRendering(
156 - 'a suspending component without a Suspense above',
157 - async render => {
158 - await render(
159 - <div>
160 - <AsyncText text="Children" />
161 - </div>,
162 - 1,
163 - );
164 - },
165 - 'Add a <Suspense fallback=...> component higher in the tree',
166 - );
139 + // TODO: Remove this in favor of @gate pragma
140 + if (__EXPERIMENTAL__) {
141 + itThrowsWhenRendering(
142 + 'a suspending component outside a Suspense node',
143 + async render => {
144 + await render(
145 + <div>
146 + <React.Suspense />
147 + <AsyncText text="Children" />
148 + <React.Suspense />
149 + </div>,
150 + 1,
151 + );
152 + },
153 + 'Add a <Suspense fallback=...> component higher in the tree',
154 + );
155 +
156 + itThrowsWhenRendering(
157 + 'a suspending component without a Suspense above',
158 + async render => {
159 + await render(
160 + <div>
161 + <AsyncText text="Children" />
162 + </div>,
163 + 1,
164 + );
165 + },
166 + 'Add a <Suspense fallback=...> component higher in the tree',
167 + );
168 + }
169
170 it('does not get confused by throwing null', () => {
171 function Bad() {
packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js
+255 -264
@@ -18,324 +18,315 @@ const renderSubtreeIntoContainer = require('react-dom')
18
19 const ReactFeatureFlags = require('shared/ReactFeatureFlags');
20
21 -// Once this flag is always true, we should delete this test file
22 -if (__EXPERIMENTAL__) {
23 - describe('renderSubtreeIntoContainer', () => {
24 - it('empty test', () => {
25 - // Empty test to prevent "Your test suite must contain at least one test." error.
26 - });
27 - });
28 -} else {
29 - describe('renderSubtreeIntoContainer', () => {
30 - it('should pass context when rendering subtree elsewhere', () => {
31 - const portal = document.createElement('div');
32 -
33 - class Component extends React.Component {
34 - static contextTypes = {
35 - foo: PropTypes.string.isRequired,
36 - };
21 +describe('renderSubtreeIntoContainer', () => {
22 + it('should pass context when rendering subtree elsewhere', () => {
23 + const portal = document.createElement('div');
24
38 - render() {
39 - return <div>{this.context.foo}</div>;
40 - }
25 + class Component extends React.Component {
26 + static contextTypes = {
27 + foo: PropTypes.string.isRequired,
28 + };
29 +
30 + render() {
31 + return <div>{this.context.foo}</div>;
32 }
33 + }
34
43 - class Parent extends React.Component {
44 - static childContextTypes = {
45 - foo: PropTypes.string.isRequired,
46 - };
35 + class Parent extends React.Component {
36 + static childContextTypes = {
37 + foo: PropTypes.string.isRequired,
38 + };
39
48 - getChildContext() {
49 - return {
50 - foo: 'bar',
51 - };
52 - }
40 + getChildContext() {
41 + return {
42 + foo: 'bar',
43 + };
44 + }
45
54 - render() {
55 - return null;
56 - }
46 + render() {
47 + return null;
48 + }
49
58 - componentDidMount() {
59 - if (ReactFeatureFlags.warnUnstableRenderSubtreeIntoContainer) {
60 - expect(
61 - function() {
62 - renderSubtreeIntoContainer(this, <Component />, portal);
63 - }.bind(this),
64 - ).toWarnDev(
65 - 'ReactDOM.unstable_renderSubtreeIntoContainer() is deprecated and ' +
66 - 'will be removed in a future major release. Consider using React Portals instead.',
67 - );
68 - } else {
69 - renderSubtreeIntoContainer(this, <Component />, portal);
70 - }
50 + componentDidMount() {
51 + if (ReactFeatureFlags.warnUnstableRenderSubtreeIntoContainer) {
52 + expect(
53 + function() {
54 + renderSubtreeIntoContainer(this, <Component />, portal);
55 + }.bind(this),
56 + ).toWarnDev(
57 + 'ReactDOM.unstable_renderSubtreeIntoContainer() is deprecated and ' +
58 + 'will be removed in a future major release. Consider using React Portals instead.',
59 + );
60 + } else {
61 + renderSubtreeIntoContainer(this, <Component />, portal);
62 }
63 }
64 + }
65
74 - ReactTestUtils.renderIntoDocument(<Parent />);
75 - expect(portal.firstChild.innerHTML).toBe('bar');
76 - });
66 + ReactTestUtils.renderIntoDocument(<Parent />);
67 + expect(portal.firstChild.innerHTML).toBe('bar');
68 + });
69
78 - it('should throw if parentComponent is invalid', () => {
79 - const portal = document.createElement('div');
70 + it('should throw if parentComponent is invalid', () => {
71 + const portal = document.createElement('div');
72
81 - class Component extends React.Component {
82 - static contextTypes = {
83 - foo: PropTypes.string.isRequired,
84 - };
73 + class Component extends React.Component {
74 + static contextTypes = {
75 + foo: PropTypes.string.isRequired,
76 + };
77
86 - render() {
87 - return <div>{this.context.foo}</div>;
88 - }
78 + render() {
79 + return <div>{this.context.foo}</div>;
80 }
90 -
91 - // ESLint is confused here and thinks Parent is unused, presumably because
92 - // it is only used inside of the class body?
93 - // eslint-disable-next-line no-unused-vars
94 - class Parent extends React.Component {
95 - static childContextTypes = {
96 - foo: PropTypes.string.isRequired,
81 + }
82 +
83 + // ESLint is confused here and thinks Parent is unused, presumably because
84 + // it is only used inside of the class body?
85 + // eslint-disable-next-line no-unused-vars
86 + class Parent extends React.Component {
87 + static childContextTypes = {
88 + foo: PropTypes.string.isRequired,
89 + };
90 +
91 + getChildContext() {
92 + return {
93 + foo: 'bar',
94 };
95 + }
96
99 - getChildContext() {
100 - return {
101 - foo: 'bar',
102 - };
103 - }
104 -
105 - render() {
106 - return null;
107 - }
97 + render() {
98 + return null;
99 + }
100
109 - componentDidMount() {
110 - expect(function() {
111 - renderSubtreeIntoContainer(<Parent />, <Component />, portal);
112 - }).toThrowError('parentComponentmust be a valid React Component');
113 - }
101 + componentDidMount() {
102 + expect(function() {
103 + renderSubtreeIntoContainer(<Parent />, <Component />, portal);
104 + }).toThrowError('parentComponentmust be a valid React Component');
105 }
115 - });
106 + }
107 + });
108
117 - it('should update context if it changes due to setState', () => {
118 - const container = document.createElement('div');
119 - document.body.appendChild(container);
120 - const portal = document.createElement('div');
109 + it('should update context if it changes due to setState', () => {
110 + const container = document.createElement('div');
111 + document.body.appendChild(container);
112 + const portal = document.createElement('div');
113
122 - class Component extends React.Component {
123 - static contextTypes = {
124 - foo: PropTypes.string.isRequired,
125 - getFoo: PropTypes.func.isRequired,
126 - };
114 + class Component extends React.Component {
115 + static contextTypes = {
116 + foo: PropTypes.string.isRequired,
117 + getFoo: PropTypes.func.isRequired,
118 + };
119
128 - render() {
129 - return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
130 - }
120 + render() {
121 + return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
122 }
132 -
133 - class Parent extends React.Component {
134 - static childContextTypes = {
135 - foo: PropTypes.string.isRequired,
136 - getFoo: PropTypes.func.isRequired,
137 - };
138 -
139 - state = {
140 - bar: 'initial',
123 + }
124 +
125 + class Parent extends React.Component {
126 + static childContextTypes = {
127 + foo: PropTypes.string.isRequired,
128 + getFoo: PropTypes.func.isRequired,
129 + };
130 +
131 + state = {
132 + bar: 'initial',
133 + };
134 +
135 + getChildContext() {
136 + return {
137 + foo: this.state.bar,
138 + getFoo: () => this.state.bar,
139 };
140 + }
141
143 - getChildContext() {
144 - return {
145 - foo: this.state.bar,
146 - getFoo: () => this.state.bar,
147 - };
148 - }
149 -
150 - render() {
151 - return null;
152 - }
142 + render() {
143 + return null;
144 + }
145
154 - componentDidMount() {
155 - renderSubtreeIntoContainer(this, <Component />, portal);
156 - }
146 + componentDidMount() {
147 + renderSubtreeIntoContainer(this, <Component />, portal);
148 + }
149
158 - componentDidUpdate() {
159 - renderSubtreeIntoContainer(this, <Component />, portal);
160 - }
150 + componentDidUpdate() {
151 + renderSubtreeIntoContainer(this, <Component />, portal);
152 }
153 + }
154
163 - const instance = ReactDOM.render(<Parent />, container);
164 - expect(portal.firstChild.innerHTML).toBe('initial-initial');
165 - instance.setState({bar: 'changed'});
166 - expect(portal.firstChild.innerHTML).toBe('changed-changed');
167 - });
155 + const instance = ReactDOM.render(<Parent />, container);
156 + expect(portal.firstChild.innerHTML).toBe('initial-initial');
157 + instance.setState({bar: 'changed'});
158 + expect(portal.firstChild.innerHTML).toBe('changed-changed');
159 + });
160
169 - it('should update context if it changes due to re-render', () => {
170 - const container = document.createElement('div');
171 - document.body.appendChild(container);
172 - const portal = document.createElement('div');
161 + it('should update context if it changes due to re-render', () => {
162 + const container = document.createElement('div');
163 + document.body.appendChild(container);
164 + const portal = document.createElement('div');
165
174 - class Component extends React.Component {
175 - static contextTypes = {
176 - foo: PropTypes.string.isRequired,
177 - getFoo: PropTypes.func.isRequired,
178 - };
166 + class Component extends React.Component {
167 + static contextTypes = {
168 + foo: PropTypes.string.isRequired,
169 + getFoo: PropTypes.func.isRequired,
170 + };
171
180 - render() {
181 - return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
182 - }
172 + render() {
173 + return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
174 }
184 -
185 - class Parent extends React.Component {
186 - static childContextTypes = {
187 - foo: PropTypes.string.isRequired,
188 - getFoo: PropTypes.func.isRequired,
175 + }
176 +
177 + class Parent extends React.Component {
178 + static childContextTypes = {
179 + foo: PropTypes.string.isRequired,
180 + getFoo: PropTypes.func.isRequired,
181 + };
182 +
183 + getChildContext() {
184 + return {
185 + foo: this.props.bar,
186 + getFoo: () => this.props.bar,
187 };
188 + }
189
191 - getChildContext() {
192 - return {
193 - foo: this.props.bar,
194 - getFoo: () => this.props.bar,
195 - };
196 - }
197 -
198 - render() {
199 - return null;
200 - }
190 + render() {
191 + return null;
192 + }
193
202 - componentDidMount() {
203 - renderSubtreeIntoContainer(this, <Component />, portal);
204 - }
194 + componentDidMount() {
195 + renderSubtreeIntoContainer(this, <Component />, portal);
196 + }
197
206 - componentDidUpdate() {
207 - renderSubtreeIntoContainer(this, <Component />, portal);
208 - }
198 + componentDidUpdate() {
199 + renderSubtreeIntoContainer(this, <Component />, portal);
200 }
201 + }
202
211 - ReactDOM.render(<Parent bar="initial" />, container);
212 - expect(portal.firstChild.innerHTML).toBe('initial-initial');
213 - ReactDOM.render(<Parent bar="changed" />, container);
214 - expect(portal.firstChild.innerHTML).toBe('changed-changed');
215 - });
203 + ReactDOM.render(<Parent bar="initial" />, container);
204 + expect(portal.firstChild.innerHTML).toBe('initial-initial');
205 + ReactDOM.render(<Parent bar="changed" />, container);
206 + expect(portal.firstChild.innerHTML).toBe('changed-changed');
207 + });
208
217 - it('should render portal with non-context-provider parent', () => {
218 - const container = document.createElement('div');
219 - document.body.appendChild(container);
220 - const portal = document.createElement('div');
209 + it('should render portal with non-context-provider parent', () => {
210 + const container = document.createElement('div');
211 + document.body.appendChild(container);
212 + const portal = document.createElement('div');
213
222 - class Parent extends React.Component {
223 - render() {
224 - return null;
225 - }
214 + class Parent extends React.Component {
215 + render() {
216 + return null;
217 + }
218
227 - componentDidMount() {
228 - renderSubtreeIntoContainer(this, <div>hello</div>, portal);
229 - }
219 + componentDidMount() {
220 + renderSubtreeIntoContainer(this, <div>hello</div>, portal);
221 }
222 + }
223
232 - ReactDOM.render(<Parent bar="initial" />, container);
233 - expect(portal.firstChild.innerHTML).toBe('hello');
234 - });
224 + ReactDOM.render(<Parent bar="initial" />, container);
225 + expect(portal.firstChild.innerHTML).toBe('hello');
226 + });
227
236 - it('should get context through non-context-provider parent', () => {
237 - const container = document.createElement('div');
238 - document.body.appendChild(container);
239 - const portal = document.createElement('div');
228 + it('should get context through non-context-provider parent', () => {
229 + const container = document.createElement('div');
230 + document.body.appendChild(container);
231 + const portal = document.createElement('div');
232
241 - class Parent extends React.Component {
242 - render() {
243 - return <Middle />;
244 - }
245 - getChildContext() {
246 - return {value: this.props.value};
247 - }
248 - static childContextTypes = {
249 - value: PropTypes.string.isRequired,
250 - };
233 + class Parent extends React.Component {
234 + render() {
235 + return <Middle />;
236 }
252 -
253 - class Middle extends React.Component {
254 - render() {
255 - return null;
256 - }
257 - componentDidMount() {
258 - renderSubtreeIntoContainer(this, <Child />, portal);
259 - }
237 + getChildContext() {
238 + return {value: this.props.value};
239 }
261 -
262 - class Child extends React.Component {
263 - static contextTypes = {
264 - value: PropTypes.string.isRequired,
265 - };
266 - render() {
267 - return <div>{this.context.value}</div>;
268 - }
240 + static childContextTypes = {
241 + value: PropTypes.string.isRequired,
242 + };
243 + }
244 +
245 + class Middle extends React.Component {
246 + render() {
247 + return null;
248 + }
249 + componentDidMount() {
250 + renderSubtreeIntoContainer(this, <Child />, portal);
251 }
252 + }
253 +
254 + class Child extends React.Component {
255 + static contextTypes = {
256 + value: PropTypes.string.isRequired,
257 + };
258 + render() {
259 + return <div>{this.context.value}</div>;
260 + }
261 + }
262
271 - ReactDOM.render(<Parent value="foo" />, container);
272 - expect(portal.textContent).toBe('foo');
273 - });
263 + ReactDOM.render(<Parent value="foo" />, container);
264 + expect(portal.textContent).toBe('foo');
265 + });
266
275 - it('should get context through middle non-context-provider layer', () => {
276 - const container = document.createElement('div');
277 - document.body.appendChild(container);
278 - const portal1 = document.createElement('div');
279 - const portal2 = document.createElement('div');
267 + it('should get context through middle non-context-provider layer', () => {
268 + const container = document.createElement('div');
269 + document.body.appendChild(container);
270 + const portal1 = document.createElement('div');
271 + const portal2 = document.createElement('div');
272
281 - class Parent extends React.Component {
282 - render() {
283 - return null;
284 - }
285 - getChildContext() {
286 - return {value: this.props.value};
287 - }
288 - componentDidMount() {
289 - renderSubtreeIntoContainer(this, <Middle />, portal1);
290 - }
291 - static childContextTypes = {
292 - value: PropTypes.string.isRequired,
293 - };
273 + class Parent extends React.Component {
274 + render() {
275 + return null;
276 }
295 -
296 - class Middle extends React.Component {
297 - render() {
298 - return null;
299 - }
300 - componentDidMount() {
301 - renderSubtreeIntoContainer(this, <Child />, portal2);
302 - }
277 + getChildContext() {
278 + return {value: this.props.value};
279 }
304 -
305 - class Child extends React.Component {
306 - static contextTypes = {
307 - value: PropTypes.string.isRequired,
308 - };
309 - render() {
310 - return <div>{this.context.value}</div>;
311 - }
280 + componentDidMount() {
281 + renderSubtreeIntoContainer(this, <Middle />, portal1);
282 }
283 + static childContextTypes = {
284 + value: PropTypes.string.isRequired,
285 + };
286 + }
287 +
288 + class Middle extends React.Component {
289 + render() {
290 + return null;
291 + }
292 + componentDidMount() {
293 + renderSubtreeIntoContainer(this, <Child />, portal2);
294 + }
295 + }
296 +
297 + class Child extends React.Component {
298 + static contextTypes = {
299 + value: PropTypes.string.isRequired,
300 + };
301 + render() {
302 + return <div>{this.context.value}</div>;
303 + }
304 + }
305
314 - ReactDOM.render(<Parent value="foo" />, container);
315 - expect(portal2.textContent).toBe('foo');
316 - });
306 + ReactDOM.render(<Parent value="foo" />, container);
307 + expect(portal2.textContent).toBe('foo');
308 + });
309
318 - it('fails gracefully when mixing React 15 and 16', () => {
319 - class C extends React.Component {
320 - render() {
321 - return <div />;
322 - }
310 + it('fails gracefully when mixing React 15 and 16', () => {
311 + class C extends React.Component {
312 + render() {
313 + return <div />;
314 }
324 - const c = ReactDOM.render(<C />, document.createElement('div'));
325 - // React 15 calls this:
326 - // https://github.com/facebook/react/blob/77b71fc3c4/src/renderers/dom/client/ReactMount.js#L478-L479
327 - expect(() => {
328 - c._reactInternalInstance._processChildContext({});
329 - }).toThrow(
330 - __DEV__
331 - ? '_processChildContext is not available in React 16+. This likely ' +
332 - 'means you have multiple copies of React and are attempting to nest ' +
333 - 'a React 15 tree inside a React 16 tree using ' +
334 - "unstable_renderSubtreeIntoContainer, which isn't supported. Try to " +
335 - 'make sure you have only one copy of React (and ideally, switch to ' +
336 - 'ReactDOM.createPortal).'
337 - : "Cannot read property '_processChildContext' of undefined",
338 - );
339 - });
315 + }
316 + const c = ReactDOM.render(<C />, document.createElement('div'));
317 + // React 15 calls this:
318 + // https://github.com/facebook/react/blob/77b71fc3c4/src/renderers/dom/client/ReactMount.js#L478-L479
319 + expect(() => {
320 + c._reactInternalInstance._processChildContext({});
321 + }).toThrow(
322 + __DEV__
323 + ? '_processChildContext is not available in React 16+. This likely ' +
324 + 'means you have multiple copies of React and are attempting to nest ' +
325 + 'a React 15 tree inside a React 16 tree using ' +
326 + "unstable_renderSubtreeIntoContainer, which isn't supported. Try to " +
327 + 'make sure you have only one copy of React (and ideally, switch to ' +
328 + 'ReactDOM.createPortal).'
329 + : "Cannot read property '_processChildContext' of undefined",
330 + );
331 });
341 -}
332 +});
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+30 -5
@@ -1097,11 +1097,7 @@ describe('DOMModernPluginEventSystem', () => {
1097 ReactTestUtils = require('react-dom/test-utils');
1098 });
1099
1100 - if (!__EXPERIMENTAL__) {
1101 - it("empty test so Jest doesn't complain", () => {});
1102 - return;
1103 - }
1104 -
1100 + // @gate experimental
1101 it('should create the same event listener map', () => {
1102 const listenerMaps = [];
1103
@@ -1119,6 +1115,7 @@ describe('DOMModernPluginEventSystem', () => {
1115 expect(listenerMaps[0]).toEqual(listenerMaps[1]);
1116 });
1117
1118 + // @gate experimental
1119 it('can render correctly with the ReactDOMServer', () => {
1120 const clickEvent = jest.fn();
1121
@@ -1136,6 +1133,7 @@ describe('DOMModernPluginEventSystem', () => {
1133 expect(output).toBe(`<div data-reactroot="">Hello world</div>`);
1134 });
1135
1136 + // @gate experimental
1137 it('can render correctly with the ReactDOMServer hydration', () => {
1138 const clickEvent = jest.fn();
1139 const spanRef = React.createRef();
@@ -1164,6 +1162,7 @@ describe('DOMModernPluginEventSystem', () => {
1162 expect(clickEvent).toHaveBeenCalledTimes(1);
1163 });
1164
1165 + // @gate experimental
1166 it('should correctly work for a basic "click" listener', () => {
1167 let log = [];
1168 const clickEvent = jest.fn(event => {
@@ -1272,6 +1271,7 @@ describe('DOMModernPluginEventSystem', () => {
1271 expect(clickEvent2).toBeCalledTimes(1);
1272 });
1273
1274 + // @gate experimental
1275 it('should correctly work for setting and clearing a basic "click" listener', () => {
1276 const clickEvent = jest.fn();
1277 const divRef = React.createRef();
@@ -1315,6 +1315,7 @@ describe('DOMModernPluginEventSystem', () => {
1315 expect(clickEvent).toBeCalledTimes(0);
1316 });
1317
1318 + // @gate experimental
1319 it('should handle the target being a text node', () => {
1320 const clickEvent = jest.fn();
1321 const buttonRef = React.createRef();
@@ -1337,6 +1338,7 @@ describe('DOMModernPluginEventSystem', () => {
1338 expect(clickEvent).toBeCalledTimes(1);
1339 });
1340
1341 + // @gate experimental
1342 it('handle propagation of click events', () => {
1343 const buttonRef = React.createRef();
1344 const divRef = React.createRef();
@@ -1390,6 +1392,7 @@ describe('DOMModernPluginEventSystem', () => {
1392 expect(log[3]).toEqual(['bubble', buttonElement]);
1393 });
1394
1395 + // @gate experimental
1396 it('handle propagation of click events mixed with onClick events', () => {
1397 const buttonRef = React.createRef();
1398 const divRef = React.createRef();
@@ -1442,6 +1445,7 @@ describe('DOMModernPluginEventSystem', () => {
1445 expect(log[5]).toEqual(['bubble', buttonElement]);
1446 });
1447
1448 + // @gate experimental
1449 it('should correctly work for a basic "click" listener on the outer target', () => {
1450 const log = [];
1451 const clickEvent = jest.fn(event => {
@@ -1507,6 +1511,7 @@ describe('DOMModernPluginEventSystem', () => {
1511 expect(clickEvent).toBeCalledTimes(2);
1512 });
1513
1514 + // @gate experimental
1515 it('should correctly handle many nested target listeners', () => {
1516 const buttonRef = React.createRef();
1517 const targetListener1 = jest.fn();
@@ -1572,6 +1577,7 @@ describe('DOMModernPluginEventSystem', () => {
1577 expect(targetListener4).toHaveBeenCalledTimes(2);
1578 });
1579
1580 + // @gate experimental
1581 it('should correctly handle stopPropagation corrrectly for target events', () => {
1582 const buttonRef = React.createRef();
1583 const divRef = React.createRef();
@@ -1605,6 +1611,7 @@ describe('DOMModernPluginEventSystem', () => {
1611 expect(clickEvent).toHaveBeenCalledTimes(0);
1612 });
1613
1614 + // @gate experimental
1615 it('should correctly handle stopPropagation corrrectly for many target events', () => {
1616 const buttonRef = React.createRef();
1617 const targetListerner1 = jest.fn(e => e.stopPropagation());
@@ -1639,6 +1646,7 @@ describe('DOMModernPluginEventSystem', () => {
1646 expect(targetListerner4).toHaveBeenCalledTimes(1);
1647 });
1648
1649 + // @gate experimental
1650 it('should correctly handle stopPropagation for mixed capture/bubbling target listeners', () => {
1651 const buttonRef = React.createRef();
1652 const targetListerner1 = jest.fn(e => e.stopPropagation());
@@ -1725,6 +1733,7 @@ describe('DOMModernPluginEventSystem', () => {
1733 expect(log).toEqual([{counter: 1}]);
1734 });
1735
1736 + // @gate experimental
1737 it('should correctly work for a basic "click" listener that upgrades', () => {
1738 const clickEvent = jest.fn();
1739 const buttonRef = React.createRef();
@@ -1776,6 +1785,7 @@ describe('DOMModernPluginEventSystem', () => {
1785 expect(clickEvent).toHaveBeenCalledTimes(1);
1786 });
1787
1788 + // @gate experimental
1789 it('should correctly work for a basic "click" listener that upgrades #2', () => {
1790 const clickEvent = jest.fn();
1791 const buttonRef = React.createRef();
@@ -1827,6 +1837,7 @@ describe('DOMModernPluginEventSystem', () => {
1837 expect(clickEvent).toHaveBeenCalledTimes(1);
1838 });
1839
1840 + // @gate experimental
1841 it('should correctly work for a basic "click" window listener', () => {
1842 const log = [];
1843 const clickEvent = jest.fn(event => {
@@ -1878,6 +1889,7 @@ describe('DOMModernPluginEventSystem', () => {
1889 expect(clickEvent).toBeCalledTimes(2);
1890 });
1891
1892 + // @gate experimental
1893 it('handle propagation of click events on the window', () => {
1894 const buttonRef = React.createRef();
1895 const divRef = React.createRef();
@@ -1937,6 +1949,7 @@ describe('DOMModernPluginEventSystem', () => {
1949 expect(log[5]).toEqual(['bubble', window]);
1950 });
1951
1952 + // @gate experimental
1953 it('should correctly handle stopPropagation for mixed listeners', () => {
1954 const buttonRef = React.createRef();
1955 const rootListerner1 = jest.fn(e => e.stopPropagation());
@@ -1975,6 +1988,7 @@ describe('DOMModernPluginEventSystem', () => {
1988 expect(rootListerner2).toHaveBeenCalledTimes(0);
1989 });
1990
1991 + // @gate experimental
1992 it('should correctly handle stopPropagation for delegated listeners', () => {
1993 const buttonRef = React.createRef();
1994 const rootListerner1 = jest.fn(e => e.stopPropagation());
@@ -2014,6 +2028,7 @@ describe('DOMModernPluginEventSystem', () => {
2028 expect(rootListerner4).toHaveBeenCalledTimes(0);
2029 });
2030
2031 + // @gate experimental
2032 it('handle propagation of click events on the window and document', () => {
2033 const buttonRef = React.createRef();
2034 const divRef = React.createRef();
@@ -2079,6 +2094,7 @@ describe('DOMModernPluginEventSystem', () => {
2094 expect(log[7]).toEqual(['bubble', window]);
2095 });
2096
2097 + // @gate experimental
2098 it('handles propagation of custom user events', () => {
2099 const buttonRef = React.createRef();
2100 const divRef = React.createRef();
@@ -2153,6 +2169,7 @@ describe('DOMModernPluginEventSystem', () => {
2169 expect(log[5]).toEqual(['bubble', buttonElement]);
2170 });
2171
2172 + // @gate experimental
2173 it('beforeblur and afterblur are called after a focused element is unmounted', () => {
2174 const log = [];
2175 // We have to persist here because we want to read relatedTarget later.
@@ -2202,6 +2219,7 @@ describe('DOMModernPluginEventSystem', () => {
2219 expect(log).toEqual(['beforeblur', 'afterblur']);
2220 });
2221
2222 + // @gate experimental
2223 it('beforeblur and afterblur are called after a nested focused element is unmounted', () => {
2224 const log = [];
2225 // We have to persist here because we want to read relatedTarget later.
@@ -2346,6 +2364,7 @@ describe('DOMModernPluginEventSystem', () => {
2364 ReactDOMServer = require('react-dom/server');
2365 });
2366
2367 + // @gate experimental
2368 it('handle propagation of click events on a scope', () => {
2369 const buttonRef = React.createRef();
2370 const log = [];
@@ -2390,6 +2409,7 @@ describe('DOMModernPluginEventSystem', () => {
2409 ]);
2410 });
2411
2412 + // @gate experimental
2413 it('handle mixed propagation of click events on a scope', () => {
2414 const buttonRef = React.createRef();
2415 const divRef = React.createRef();
@@ -2464,6 +2484,7 @@ describe('DOMModernPluginEventSystem', () => {
2484 ]);
2485 });
2486
2487 + // @gate experimental
2488 it('should not handle the target being a dangling text node within a scope', () => {
2489 const clickEvent = jest.fn();
2490 const buttonRef = React.createRef();
@@ -2495,6 +2516,7 @@ describe('DOMModernPluginEventSystem', () => {
2516 expect(clickEvent).toBeCalledTimes(0);
2517 });
2518
2519 + // @gate experimental
2520 it('handle stopPropagation (inner) correctly between scopes', () => {
2521 const buttonRef = React.createRef();
2522 const outerOnClick = jest.fn();
@@ -2531,6 +2553,7 @@ describe('DOMModernPluginEventSystem', () => {
2553 expect(outerOnClick).toHaveBeenCalledTimes(0);
2554 });
2555
2556 + // @gate experimental
2557 it('handle stopPropagation (outer) correctly between scopes', () => {
2558 const buttonRef = React.createRef();
2559 const outerOnClick = jest.fn(e => e.stopPropagation());
@@ -2567,6 +2590,8 @@ describe('DOMModernPluginEventSystem', () => {
2590 expect(outerOnClick).toHaveBeenCalledTimes(1);
2591 });
2592
2593 + // @gate experimental
2594 +
2595 it('handle stopPropagation (inner and outer) correctly between scopes', () => {
2596 const buttonRef = React.createRef();
2597 const onClick = jest.fn(e => e.stopPropagation());
packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js
+20 -5
@@ -66,11 +66,6 @@ function dispatchClickEvent(element) {
66 describe('DOMEventResponderSystem', () => {
67 let container;
68
69 - if (!__EXPERIMENTAL__) {
70 - it("empty test so Jest doesn't complain", () => {});
71 - return;
72 - }
73 -
69 beforeEach(() => {
70 jest.resetModules();
71 ReactFeatureFlags = require('shared/ReactFeatureFlags');
@@ -89,6 +84,7 @@ describe('DOMEventResponderSystem', () => {
84 container = null;
85 });
86
87 + // @gate experimental
88 it('can mount and render correctly with the ReactTestRenderer', () => {
89 jest.resetModules();
90 ReactFeatureFlags = require('shared/ReactFeatureFlags');
@@ -106,6 +102,7 @@ describe('DOMEventResponderSystem', () => {
102 expect(renderer).toMatchRenderedOutput(<div>Hello world</div>);
103 });
104
105 + // @gate experimental
106 it('can render correctly with the ReactDOMServer', () => {
107 const TestResponder = createEventResponder({});
108
@@ -118,6 +115,7 @@ describe('DOMEventResponderSystem', () => {
115 expect(output).toBe(`<div data-reactroot="">Hello world</div>`);
116 });
117
118 + // @gate experimental
119 it('can render correctly with the ReactDOMServer hydration', () => {
120 const onEvent = jest.fn();
121 const TestResponder = createEventResponder({
@@ -147,6 +145,7 @@ describe('DOMEventResponderSystem', () => {
145 expect(onEvent).toHaveBeenCalledTimes(1);
146 });
147
148 + // @gate experimental
149 it('the event responders should fire on click event', () => {
150 let eventResponderFiredCount = 0;
151 const eventLog = [];
@@ -202,6 +201,7 @@ describe('DOMEventResponderSystem', () => {
201 expect(eventResponderFiredCount).toBe(2);
202 });
203
204 + // @gate experimental
205 it('the event responders should fire on click event (passive events forced)', () => {
206 // JSDOM does not support passive events, so this manually overrides the value to be true
207 const checkPassiveEvents = require('react-dom/src/events/checkPassiveEvents');
@@ -246,6 +246,7 @@ describe('DOMEventResponderSystem', () => {
246 ]);
247 });
248
249 + // @gate experimental
250 it('nested event responders should not fire multiple times', () => {
251 let eventResponderFiredCount = 0;
252 let eventLog = [];
@@ -327,6 +328,7 @@ describe('DOMEventResponderSystem', () => {
328 ]);
329 });
330
331 + // @gate experimental
332 it('nested event responders should fire in the correct order', () => {
333 let eventLog = [];
334 const buttonRef = React.createRef();
@@ -390,6 +392,7 @@ describe('DOMEventResponderSystem', () => {
392 expect(eventLog).toEqual(['B [bubble]', 'A [bubble]']);
393 });
394
395 + // @gate experimental
396 it('nested event responders should fire in the correct order #2', () => {
397 const eventLog = [];
398 const buttonRef = React.createRef();
@@ -426,6 +429,7 @@ describe('DOMEventResponderSystem', () => {
429 expect(eventLog).toEqual(['B [bubble]']);
430 });
431
432 + // @gate experimental
433 it('custom event dispatching for click -> magicClick works', () => {
434 const eventLog = [];
435 const buttonRef = React.createRef();
@@ -472,6 +476,7 @@ describe('DOMEventResponderSystem', () => {
476 expect(eventLog).toEqual(['magic event fired', 'magicclick', 'bubble']);
477 });
478
479 + // @gate experimental
480 it('the event responder onMount() function should fire', () => {
481 let onMountFired = 0;
482
@@ -505,6 +510,7 @@ describe('DOMEventResponderSystem', () => {
510 expect(onMountFired).toEqual(2);
511 });
512
513 + // @gate experimental
514 it('the event responder onUnmount() function should fire', () => {
515 let onUnmountFired = 0;
516
@@ -551,6 +557,7 @@ describe('DOMEventResponderSystem', () => {
557 expect(onUnmountFired).toEqual(4);
558 });
559
560 + // @gate experimental
561 it('the event responder onUnmount() function should fire using scopes', () => {
562 let onUnmountFired = 0;
563
@@ -598,6 +605,7 @@ describe('DOMEventResponderSystem', () => {
605 expect(onUnmountFired).toEqual(4);
606 });
607
608 + // @gate experimental
609 it('the event responder onUnmount() function should fire with state', () => {
610 let counter = 0;
611
@@ -621,6 +629,7 @@ describe('DOMEventResponderSystem', () => {
629 expect(counter).toEqual(5);
630 });
631
632 + // @gate experimental
633 it('the event responder target listeners should correctly fire for only their events', () => {
634 let clickEventComponent1Fired = 0;
635 let clickEventComponent2Fired = 0;
@@ -682,6 +691,7 @@ describe('DOMEventResponderSystem', () => {
691 ]);
692 });
693
694 + // @gate experimental
695 it('the event responder system should warn on accessing invalid properties', () => {
696 const TestResponder = createEventResponder({
697 targetEventTypes: ['click'],
@@ -747,6 +757,7 @@ describe('DOMEventResponderSystem', () => {
757 expect(container.innerHTML).toBe('<button>Click me!</button>');
758 });
759
760 + // @gate experimental
761 it('should work with event responder hooks', () => {
762 const buttonRef = React.createRef();
763 const eventLogs = [];
@@ -839,6 +850,7 @@ describe('DOMEventResponderSystem', () => {
850 expect(log).toEqual([{counter: 1}]);
851 });
852
853 + // @gate experimental
854 it('should correctly pass through event properties', () => {
855 const timeStamps = [];
856 const ref = React.createRef();
@@ -903,6 +915,7 @@ describe('DOMEventResponderSystem', () => {
915 ]);
916 });
917
918 + // @gate experimental
919 it('should not propagate target events through portals by default', () => {
920 const buttonRef = React.createRef();
921 const onEvent = jest.fn();
@@ -926,6 +939,7 @@ describe('DOMEventResponderSystem', () => {
939 expect(onEvent).not.toBeCalled();
940 });
941
942 + // @gate experimental
943 it('should propagate target events through portals when enabled', () => {
944 const buttonRef = React.createRef();
945 const onEvent = jest.fn();
@@ -950,6 +964,7 @@ describe('DOMEventResponderSystem', () => {
964 expect(onEvent).toBeCalled();
965 });
966
967 + // @gate experimental
968 it('event upgrading should work correctly', () => {
969 let eventResponderFiredCount = 0;
970 const buttonRef = React.createRef();
packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js
+13 -7
@@ -28,8 +28,13 @@ function initializeModules(hasPointerEvents) {
28 ReactFeatureFlags.enableDeprecatedFlareAPI = true;
29 React = require('react');
30 ReactDOM = require('react-dom');
31 - useContextMenu = require('react-interactions/events/context-menu')
32 - .useContextMenu;
31 +
32 + // TODO: This import throws outside of experimental mode. Figure out better
33 + // strategy for gated imports.
34 + if (__EXPERIMENTAL__) {
35 + useContextMenu = require('react-interactions/events/context-menu')
36 + .useContextMenu;
37 + }
38 }
39
40 const forcePointerEvents = true;
@@ -38,11 +43,6 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
43 describe.each(table)('ContextMenu responder', hasPointerEvents => {
44 let container;
45
41 - if (!__EXPERIMENTAL__) {
42 - it("empty test so Jest doesn't complain", () => {});
43 - return;
44 - }
45 -
46 beforeEach(() => {
47 initializeModules(hasPointerEvents);
48 container = document.createElement('div');
@@ -56,6 +56,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
56 });
57
58 describe('all platforms', () => {
59 + // @gate experimental
60 it('mouse right-click', () => {
61 const onContextMenu = jest.fn();
62 const preventDefault = jest.fn();
@@ -79,6 +80,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
80 );
81 });
82
83 + // @gate experimental
84 it('touch long-press', () => {
85 const onContextMenu = jest.fn();
86 const preventDefault = jest.fn();
@@ -102,6 +104,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
104 );
105 });
106
107 + // @gate experimental
108 it('"disabled" is true', () => {
109 const onContextMenu = jest.fn();
110 const ref = React.createRef();
@@ -119,6 +122,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
122 expect(onContextMenu).toHaveBeenCalledTimes(0);
123 });
124
125 + // @gate experimental
126 it('"preventDefault" is false', () => {
127 const preventDefault = jest.fn();
128 const onContextMenu = jest.fn();
@@ -149,6 +153,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
153 platform.clear();
154 });
155
156 + // @gate experimental
157 it('mouse modified left-click', () => {
158 const onContextMenu = jest.fn();
159 const ref = React.createRef();
@@ -181,6 +186,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
186 platform.clear();
187 });
188
189 + // @gate experimental
190 it('mouse modified left-click', () => {
191 const onContextMenu = jest.fn();
192 const ref = React.createRef();
packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js
+46 -17
@@ -28,8 +28,13 @@ function initializeModules(hasPointerEvents) {
28 ReactFeatureFlags.enableDeprecatedFlareAPI = true;
29 React = require('react');
30 ReactDOM = require('react-dom');
31 - FocusResponder = require('react-interactions/events/focus').FocusResponder;
32 - useFocus = require('react-interactions/events/focus').useFocus;
31 +
32 + // TODO: This import throws outside of experimental mode. Figure out better
33 + // strategy for gated imports.
34 + if (__EXPERIMENTAL__) {
35 + FocusResponder = require('react-interactions/events/focus').FocusResponder;
36 + useFocus = require('react-interactions/events/focus').useFocus;
37 + }
38 }
39
40 const forcePointerEvents = true;
@@ -38,11 +43,6 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
43 describe.each(table)('Focus responder', hasPointerEvents => {
44 let container;
45
41 - if (!__EXPERIMENTAL__) {
42 - it("empty test so Jest doesn't complain", () => {});
43 - return;
44 - }
45 -
46 beforeEach(() => {
47 initializeModules(hasPointerEvents);
48 container = document.createElement('div');
@@ -58,7 +58,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
58 describe('disabled', () => {
59 let onBlur, onFocus, ref;
60
61 - beforeEach(() => {
61 + const componentInit = () => {
62 onBlur = jest.fn();
63 onFocus = jest.fn();
64 ref = React.createRef();
@@ -71,9 +71,11 @@ describe.each(table)('Focus responder', hasPointerEvents => {
71 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
72 };
73 ReactDOM.render(<Component />, container);
74 - });
74 + };
75
76 + // @gate experimental
77 it('does not call callbacks', () => {
78 + componentInit();
79 const target = createEventTarget(ref.current);
80 target.focus();
81 target.blur();
@@ -85,7 +87,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
87 describe('onBlur', () => {
88 let onBlur, ref;
89
88 - beforeEach(() => {
90 + const componentInit = () => {
91 onBlur = jest.fn();
92 ref = React.createRef();
93 const Component = () => {
@@ -95,9 +97,11 @@ describe.each(table)('Focus responder', hasPointerEvents => {
97 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
98 };
99 ReactDOM.render(<Component />, container);
98 - });
100 + };
101
102 + // @gate experimental
103 it('is called after "blur" event', () => {
104 + componentInit();
105 const target = createEventTarget(ref.current);
106 target.focus();
107 target.blur();
@@ -125,21 +129,25 @@ describe.each(table)('Focus responder', hasPointerEvents => {
129 ReactDOM.render(<Component />, container);
130 };
131
128 - beforeEach(componentInit);
129 -
132 + // @gate experimental
133 it('is called after "focus" event', () => {
134 + componentInit();
135 const target = createEventTarget(ref.current);
136 target.focus();
137 expect(onFocus).toHaveBeenCalledTimes(1);
138 });
139
140 + // @gate experimental
141 it('is not called if descendants of target receive focus', () => {
142 + componentInit();
143 const target = createEventTarget(innerRef.current);
144 target.focus();
145 expect(onFocus).not.toBeCalled();
146 });
147
148 + // @gate experimental
149 it('is called with the correct pointerType: mouse', () => {
150 + componentInit();
151 const target = createEventTarget(ref.current);
152 target.pointerdown();
153 target.pointerup();
@@ -149,7 +157,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
157 );
158 });
159
160 + // @gate experimental
161 it('is called with the correct pointerType: touch', () => {
162 + componentInit();
163 const target = createEventTarget(ref.current);
164 const pointerType = 'touch';
165 target.pointerdown({pointerType});
@@ -161,7 +171,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
171 });
172
173 if (hasPointerEvents) {
174 + // @gate experimental
175 it('is called with the correct pointerType: pen', () => {
176 + componentInit();
177 const target = createEventTarget(ref.current);
178 const pointerType = 'pen';
179 target.pointerdown({pointerType});
@@ -173,7 +185,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
185 });
186 }
187
188 + // @gate experimental
189 it('is called with the correct pointerType using a keyboard', () => {
190 + componentInit();
191 const target = createEventTarget(ref.current);
192 target.keydown({key: 'LeftArrow'});
193 target.focus();
@@ -183,6 +197,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
197 );
198 });
199
200 + // @gate experimental
201 it('is called with the correct pointerType using Tab+altKey on Mac', () => {
202 platform.set('mac');
203 jest.resetModules();
@@ -207,7 +222,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
222 describe('onFocusChange', () => {
223 let onFocusChange, ref, innerRef;
224
210 - beforeEach(() => {
225 + const componentInit = () => {
226 onFocusChange = jest.fn();
227 ref = React.createRef();
228 innerRef = React.createRef();
@@ -222,9 +237,11 @@ describe.each(table)('Focus responder', hasPointerEvents => {
237 );
238 };
239 ReactDOM.render(<Component />, container);
225 - });
240 + };
241
242 + // @gate experimental
243 it('is called after "blur" and "focus" events', () => {
244 + componentInit();
245 const target = createEventTarget(ref.current);
246 target.focus();
247 expect(onFocusChange).toHaveBeenCalledTimes(1);
@@ -234,7 +251,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
251 expect(onFocusChange).toHaveBeenCalledWith(false);
252 });
253
254 + // @gate experimental
255 it('is not called after "blur" and "focus" events on descendants', () => {
256 + componentInit();
257 const target = createEventTarget(innerRef.current);
258 target.focus();
259 expect(onFocusChange).toHaveBeenCalledTimes(0);
@@ -246,7 +265,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
265 describe('onFocusVisibleChange', () => {
266 let onFocusVisibleChange, ref, innerRef;
267
249 - beforeEach(() => {
268 + const componentInit = () => {
269 onFocusVisibleChange = jest.fn();
270 ref = React.createRef();
271 innerRef = React.createRef();
@@ -261,9 +280,11 @@ describe.each(table)('Focus responder', hasPointerEvents => {
280 );
281 };
282 ReactDOM.render(<Component />, container);
264 - });
283 + };
284
285 + // @gate experimental
286 it('is called after "focus" and "blur" if keyboard navigation is active', () => {
287 + componentInit();
288 const target = createEventTarget(ref.current);
289 const containerTarget = createEventTarget(container);
290 // use keyboard first
@@ -276,7 +297,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
297 expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
298 });
299
300 + // @gate experimental
301 it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
302 + componentInit();
303 const target = createEventTarget(ref.current);
304 const containerTarget = createEventTarget(container);
305 // use keyboard first
@@ -293,7 +316,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
316 expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
317 });
318
319 + // @gate experimental
320 it('is not called after "focus" and "blur" events without keyboard', () => {
321 + componentInit();
322 const target = createEventTarget(ref.current);
323 const containerTarget = createEventTarget(container);
324 target.pointerdown();
@@ -303,7 +328,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
328 expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
329 });
330
331 + // @gate experimental
332 it('is not called after "blur" and "focus" events on descendants', () => {
333 + componentInit();
334 const innerTarget = createEventTarget(innerRef.current);
335 const containerTarget = createEventTarget(container);
336 containerTarget.keydown({key: 'Tab'});
@@ -315,6 +342,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
342 });
343
344 describe('nested Focus components', () => {
345 + // @gate experimental
346 it('propagates events in the correct order', () => {
347 const events = [];
348 const innerRef = React.createRef();
@@ -367,6 +395,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
395 });
396 });
397
398 + // @gate experimental
399 it('expect displayName to show up for event component', () => {
400 expect(FocusResponder.displayName).toBe('Focus');
401 });
packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+35 -14
@@ -25,10 +25,15 @@ const initializeModules = hasPointerEvents => {
25 ReactFeatureFlags.enableDeprecatedFlareAPI = true;
26 React = require('react');
27 ReactDOM = require('react-dom');
28 - FocusWithinResponder = require('react-interactions/events/focus')
29 - .FocusWithinResponder;
30 - useFocusWithin = require('react-interactions/events/focus').useFocusWithin;
28 Scheduler = require('scheduler');
29 +
30 + // TODO: This import throws outside of experimental mode. Figure out better
31 + // strategy for gated imports.
32 + if (__EXPERIMENTAL__) {
33 + FocusWithinResponder = require('react-interactions/events/focus')
34 + .FocusWithinResponder;
35 + useFocusWithin = require('react-interactions/events/focus').useFocusWithin;
36 + }
37 };
38
39 const forcePointerEvents = true;
@@ -37,11 +42,6 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
42 describe.each(table)('FocusWithin responder', hasPointerEvents => {
43 let container;
44
40 - if (!__EXPERIMENTAL__) {
41 - it("empty test so Jest doesn't complain", () => {});
42 - return;
43 - }
44 -
45 beforeEach(() => {
46 initializeModules();
47 container = document.createElement('div');
@@ -57,7 +57,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
57 describe('disabled', () => {
58 let onFocusWithinChange, onFocusWithinVisibleChange, ref;
59
60 - beforeEach(() => {
60 + const componentInit = () => {
61 onFocusWithinChange = jest.fn();
62 onFocusWithinVisibleChange = jest.fn();
63 ref = React.createRef();
@@ -70,9 +70,11 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
70 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
71 };
72 ReactDOM.render(<Component />, container);
73 - });
73 + };
74
75 + // @gate experimental
76 it('prevents custom events being dispatched', () => {
77 + componentInit();
78 const target = createEventTarget(ref.current);
79 target.focus();
80 target.blur();
@@ -96,15 +98,17 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
98 );
99 };
100
99 - beforeEach(() => {
101 + const componentInit = () => {
102 onFocusWithinChange = jest.fn();
103 ref = React.createRef();
104 innerRef = React.createRef();
105 innerRef2 = React.createRef();
106 ReactDOM.render(<Component show={true} />, container);
105 - });
107 + };
108
109 + // @gate experimental
110 it('is called after "blur" and "focus" events on focus target', () => {
111 + componentInit();
112 const target = createEventTarget(ref.current);
113 target.focus();
114 expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
@@ -114,7 +118,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
118 expect(onFocusWithinChange).toHaveBeenCalledWith(false);
119 });
120
121 + // @gate experimental
122 it('is called after "blur" and "focus" events on descendants', () => {
123 + componentInit();
124 const target = createEventTarget(innerRef.current);
125 target.focus();
126 expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
@@ -124,7 +130,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
130 expect(onFocusWithinChange).toHaveBeenCalledWith(false);
131 });
132
133 + // @gate experimental
134 it('is only called once when focus moves within and outside the subtree', () => {
135 + componentInit();
136 const node = ref.current;
137 const innerNode1 = innerRef.current;
138 const innerNode2 = innerRef.current;
@@ -165,15 +173,17 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
173 );
174 };
175
168 - beforeEach(() => {
176 + const componentInit = () => {
177 onFocusWithinVisibleChange = jest.fn();
178 ref = React.createRef();
179 innerRef = React.createRef();
180 innerRef2 = React.createRef();
181 ReactDOM.render(<Component show={true} />, container);
174 - });
182 + };
183
184 + // @gate experimental
185 it('is called after "focus" and "blur" on focus target if keyboard was used', () => {
186 + componentInit();
187 const target = createEventTarget(ref.current);
188 const containerTarget = createEventTarget(container);
189 // use keyboard first
@@ -186,7 +196,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
196 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
197 });
198
199 + // @gate experimental
200 it('is called after "focus" and "blur" on descendants if keyboard was used', () => {
201 + componentInit();
202 const innerTarget = createEventTarget(innerRef.current);
203 const containerTarget = createEventTarget(container);
204 // use keyboard first
@@ -199,7 +211,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
211 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
212 });
213
214 + // @gate experimental
215 it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
216 + componentInit();
217 const node = ref.current;
218 const innerNode1 = innerRef.current;
219 const innerNode2 = innerRef2.current;
@@ -235,7 +249,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
249 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
250 });
251
252 + // @gate experimental
253 it('is not called after "focus" and "blur" events without keyboard', () => {
254 + componentInit();
255 const innerTarget = createEventTarget(innerRef.current);
256 innerTarget.pointerdown();
257 innerTarget.pointerup();
@@ -243,7 +259,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
259 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0);
260 });
261
262 + // @gate experimental
263 it('is only called once when focus moves within and outside the subtree', () => {
264 + componentInit();
265 const node = ref.current;
266 const innerNode1 = innerRef.current;
267 const innerNode2 = innerRef2.current;
@@ -280,6 +298,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
298 innerRef2 = React.createRef();
299 });
300
301 + // @gate experimental
302 it('is called after a focused element is unmounted', () => {
303 const Component = ({show}) => {
304 const listener = useFocusWithin({
@@ -310,6 +329,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
329 );
330 });
331
332 + // @gate experimental
333 it('is called after a nested focused element is unmounted', () => {
334 const Component = ({show}) => {
335 const listener = useFocusWithin({
@@ -405,6 +425,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
425 });
426 });
427
428 + // @gate experimental
429 it('expect displayName to show up for event component', () => {
430 expect(FocusWithinResponder.displayName).toBe('FocusWithin');
431 });
packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js
+39 -15
@@ -24,8 +24,13 @@ function initializeModules(hasPointerEvents) {
24 ReactFeatureFlags.enableDeprecatedFlareAPI = true;
25 React = require('react');
26 ReactDOM = require('react-dom');
27 - HoverResponder = require('react-interactions/events/hover').HoverResponder;
28 - useHover = require('react-interactions/events/hover').useHover;
27 +
28 + // TODO: This import throws outside of experimental mode. Figure out better
29 + // strategy for gated imports.
30 + if (__EXPERIMENTAL__) {
31 + HoverResponder = require('react-interactions/events/hover').HoverResponder;
32 + useHover = require('react-interactions/events/hover').useHover;
33 + }
34 }
35
36 const forcePointerEvents = true;
@@ -34,11 +39,6 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
39 describe.each(table)('Hover responder', hasPointerEvents => {
40 let container;
41
37 - if (!__EXPERIMENTAL__) {
38 - it("empty test so Jest doesn't complain", () => {});
39 - return;
40 - }
41 -
42 beforeEach(() => {
43 initializeModules(hasPointerEvents);
44 container = document.createElement('div');
@@ -54,7 +54,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
54 describe('disabled', () => {
55 let onHoverChange, onHoverStart, onHoverMove, onHoverEnd, ref;
56
57 - beforeEach(() => {
57 + const componentInit = () => {
58 onHoverChange = jest.fn();
59 onHoverStart = jest.fn();
60 onHoverMove = jest.fn();
@@ -71,9 +71,11 @@ describe.each(table)('Hover responder', hasPointerEvents => {
71 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
72 };
73 ReactDOM.render(<Component />, container);
74 - });
74 + };
75
76 + // @gate experimental
77 it('does not call callbacks', () => {
78 + componentInit();
79 const target = createEventTarget(ref.current);
80 target.pointerenter();
81 target.pointerexit();
@@ -87,7 +89,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
89 describe('onHoverStart', () => {
90 let onHoverStart, ref;
91
90 - beforeEach(() => {
92 + const componentInit = () => {
93 onHoverStart = jest.fn();
94 ref = React.createRef();
95 const Component = () => {
@@ -97,22 +99,28 @@ describe.each(table)('Hover responder', hasPointerEvents => {
99 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
100 };
101 ReactDOM.render(<Component />, container);
100 - });
102 + };
103
104 + // @gate experimental
105 it('is called for mouse pointers', () => {
106 + componentInit();
107 const target = createEventTarget(ref.current);
108 target.pointerenter();
109 expect(onHoverStart).toHaveBeenCalledTimes(1);
110 });
111
112 + // @gate experimental
113 it('is not called for touch pointers', () => {
114 + componentInit();
115 const target = createEventTarget(ref.current);
116 target.pointerdown({pointerType: 'touch'});
117 target.pointerup({pointerType: 'touch'});
118 expect(onHoverStart).not.toBeCalled();
119 });
120
121 + // @gate experimental
122 it('is called if a mouse pointer is used after a touch pointer', () => {
123 + componentInit();
124 const target = createEventTarget(ref.current);
125 target.pointerdown({pointerType: 'touch'});
126 target.pointerup({pointerType: 'touch'});
@@ -124,7 +132,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
132 describe('onHoverChange', () => {
133 let onHoverChange, ref;
134
127 - beforeEach(() => {
135 + const componentInit = () => {
136 onHoverChange = jest.fn();
137 ref = React.createRef();
138 const Component = () => {
@@ -134,9 +142,11 @@ describe.each(table)('Hover responder', hasPointerEvents => {
142 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
143 };
144 ReactDOM.render(<Component />, container);
137 - });
145 + };
146
147 + // @gate experimental
148 it('is called for mouse pointers', () => {
149 + componentInit();
150 const target = createEventTarget(ref.current);
151 target.pointerenter();
152 expect(onHoverChange).toHaveBeenCalledTimes(1);
@@ -146,7 +156,9 @@ describe.each(table)('Hover responder', hasPointerEvents => {
156 expect(onHoverChange).toHaveBeenCalledWith(false);
157 });
158
159 + // @gate experimental
160 it('is not called for touch pointers', () => {
161 + componentInit();
162 const target = createEventTarget(ref.current);
163 target.pointerdown({pointerType: 'touch'});
164 target.pointerup({pointerType: 'touch'});
@@ -157,7 +169,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
169 describe('onHoverEnd', () => {
170 let onHoverEnd, ref;
171
160 - beforeEach(() => {
172 + const componentInit = () => {
173 onHoverEnd = jest.fn();
174 ref = React.createRef();
175 const Component = () => {
@@ -167,9 +179,11 @@ describe.each(table)('Hover responder', hasPointerEvents => {
179 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
180 };
181 ReactDOM.render(<Component />, container);
170 - });
182 + };
183
184 + // @gate experimental
185 it('is called for mouse pointers', () => {
186 + componentInit();
187 const target = createEventTarget(ref.current);
188 target.pointerenter();
189 target.pointerexit();
@@ -177,7 +191,9 @@ describe.each(table)('Hover responder', hasPointerEvents => {
191 });
192
193 if (hasPointerEvents) {
194 + // @gate experimental
195 it('is called once for cancelled mouse pointers', () => {
196 + componentInit();
197 const target = createEventTarget(ref.current);
198 target.pointerenter();
199 target.pointercancel();
@@ -192,14 +208,18 @@ describe.each(table)('Hover responder', hasPointerEvents => {
208 });
209 }
210
211 + // @gate experimental
212 it('is not called for touch pointers', () => {
213 + componentInit();
214 const target = createEventTarget(ref.current);
215 target.pointerdown({pointerType: 'touch'});
216 target.pointerup({pointerType: 'touch'});
217 expect(onHoverEnd).not.toBeCalled();
218 });
219
220 + // @gate experimental
221 it('should correctly work with React Portals', () => {
222 + componentInit();
223 const portalNode = document.createElement('div');
224 const divRef = React.createRef();
225 const spanRef = React.createRef();
@@ -227,6 +247,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
247 });
248
249 describe('onHoverMove', () => {
250 + // @gate experimental
251 it('is called after the active pointer moves"', () => {
252 const onHoverMove = jest.fn();
253 const ref = React.createRef();
@@ -250,6 +271,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
271 });
272
273 describe('nested Hover components', () => {
274 + // @gate experimental
275 it('not propagate by default', () => {
276 const events = [];
277 const innerRef = React.createRef();
@@ -310,10 +332,12 @@ describe.each(table)('Hover responder', hasPointerEvents => {
332 });
333 });
334
335 + // @gate experimental
336 it('expect displayName to show up for event component', () => {
337 expect(HoverResponder.displayName).toBe('Hover');
338 });
339
340 + // @gate experimental
341 it('should correctly pass through event properties', () => {
342 const timeStamps = [];
343 const ref = React.createRef();
packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js
+28 -9
@@ -38,18 +38,18 @@ const modulesInit = () => {
38 React = require('react');
39 ReactDOM = require('react-dom');
40 Scheduler = require('scheduler');
41 - InputResponder = require('react-interactions/events/input').InputResponder;
42 - useInput = require('react-interactions/events/input').useInput;
41 +
42 + // TODO: This import throws outside of experimental mode. Figure out better
43 + // strategy for gated imports.
44 + if (__EXPERIMENTAL__) {
45 + InputResponder = require('react-interactions/events/input').InputResponder;
46 + useInput = require('react-interactions/events/input').useInput;
47 + }
48 };
49
50 describe('Input event responder', () => {
51 let container;
52
48 - if (!__EXPERIMENTAL__) {
49 - it("empty test so Jest doesn't complain", () => {});
50 - return;
51 - }
52 -
53 beforeEach(() => {
54 jest.resetModules();
55 modulesInit();
@@ -66,7 +66,7 @@ describe('Input event responder', () => {
66 describe('disabled', () => {
67 let onChange, onValueChange, ref;
68
69 - beforeEach(() => {
69 + const componentInit = () => {
70 onChange = jest.fn();
71 onValueChange = jest.fn();
72 ref = React.createRef();
@@ -80,9 +80,11 @@ describe('Input event responder', () => {
80 return <input ref={ref} DEPRECATED_flareListeners={listener} />;
81 }
82 ReactDOM.render(<Component />, container);
83 - });
83 + };
84
85 + // @gate experimental
86 it('prevents custom events being dispatched', () => {
87 + componentInit();
88 ref.current.dispatchEvent(
89 new Event('change', {bubbles: true, cancelable: true}),
90 );
@@ -104,6 +106,7 @@ describe('Input event responder', () => {
106 // keep track of the "current" value and only fire events when it changes.
107 // See https://github.com/facebook/react/pull/5746.
108
109 + // @gate experimental
110 it('should consider initial text value to be current', () => {
111 let onChangeCalled = 0;
112 let onValueChangeCalled = 0;
@@ -151,6 +154,7 @@ describe('Input event responder', () => {
154 }
155 });
156
157 + // @gate experimental
158 it('should consider initial checkbox checked=true to be current', () => {
159 let onChangeCalled = 0;
160 let onValueChangeCalled = 0;
@@ -194,6 +198,7 @@ describe('Input event responder', () => {
198 expect(onValueChangeCalled).toBe(0);
199 });
200
201 + // @gate experimental
202 it('should consider initial checkbox checked=false to be current', () => {
203 let onChangeCalled = 0;
204 let onValueChangeCalled = 0;
@@ -236,6 +241,7 @@ describe('Input event responder', () => {
241 expect(onValueChangeCalled).toBe(0);
242 });
243
244 + // @gate experimental
245 it('should fire change for checkbox input', () => {
246 let onChangeCalled = 0;
247 let onValueChangeCalled = 0;
@@ -283,6 +289,7 @@ describe('Input event responder', () => {
289 expect(onValueChangeCalled).toBe(2);
290 });
291
292 + // @gate experimental
293 it('should not fire change setting the value programmatically', () => {
294 let onChangeCalled = 0;
295 let onValueChangeCalled = 0;
@@ -345,6 +352,7 @@ describe('Input event responder', () => {
352 expect(onValueChangeCalled).toBe(1);
353 });
354
355 + // @gate experimental
356 it('should not distinguish equal string and number values', () => {
357 let onChangeCalled = 0;
358 let onValueChangeCalled = 0;
@@ -390,6 +398,7 @@ describe('Input event responder', () => {
398 });
399
400 // See a similar input test above for a detailed description of why.
401 + // @gate experimental
402 it('should not fire change when setting checked programmatically', () => {
403 let onChangeCalled = 0;
404 let onValueChangeCalled = 0;
@@ -441,6 +450,7 @@ describe('Input event responder', () => {
450 expect(onValueChangeCalled).toBe(1);
451 });
452
453 + // @gate experimental
454 it('should only fire change for checked radio button once', () => {
455 let onChangeCalled = 0;
456 let onValueChangeCalled = 0;
@@ -477,6 +487,7 @@ describe('Input event responder', () => {
487 expect(onValueChangeCalled).toBe(1);
488 });
489
490 + // @gate experimental
491 it('should track radio button cousins in a group', () => {
492 let onChangeCalled1 = 0;
493 let onValueChangeCalled1 = 0;
@@ -566,6 +577,7 @@ describe('Input event responder', () => {
577 expect(onValueChangeCalled2).toBe(1);
578 });
579
580 + // @gate experimental
581 it('should deduplicate input value change events', () => {
582 let onChangeCalled = 0;
583 let onValueChangeCalled = 0;
@@ -672,6 +684,7 @@ describe('Input event responder', () => {
684 });
685 });
686
687 + // @gate experimental
688 it('should listen for both change and input events when supported', () => {
689 let onChangeCalled = 0;
690 let onValueChangeCalled = 0;
@@ -711,6 +724,7 @@ describe('Input event responder', () => {
724 expect(onValueChangeCalled).toBe(2);
725 });
726
727 + // @gate experimental
728 it('should only fire events when the value changes for range inputs', () => {
729 let onChangeCalled = 0;
730 let onValueChangeCalled = 0;
@@ -756,6 +770,7 @@ describe('Input event responder', () => {
770 expect(onValueChangeCalled).toBe(2);
771 });
772
773 + // @gate experimental || build === "production"
774 it('does not crash for nodes with custom value property', () => {
775 let originalCreateElement;
776 // https://github.com/facebook/react/issues/10196
@@ -793,6 +808,7 @@ describe('Input event responder', () => {
808 });
809
810 describe('concurrent mode', () => {
811 + // @gate experimental
812 // @gate experimental
813 it('text input', () => {
814 const root = ReactDOM.createRoot(container);
@@ -849,6 +865,7 @@ describe('Input event responder', () => {
865 expect(input.value).toBe('changed [!]');
866 });
867
868 + // @gate experimental
869 // @gate experimental
870 it('checkbox input', () => {
871 const root = ReactDOM.createRoot(container);
@@ -918,6 +935,7 @@ describe('Input event responder', () => {
935 expect(input.checked).toBe(false);
936 });
937
938 + // @gate experimental
939 // @gate experimental
940 it('textarea', () => {
941 const root = ReactDOM.createRoot(container);
@@ -976,6 +994,7 @@ describe('Input event responder', () => {
994 });
995 });
996
997 + // @gate experimental
998 it('expect displayName to show up for event component', () => {
999 expect(InputResponder.displayName).toBe('Input');
1000 });
packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js
+37 -14
@@ -22,17 +22,17 @@ function initializeModules(hasPointerEvents) {
22 ReactFeatureFlags.enableDeprecatedFlareAPI = true;
23 React = require('react');
24 ReactDOM = require('react-dom');
25 - useKeyboard = require('react-interactions/events/keyboard').useKeyboard;
25 +
26 + // TODO: This import throws outside of experimental mode. Figure out better
27 + // strategy for gated imports.
28 + if (__EXPERIMENTAL__) {
29 + useKeyboard = require('react-interactions/events/keyboard').useKeyboard;
30 + }
31 }
32
33 describe('Keyboard responder', () => {
34 let container;
35
31 - if (!__EXPERIMENTAL__) {
32 - it("empty test so Jest doesn't complain", () => {});
33 - return;
34 - }
35 -
36 beforeEach(() => {
37 initializeModules();
38 container = document.createElement('div');
@@ -82,6 +82,7 @@ describe('Keyboard responder', () => {
82 };
83 }
84
85 + // @gate experimental
86 test('propagates key event when a continuePropagation() is used', () => {
87 const {
88 onClickInner,
@@ -104,6 +105,7 @@ describe('Keyboard responder', () => {
105 expect(onClickOuter).toBeCalled();
106 });
107
108 + // @gate experimental
109 test('does not propagate key event by default', () => {
110 const {
111 onClickInner,
@@ -129,7 +131,7 @@ describe('Keyboard responder', () => {
131 describe('disabled', () => {
132 let onKeyDown, onKeyUp, ref;
133
132 - beforeEach(() => {
134 + const componentInit = () => {
135 onKeyDown = jest.fn();
136 onKeyUp = jest.fn();
137 ref = React.createRef();
@@ -138,9 +140,11 @@ describe('Keyboard responder', () => {
140 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
141 };
142 ReactDOM.render(<Component />, container);
141 - });
143 + };
144
145 + // @gate experimental
146 test('does not call callbacks', () => {
147 + componentInit();
148 const target = createEventTarget(ref.current);
149 target.keydown();
150 target.keyup();
@@ -152,7 +156,7 @@ describe('Keyboard responder', () => {
156 describe('onClick', () => {
157 let onClick, ref;
158
155 - beforeEach(() => {
159 + const componentInit = () => {
160 onClick = jest.fn(e => {
161 e.preventDefault();
162 });
@@ -162,10 +166,12 @@ describe('Keyboard responder', () => {
166 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
167 };
168 ReactDOM.render(<Component />, container);
165 - });
169 + };
170
171 // e.g, "Enter" on link
172 + // @gate experimental
173 test('click is between key events', () => {
174 + componentInit();
175 const target = createEventTarget(ref.current);
176 target.keydown({key: 'Enter'});
177 target.keyup({key: 'Enter'});
@@ -187,7 +193,9 @@ describe('Keyboard responder', () => {
193 });
194
195 // e.g., "Spacebar" on button
196 + // @gate experimental
197 test('click is after key events', () => {
198 + componentInit();
199 const target = createEventTarget(ref.current);
200 target.keydown({key: 'Enter'});
201 target.keyup({key: 'Enter'});
@@ -209,7 +217,9 @@ describe('Keyboard responder', () => {
217 });
218
219 // e.g, generated by a screen-reader
220 + // @gate experimental
221 test('click is orphan', () => {
222 + componentInit();
223 const target = createEventTarget(ref.current);
224 target.virtualclick();
225 expect(onClick).toHaveBeenCalledTimes(1);
@@ -232,7 +242,7 @@ describe('Keyboard responder', () => {
242 describe('onKeyDown', () => {
243 let onKeyDown, ref;
244
235 - beforeEach(() => {
245 + const componentInit = () => {
246 onKeyDown = jest.fn();
247 ref = React.createRef();
248 const Component = () => {
@@ -240,9 +250,11 @@ describe('Keyboard responder', () => {
250 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
251 };
252 ReactDOM.render(<Component />, container);
243 - });
253 + };
254
255 + // @gate experimental
256 test('key down', () => {
257 + componentInit();
258 const target = createEventTarget(ref.current);
259 target.keydown({key: 'Q'});
260 expect(onKeyDown).toHaveBeenCalledTimes(1);
@@ -263,7 +275,9 @@ describe('Keyboard responder', () => {
275 );
276 });
277
278 + // @gate experimental
279 test('modified key down', () => {
280 + componentInit();
281 const target = createEventTarget(ref.current);
282 target.keydown({
283 key: 'Q',
@@ -293,7 +307,7 @@ describe('Keyboard responder', () => {
307 describe('onKeyUp', () => {
308 let onKeyUp, ref;
309
296 - beforeEach(() => {
310 + const componentInit = () => {
311 onKeyUp = jest.fn();
312 ref = React.createRef();
313 const Component = () => {
@@ -301,9 +315,11 @@ describe('Keyboard responder', () => {
315 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
316 };
317 ReactDOM.render(<Component />, container);
304 - });
318 + };
319
320 + // @gate experimental
321 test('key up', () => {
322 + componentInit();
323 const target = createEventTarget(ref.current);
324 target.keydown({key: 'Q'});
325 target.keyup({key: 'Q'});
@@ -325,7 +341,9 @@ describe('Keyboard responder', () => {
341 );
342 });
343
344 + // @gate experimental
345 test('modified key up', () => {
346 + componentInit();
347 const target = createEventTarget(ref.current);
348 target.keydown({key: 'Q'});
349 target.keyup({
@@ -364,6 +382,7 @@ describe('Keyboard responder', () => {
382 return ref;
383 }
384
385 + // @gate experimental
386 test('does not prevent native click by default', () => {
387 const onClick = jest.fn();
388 const preventDefault = jest.fn();
@@ -381,6 +400,7 @@ describe('Keyboard responder', () => {
400 );
401 });
402
403 + // @gate experimental
404 test('prevents native behaviour with preventDefault', () => {
405 const onClick = jest.fn(e => e.preventDefault());
406 const preventDefault = jest.fn();
@@ -409,6 +429,7 @@ describe('Keyboard responder', () => {
429 return ref;
430 }
431
432 + // @gate experimental
433 test('key config matches', () => {
434 const onKeyDown = jest.fn(e => {
435 if (e.key === 'Tab') {
@@ -434,6 +455,7 @@ describe('Keyboard responder', () => {
455 );
456 });
457
458 + // @gate experimental
459 test('key config matches (modifier keys)', () => {
460 const onKeyDown = jest.fn(e => {
461 if (e.key === 'Tab' && e.shiftKey) {
@@ -457,6 +479,7 @@ describe('Keyboard responder', () => {
479 );
480 });
481
482 + // @gate experimental
483 test('key config does not match (modifier keys)', () => {
484 const onKeyDown = jest.fn(e => {
485 if (e.key === 'Tab' && e.shiftKey) {
packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js
+4 -10
@@ -19,11 +19,6 @@ let Scheduler;
19 describe('mixing responders with the heritage event system', () => {
20 let container;
21
22 - if (!__EXPERIMENTAL__) {
23 - it("empty test so Jest doesn't complain", () => {});
24 - return;
25 - }
26 -
22 beforeEach(() => {
23 ReactFeatureFlags = require('shared/ReactFeatureFlags');
24 ReactFeatureFlags.enableDeprecatedFlareAPI = true;
@@ -39,11 +34,7 @@ describe('mixing responders with the heritage event system', () => {
34 container = null;
35 });
36
42 - if (!__EXPERIMENTAL__) {
43 - it("empty test so Jest doesn't complain", () => {});
44 - return;
45 - }
46 -
37 + // @gate experimental
38 it('should properly only flush sync once when the event systems are mixed', () => {
39 const useTap = require('react-interactions/events/tap').useTap;
40 const ref = React.createRef();
@@ -113,6 +104,7 @@ describe('mixing responders with the heritage event system', () => {
104 document.body.removeChild(newContainer);
105 });
106
107 + // @gate experimental
108 it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => {
109 const useTap = require('react-interactions/events/tap').useTap;
110 const ref = React.createRef();
@@ -182,6 +174,7 @@ describe('mixing responders with the heritage event system', () => {
174 document.body.removeChild(newContainer);
175 });
176
177 + // @gate experimental
178 it(
179 'should only flush before outermost discrete event handler when mixing ' +
180 'event systems',
@@ -241,6 +234,7 @@ describe('mixing responders with the heritage event system', () => {
234 );
235
236 describe('mixing the Input and Press repsonders', () => {
237 + // @gate experimental
238 it('is async for non-input events', () => {
239 const useTap = require('react-interactions/events/tap').useTap;
240 const useInput = require('react-interactions/events/input').useInput;
packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js
+185 -77
@@ -30,19 +30,17 @@ function initializeModules(hasPointerEvents) {
30 ReactFeatureFlags.enableDeprecatedFlareAPI = true;
31 React = require('react');
32 ReactDOM = require('react-dom');
33 - usePress = require('react-interactions/events/press').usePress;
34 -}
33
36 -const pointerTypesTable = [['mouse'], ['touch']];
34 + // TODO: This import throws outside of experimental mode. Figure out better
35 + // strategy for gated imports.
36 + if (__EXPERIMENTAL__) {
37 + usePress = require('react-interactions/events/press').usePress;
38 + }
39 +}
40
41 describeWithPointerEvent('Press responder', hasPointerEvents => {
42 let container;
43
41 - if (!__EXPERIMENTAL__) {
42 - it("empty test so Jest doesn't complain", () => {});
43 - return;
44 - }
45 -
44 beforeEach(() => {
45 initializeModules(hasPointerEvents);
46 container = document.createElement('div');
@@ -59,7 +57,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
57 describe('disabled', () => {
58 let onPressStart, onPressChange, onPressMove, onPressEnd, onPress, ref;
59
62 - beforeEach(() => {
60 + const componentInit = () => {
61 onPressStart = jest.fn();
62 onPressChange = jest.fn();
63 onPressMove = jest.fn();
@@ -78,9 +76,11 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
76 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
77 };
78 ReactDOM.render(<Component />, container);
81 - });
79 + };
80
81 + // @gate experimental
82 test('does not call callbacks for pointers', () => {
83 + componentInit();
84 const target = createEventTarget(ref.current);
85 target.pointerdown();
86 target.pointerup();
@@ -91,7 +91,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
91 expect(onPress).not.toBeCalled();
92 });
93
94 + // @gate experimental
95 test('does not call callbacks for keyboard', () => {
96 + componentInit();
97 const target = createEventTarget(ref.current);
98 target.keydown({key: 'Enter'});
99 target.keyup({key: 'Enter'});
@@ -106,7 +108,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
108 describe('onPressStart', () => {
109 let onPressStart, ref;
110
109 - beforeEach(() => {
111 + const componentInit = () => {
112 onPressStart = jest.fn();
113 ref = React.createRef();
114 const Component = () => {
@@ -117,21 +119,33 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
119 };
120 ReactDOM.render(<Component />, container);
121 document.elementFromPoint = () => ref.current;
122 + };
123 +
124 + // @gate experimental
125 + it('is called after pointer down: mouse', () => {
126 + componentInit();
127 + const target = createEventTarget(ref.current);
128 + target.pointerdown({pointerType: 'mouse'});
129 + expect(onPressStart).toHaveBeenCalledTimes(1);
130 + expect(onPressStart).toHaveBeenCalledWith(
131 + expect.objectContaining({pointerType: 'mouse', type: 'pressstart'}),
132 + );
133 });
134
122 - it.each(pointerTypesTable)(
123 - 'is called after pointer down: %s',
124 - pointerType => {
125 - const target = createEventTarget(ref.current);
126 - target.pointerdown({pointerType});
127 - expect(onPressStart).toHaveBeenCalledTimes(1);
128 - expect(onPressStart).toHaveBeenCalledWith(
129 - expect.objectContaining({pointerType, type: 'pressstart'}),
130 - );
131 - },
132 - );
135 + // @gate experimental
136 + it('is called after pointer down: mouse', () => {
137 + componentInit();
138 + const target = createEventTarget(ref.current);
139 + target.pointerdown({pointerType: 'touch'});
140 + expect(onPressStart).toHaveBeenCalledTimes(1);
141 + expect(onPressStart).toHaveBeenCalledWith(
142 + expect.objectContaining({pointerType: 'touch', type: 'pressstart'}),
143 + );
144 + });
145
146 + // @gate experimental
147 it('is called after middle-button pointer down', () => {
148 + componentInit();
149 const target = createEventTarget(ref.current);
150 const pointerType = 'mouse';
151 target.pointerdown({
@@ -150,7 +164,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
164 );
165 });
166
167 + // @gate experimental
168 it('is called after virtual middle-button pointer down', () => {
169 + componentInit();
170 const target = createEventTarget(ref.current);
171 const pointerType = 'mouse';
172 target.pointerdown({
@@ -169,7 +185,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
185 );
186 });
187
188 + // @gate experimental
189 it('ignores any events not caused by primary/middle-click or touch/pen contact', () => {
190 + componentInit();
191 const target = createEventTarget(ref.current);
192 target.pointerdown({buttons: buttonsType.secondary});
193 target.pointerup({buttons: buttonsType.secondary});
@@ -178,7 +196,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
196 expect(onPressStart).toHaveBeenCalledTimes(0);
197 });
198
199 + // @gate experimental
200 it('is called once after "keydown" events for Enter', () => {
201 + componentInit();
202 const target = createEventTarget(ref.current);
203 target.keydown({key: 'Enter'});
204 target.keydown({key: 'Enter'});
@@ -189,7 +209,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
209 );
210 });
211
212 + // @gate experimental
213 it('is called once after "keydown" events for Spacebar', () => {
214 + componentInit();
215 const target = createEventTarget(ref.current);
216 const preventDefault = jest.fn();
217 target.keydown({key: ' ', preventDefault});
@@ -204,7 +226,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
226 );
227 });
228
229 + // @gate experimental
230 it('is not called after "keydown" for other keys', () => {
231 + componentInit();
232 const target = createEventTarget(ref.current);
233 target.keydown({key: 'a'});
234 expect(onPressStart).not.toBeCalled();
@@ -214,7 +238,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
238 describe('onPressEnd', () => {
239 let onPressEnd, ref;
240
217 - beforeEach(() => {
241 + const componentInit = () => {
242 onPressEnd = jest.fn();
243 ref = React.createRef();
244 const Component = () => {
@@ -225,22 +249,35 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
249 };
250 ReactDOM.render(<Component />, container);
251 document.elementFromPoint = () => ref.current;
252 + };
253 +
254 + // @gate experimental
255 + it('is called after pointer up: mouse', () => {
256 + componentInit();
257 + const target = createEventTarget(ref.current);
258 + target.pointerdown({pointerType: 'mouse'});
259 + target.pointerup({pointerType: 'mouse'});
260 + expect(onPressEnd).toHaveBeenCalledTimes(1);
261 + expect(onPressEnd).toHaveBeenCalledWith(
262 + expect.objectContaining({pointerType: 'mouse', type: 'pressend'}),
263 + );
264 });
265
230 - it.each(pointerTypesTable)(
231 - 'is called after pointer up: %s',
232 - pointerType => {
233 - const target = createEventTarget(ref.current);
234 - target.pointerdown({pointerType});
235 - target.pointerup({pointerType});
236 - expect(onPressEnd).toHaveBeenCalledTimes(1);
237 - expect(onPressEnd).toHaveBeenCalledWith(
238 - expect.objectContaining({pointerType, type: 'pressend'}),
239 - );
240 - },
241 - );
266 + // @gate experimental
267 + it('is called after pointer up: touch', () => {
268 + componentInit();
269 + const target = createEventTarget(ref.current);
270 + target.pointerdown({pointerType: 'touch'});
271 + target.pointerup({pointerType: 'touch'});
272 + expect(onPressEnd).toHaveBeenCalledTimes(1);
273 + expect(onPressEnd).toHaveBeenCalledWith(
274 + expect.objectContaining({pointerType: 'touch', type: 'pressend'}),
275 + );
276 + });
277
278 + // @gate experimental
279 it('is called after middle-button pointer up', () => {
280 + componentInit();
281 const target = createEventTarget(ref.current);
282 target.pointerdown({
283 button: buttonType.auxiliary,
@@ -258,7 +295,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
295 );
296 });
297
298 + // @gate experimental
299 it('is called after virtual middle-button pointer up', () => {
300 + componentInit();
301 const target = createEventTarget(ref.current);
302 target.pointerdown({
303 button: buttonType.auxiliary,
@@ -276,7 +315,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
315 );
316 });
317
318 + // @gate experimental
319 it('is called after "keyup" event for Enter', () => {
320 + componentInit();
321 const target = createEventTarget(ref.current);
322 target.keydown({key: 'Enter'});
323 // click occurs before keyup
@@ -288,7 +329,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
329 );
330 });
331
332 + // @gate experimental
333 it('is called after "keyup" event for Spacebar', () => {
334 + componentInit();
335 const target = createEventTarget(ref.current);
336 target.keydown({key: ' '});
337 target.keyup({key: ' '});
@@ -298,14 +341,18 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
341 );
342 });
343
344 + // @gate experimental
345 it('is not called after "keyup" event for other keys', () => {
346 + componentInit();
347 const target = createEventTarget(ref.current);
348 target.keydown({key: 'Enter'});
349 target.keyup({key: 'a'});
350 expect(onPressEnd).not.toBeCalled();
351 });
352
353 + // @gate experimental
354 it('is called with keyboard modifiers', () => {
355 + componentInit();
356 const target = createEventTarget(ref.current);
357 target.keydown({key: 'Enter'});
358 target.keyup({
@@ -331,7 +378,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
378 describe('onPressChange', () => {
379 let onPressChange, ref;
380
334 - beforeEach(() => {
381 + const componentInit = () => {
382 onPressChange = jest.fn();
383 ref = React.createRef();
384 const Component = () => {
@@ -342,22 +389,35 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
389 };
390 ReactDOM.render(<Component />, container);
391 document.elementFromPoint = () => ref.current;
392 + };
393 +
394 + // @gate experimental
395 + it('is called after pointer down and up: %s', () => {
396 + componentInit();
397 + const target = createEventTarget(ref.current);
398 + target.pointerdown({pointerType: 'mouse'});
399 + expect(onPressChange).toHaveBeenCalledTimes(1);
400 + expect(onPressChange).toHaveBeenCalledWith(true);
401 + target.pointerup({pointerType: 'mouse'});
402 + expect(onPressChange).toHaveBeenCalledTimes(2);
403 + expect(onPressChange).toHaveBeenCalledWith(false);
404 });
405
347 - it.each(pointerTypesTable)(
348 - 'is called after pointer down and up: %s',
349 - pointerType => {
350 - const target = createEventTarget(ref.current);
351 - target.pointerdown({pointerType});
352 - expect(onPressChange).toHaveBeenCalledTimes(1);
353 - expect(onPressChange).toHaveBeenCalledWith(true);
354 - target.pointerup({pointerType});
355 - expect(onPressChange).toHaveBeenCalledTimes(2);
356 - expect(onPressChange).toHaveBeenCalledWith(false);
357 - },
358 - );
406 + // @gate experimental
407 + it('is called after pointer down and up: touch', () => {
408 + componentInit();
409 + const target = createEventTarget(ref.current);
410 + target.pointerdown({pointerType: 'touch'});
411 + expect(onPressChange).toHaveBeenCalledTimes(1);
412 + expect(onPressChange).toHaveBeenCalledWith(true);
413 + target.pointerup({pointerType: 'touch'});
414 + expect(onPressChange).toHaveBeenCalledTimes(2);
415 + expect(onPressChange).toHaveBeenCalledWith(false);
416 + });
417
418 + // @gate experimental
419 it('is called after valid "keydown" and "keyup" events', () => {
420 + componentInit();
421 const target = createEventTarget(ref.current);
422 target.keydown({key: 'Enter'});
423 expect(onPressChange).toHaveBeenCalledTimes(1);
@@ -371,7 +431,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
431 describe('onPress', () => {
432 let onPress, ref;
433
374 - beforeEach(() => {
434 + const componentInit = () => {
435 onPress = jest.fn();
436 ref = React.createRef();
437 const Component = () => {
@@ -388,22 +448,35 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
448 right: 100,
449 });
450 document.elementFromPoint = () => ref.current;
451 + };
452 +
453 + // @gate experimental
454 + it('is called after pointer up: %s', () => {
455 + componentInit();
456 + const target = createEventTarget(ref.current);
457 + target.pointerdown({pointerType: 'mouse'});
458 + target.pointerup({pointerType: 'mouse', x: 10, y: 10});
459 + expect(onPress).toHaveBeenCalledTimes(1);
460 + expect(onPress).toHaveBeenCalledWith(
461 + expect.objectContaining({pointerType: 'mouse', type: 'press'}),
462 + );
463 });
464
393 - it.each(pointerTypesTable)(
394 - 'is called after pointer up: %s',
395 - pointerType => {
396 - const target = createEventTarget(ref.current);
397 - target.pointerdown({pointerType});
398 - target.pointerup({pointerType, x: 10, y: 10});
399 - expect(onPress).toHaveBeenCalledTimes(1);
400 - expect(onPress).toHaveBeenCalledWith(
401 - expect.objectContaining({pointerType, type: 'press'}),
402 - );
403 - },
404 - );
465 + // @gate experimental
466 + it('is called after pointer up: %s', () => {
467 + componentInit();
468 + const target = createEventTarget(ref.current);
469 + target.pointerdown({pointerType: 'touch'});
470 + target.pointerup({pointerType: 'touch', x: 10, y: 10});
471 + expect(onPress).toHaveBeenCalledTimes(1);
472 + expect(onPress).toHaveBeenCalledWith(
473 + expect.objectContaining({pointerType: 'touch', type: 'press'}),
474 + );
475 + });
476
477 + // @gate experimental
478 it('is not called after middle-button press', () => {
479 + componentInit();
480 const target = createEventTarget(ref.current);
481 target.pointerdown({
482 button: buttonType.auxiliary,
@@ -414,7 +487,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
487 expect(onPress).not.toHaveBeenCalled();
488 });
489
490 + // @gate experimental
491 it('is not called after virtual middle-button press', () => {
492 + componentInit();
493 const target = createEventTarget(ref.current);
494 target.pointerdown({
495 button: buttonType.auxiliary,
@@ -425,7 +500,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
500 expect(onPress).not.toHaveBeenCalled();
501 });
502
503 + // @gate experimental
504 it('is called after valid "keyup" event', () => {
505 + componentInit();
506 const target = createEventTarget(ref.current);
507 target.keydown({key: 'Enter'});
508 target.keyup({key: 'Enter'});
@@ -435,7 +512,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
512 );
513 });
514
515 + // @gate experimental
516 it('is not called after invalid "keyup" event', () => {
517 + componentInit();
518 const inputRef = React.createRef();
519 const Component = () => {
520 const listener = usePress({onPress});
@@ -450,7 +529,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
529 expect(onPress).not.toBeCalled();
530 });
531
532 + // @gate experimental
533 it('is called with modifier keys', () => {
534 + componentInit();
535 const target = createEventTarget(ref.current);
536 target.pointerdown({metaKey: true, pointerType: 'mouse'});
537 target.pointerup({metaKey: true, pointerType: 'mouse'});
@@ -463,7 +544,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
544 );
545 });
546
547 + // @gate experimental
548 it('is called once after virtual screen reader "click" event', () => {
549 + componentInit();
550 const target = createEventTarget(ref.current);
551 const preventDefault = jest.fn();
552 target.virtualclick({preventDefault});
@@ -481,7 +564,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
564 describe('onPressMove', () => {
565 let onPressMove, ref;
566
484 - beforeEach(() => {
567 + const componentInit = () => {
568 onPressMove = jest.fn();
569 ref = React.createRef();
570 const Component = () => {
@@ -498,25 +581,41 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
581 right: 100,
582 });
583 document.elementFromPoint = () => ref.current;
584 + };
585 +
586 + // @gate experimental
587 + it('is called after pointer move: mouse', () => {
588 + componentInit();
589 + const node = ref.current;
590 + const target = createEventTarget(node);
591 + target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
592 + target.pointerdown({pointerType: 'mouse'});
593 + target.pointermove({pointerType: 'mouse', x: 10, y: 10});
594 + target.pointermove({pointerType: 'mouse', x: 20, y: 20});
595 + expect(onPressMove).toHaveBeenCalledTimes(2);
596 + expect(onPressMove).toHaveBeenCalledWith(
597 + expect.objectContaining({pointerType: 'mouse', type: 'pressmove'}),
598 + );
599 });
600
503 - it.each(pointerTypesTable)(
504 - 'is called after pointer move: %s',
505 - pointerType => {
506 - const node = ref.current;
507 - const target = createEventTarget(node);
508 - target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
509 - target.pointerdown({pointerType});
510 - target.pointermove({pointerType, x: 10, y: 10});
511 - target.pointermove({pointerType, x: 20, y: 20});
512 - expect(onPressMove).toHaveBeenCalledTimes(2);
513 - expect(onPressMove).toHaveBeenCalledWith(
514 - expect.objectContaining({pointerType, type: 'pressmove'}),
515 - );
516 - },
517 - );
601 + // @gate experimental
602 + it('is called after pointer move: touch', () => {
603 + componentInit();
604 + const node = ref.current;
605 + const target = createEventTarget(node);
606 + target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
607 + target.pointerdown({pointerType: 'touch'});
608 + target.pointermove({pointerType: 'touch', x: 10, y: 10});
609 + target.pointermove({pointerType: 'touch', x: 20, y: 20});
610 + expect(onPressMove).toHaveBeenCalledTimes(2);
611 + expect(onPressMove).toHaveBeenCalledWith(
612 + expect.objectContaining({pointerType: 'touch', type: 'pressmove'}),
613 + );
614 + });
615
616 + // @gate experimental
617 it('is not called if pointer move occurs during keyboard press', () => {
618 + componentInit();
619 const target = createEventTarget(ref.current);
620 target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
621 target.keydown({key: 'Enter'});
@@ -531,6 +630,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
630 });
631
632 describe('link components', () => {
633 + // @gate experimental
634 it('prevents native behavior by default', () => {
635 const onPress = jest.fn();
636 const preventDefault = jest.fn();
@@ -551,6 +651,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
651 );
652 });
653
654 + // @gate experimental
655 it('prevents native behaviour for keyboard events by default', () => {
656 const onPress = jest.fn();
657 const preventDefaultClick = jest.fn();
@@ -575,6 +676,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
676 );
677 });
678
679 + // @gate experimental
680 it('deeply prevents native behaviour by default', () => {
681 const onPress = jest.fn();
682 const preventDefault = jest.fn();
@@ -596,6 +698,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
698 expect(preventDefault).toBeCalled();
699 });
700
701 + // @gate experimental
702 it('prevents native behaviour by default with nested elements', () => {
703 const onPress = jest.fn();
704 const preventDefault = jest.fn();
@@ -620,6 +723,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
723 );
724 });
725
726 + // @gate experimental
727 it('uses native behaviour for interactions with modifier keys', () => {
728 const onPress = jest.fn();
729 const preventDefault = jest.fn();
@@ -642,6 +746,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
746 });
747 });
748
749 + // @gate experimental
750 it('uses native behaviour for pointer events if preventDefault is false', () => {
751 const onPress = jest.fn();
752 const preventDefault = jest.fn();
@@ -662,6 +767,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
767 );
768 });
769
770 + // @gate experimental
771 it('uses native behaviour for keyboard events if preventDefault is false', () => {
772 const onPress = jest.fn();
773 const preventDefault = jest.fn();
@@ -685,6 +791,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
791 });
792 });
793
794 + // @gate experimental
795 it('should not trigger an invariant in addRootEventTypes()', () => {
796 const ref = React.createRef();
797
@@ -701,6 +808,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
808 target.pointerdown();
809 });
810
811 + // @gate experimental
812 it('when blur occurs on a pressed target, we should disengage press', () => {
813 const onPress = jest.fn();
814 const onPressStart = jest.fn();
packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js
+231 -82
@@ -30,9 +30,14 @@ function initializeModules(hasPointerEvents) {
30 ReactFeatureFlags.enableDeprecatedFlareAPI = true;
31 React = require('react');
32 ReactDOM = require('react-dom');
33 - PressResponder = require('react-interactions/events/press-legacy')
34 - .PressResponder;
35 - usePress = require('react-interactions/events/press-legacy').usePress;
33 +
34 + // TODO: This import throws outside of experimental mode. Figure out better
35 + // strategy for gated imports.
36 + if (__EXPERIMENTAL__) {
37 + PressResponder = require('react-interactions/events/press-legacy')
38 + .PressResponder;
39 + usePress = require('react-interactions/events/press-legacy').usePress;
40 + }
41 }
42
43 function removePressMoveStrings(eventString) {
@@ -50,11 +55,6 @@ const pointerTypesTable = [['mouse'], ['touch']];
55 describe.each(environmentTable)('Press responder', hasPointerEvents => {
56 let container;
57
53 - if (!__EXPERIMENTAL__) {
54 - it("empty test so Jest doesn't complain", () => {});
55 - return;
56 - }
57 -
58 beforeEach(() => {
59 initializeModules(hasPointerEvents);
60 container = document.createElement('div');
@@ -71,7 +71,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
71 describe('disabled', () => {
72 let onPressStart, onPress, onPressEnd, ref;
73
74 - beforeEach(() => {
74 + const componentInit = () => {
75 onPressStart = jest.fn();
76 onPress = jest.fn();
77 onPressEnd = jest.fn();
@@ -87,9 +87,11 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
87 };
88 ReactDOM.render(<Component />, container);
89 document.elementFromPoint = () => ref.current;
90 - });
90 + };
91
92 + // @gate experimental
93 it('does not call callbacks', () => {
94 + componentInit();
95 const target = createEventTarget(ref.current);
96 target.pointerdown();
97 target.pointerup();
@@ -102,7 +104,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
104 describe('onPressStart', () => {
105 let onPressStart, ref;
106
105 - beforeEach(() => {
107 + const componentInit = () => {
108 onPressStart = jest.fn();
109 ref = React.createRef();
110 const Component = () => {
@@ -113,21 +115,33 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
115 };
116 ReactDOM.render(<Component />, container);
117 document.elementFromPoint = () => ref.current;
118 + };
119 +
120 + // @gate experimental
121 + it('is called after pointer down: mouse', () => {
122 + componentInit();
123 + const target = createEventTarget(ref.current);
124 + target.pointerdown({pointerType: 'mouse'});
125 + expect(onPressStart).toHaveBeenCalledTimes(1);
126 + expect(onPressStart).toHaveBeenCalledWith(
127 + expect.objectContaining({pointerType: 'mouse', type: 'pressstart'}),
128 + );
129 });
130
118 - it.each(pointerTypesTable)(
119 - 'is called after pointer down: %s',
120 - pointerType => {
121 - const target = createEventTarget(ref.current);
122 - target.pointerdown({pointerType});
123 - expect(onPressStart).toHaveBeenCalledTimes(1);
124 - expect(onPressStart).toHaveBeenCalledWith(
125 - expect.objectContaining({pointerType, type: 'pressstart'}),
126 - );
127 - },
128 - );
131 + // @gate experimental
132 + it('is called after pointer down: touch', () => {
133 + componentInit();
134 + const target = createEventTarget(ref.current);
135 + target.pointerdown({pointerType: 'touch'});
136 + expect(onPressStart).toHaveBeenCalledTimes(1);
137 + expect(onPressStart).toHaveBeenCalledWith(
138 + expect.objectContaining({pointerType: 'touch', type: 'pressstart'}),
139 + );
140 + });
141
142 + // @gate experimental
143 it('is called after middle-button pointer down', () => {
144 + componentInit();
145 const target = createEventTarget(ref.current);
146 target.pointerdown({
147 button: buttonType.auxiliary,
@@ -144,7 +158,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
158 );
159 });
160
161 + // @gate experimental
162 it('is not called after pointer move following middle-button press', () => {
163 + componentInit();
164 const node = ref.current;
165 const target = createEventTarget(node);
166 target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
@@ -159,7 +175,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
175 expect(onPressStart).toHaveBeenCalledTimes(1);
176 });
177
178 + // @gate experimental
179 it('ignores any events not caused by primary/middle-click or touch/pen contact', () => {
180 + componentInit();
181 const target = createEventTarget(ref.current);
182 target.pointerdown({buttons: buttonsType.secondary});
183 target.pointerup({buttons: buttonsType.secondary});
@@ -168,7 +186,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
186 expect(onPressStart).toHaveBeenCalledTimes(0);
187 });
188
189 + // @gate experimental
190 it('is called once after "keydown" events for Enter', () => {
191 + componentInit();
192 const target = createEventTarget(ref.current);
193 target.keydown({key: 'Enter'});
194 target.keydown({key: 'Enter'});
@@ -179,7 +199,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
199 );
200 });
201
202 + // @gate experimental
203 it('is called once after "keydown" events for Spacebar', () => {
204 + componentInit();
205 const target = createEventTarget(ref.current);
206 const preventDefault = jest.fn();
207 target.keydown({key: ' ', preventDefault});
@@ -194,7 +216,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
216 );
217 });
218
219 + // @gate experimental
220 it('is not called after "keydown" for other keys', () => {
221 + componentInit();
222 const target = createEventTarget(ref.current);
223 target.keydown({key: 'a'});
224 expect(onPressStart).not.toBeCalled();
@@ -204,7 +228,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
228 describe('onPressEnd', () => {
229 let onPressEnd, ref;
230
207 - beforeEach(() => {
231 + const componentInit = () => {
232 onPressEnd = jest.fn();
233 ref = React.createRef();
234 const Component = () => {
@@ -215,22 +239,35 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
239 };
240 ReactDOM.render(<Component />, container);
241 document.elementFromPoint = () => ref.current;
242 + };
243 +
244 + // @gate experimental
245 + it('is called after pointer up: mouse', () => {
246 + componentInit();
247 + const target = createEventTarget(ref.current);
248 + target.pointerdown({pointerType: 'mouse'});
249 + target.pointerup({pointerType: 'mouse'});
250 + expect(onPressEnd).toHaveBeenCalledTimes(1);
251 + expect(onPressEnd).toHaveBeenCalledWith(
252 + expect.objectContaining({pointerType: 'mouse', type: 'pressend'}),
253 + );
254 });
255
220 - it.each(pointerTypesTable)(
221 - 'is called after pointer up: %s',
222 - pointerType => {
223 - const target = createEventTarget(ref.current);
224 - target.pointerdown({pointerType});
225 - target.pointerup({pointerType});
226 - expect(onPressEnd).toHaveBeenCalledTimes(1);
227 - expect(onPressEnd).toHaveBeenCalledWith(
228 - expect.objectContaining({pointerType, type: 'pressend'}),
229 - );
230 - },
231 - );
256 + // @gate experimental
257 + it('is called after pointer up: touch', () => {
258 + componentInit();
259 + const target = createEventTarget(ref.current);
260 + target.pointerdown({pointerType: 'touch'});
261 + target.pointerup({pointerType: 'touch'});
262 + expect(onPressEnd).toHaveBeenCalledTimes(1);
263 + expect(onPressEnd).toHaveBeenCalledWith(
264 + expect.objectContaining({pointerType: 'touch', type: 'pressend'}),
265 + );
266 + });
267
268 + // @gate experimental
269 it('is called after middle-button pointer up', () => {
270 + componentInit();
271 const target = createEventTarget(ref.current);
272 target.pointerdown({
273 buttons: buttonsType.auxiliary,
@@ -247,7 +284,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
284 );
285 });
286
287 + // @gate experimental
288 it('is called after "keyup" event for Enter', () => {
289 + componentInit();
290 const target = createEventTarget(ref.current);
291 target.keydown({key: 'Enter'});
292 // click occurs before keyup
@@ -259,7 +298,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
298 );
299 });
300
301 + // @gate experimental
302 it('is called after "keyup" event for Spacebar', () => {
303 + componentInit();
304 const target = createEventTarget(ref.current);
305 target.keydown({key: ' '});
306 target.keyup({key: ' '});
@@ -269,14 +310,18 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
310 );
311 });
312
313 + // @gate experimental
314 it('is not called after "keyup" event for other keys', () => {
315 + componentInit();
316 const target = createEventTarget(ref.current);
317 target.keydown({key: 'Enter'});
318 target.keyup({key: 'a'});
319 expect(onPressEnd).not.toBeCalled();
320 });
321
322 + // @gate experimental
323 it('is called with keyboard modifiers', () => {
324 + componentInit();
325 const target = createEventTarget(ref.current);
326 target.keydown({key: 'Enter'});
327 target.keyup({
@@ -302,7 +347,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
347 describe('onPressChange', () => {
348 let onPressChange, ref;
349
305 - beforeEach(() => {
350 + const componentInit = () => {
351 onPressChange = jest.fn();
352 ref = React.createRef();
353 const Component = () => {
@@ -313,22 +358,35 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
358 };
359 ReactDOM.render(<Component />, container);
360 document.elementFromPoint = () => ref.current;
361 + };
362 +
363 + // @gate experimental
364 + it('is called after pointer down and up: mouse', () => {
365 + componentInit();
366 + const target = createEventTarget(ref.current);
367 + target.pointerdown({pointerType: 'mouse'});
368 + expect(onPressChange).toHaveBeenCalledTimes(1);
369 + expect(onPressChange).toHaveBeenCalledWith(true);
370 + target.pointerup({pointerType: 'mouse'});
371 + expect(onPressChange).toHaveBeenCalledTimes(2);
372 + expect(onPressChange).toHaveBeenCalledWith(false);
373 });
374
318 - it.each(pointerTypesTable)(
319 - 'is called after pointer down and up: %s',
320 - pointerType => {
321 - const target = createEventTarget(ref.current);
322 - target.pointerdown({pointerType});
323 - expect(onPressChange).toHaveBeenCalledTimes(1);
324 - expect(onPressChange).toHaveBeenCalledWith(true);
325 - target.pointerup({pointerType});
326 - expect(onPressChange).toHaveBeenCalledTimes(2);
327 - expect(onPressChange).toHaveBeenCalledWith(false);
328 - },
329 - );
375 + // @gate experimental
376 + it('is called after pointer down and up: touch', () => {
377 + componentInit();
378 + const target = createEventTarget(ref.current);
379 + target.pointerdown({pointerType: 'touch'});
380 + expect(onPressChange).toHaveBeenCalledTimes(1);
381 + expect(onPressChange).toHaveBeenCalledWith(true);
382 + target.pointerup({pointerType: 'touch'});
383 + expect(onPressChange).toHaveBeenCalledTimes(2);
384 + expect(onPressChange).toHaveBeenCalledWith(false);
385 + });
386
387 + // @gate experimental
388 it('is called after valid "keydown" and "keyup" events', () => {
389 + componentInit();
390 const target = createEventTarget(ref.current);
391 target.keydown({key: 'Enter'});
392 expect(onPressChange).toHaveBeenCalledTimes(1);
@@ -342,7 +400,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
400 describe('onPress', () => {
401 let onPress, ref;
402
345 - beforeEach(() => {
403 + const componentInit = () => {
404 onPress = jest.fn();
405 ref = React.createRef();
406 const Component = () => {
@@ -359,22 +417,35 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
417 right: 100,
418 });
419 document.elementFromPoint = () => ref.current;
420 + };
421 +
422 + // @gate experimental
423 + it('is called after pointer up: mouse', () => {
424 + componentInit();
425 + const target = createEventTarget(ref.current);
426 + target.pointerdown({pointerType: 'mouse'});
427 + target.pointerup({pointerType: 'mouse', x: 10, y: 10});
428 + expect(onPress).toHaveBeenCalledTimes(1);
429 + expect(onPress).toHaveBeenCalledWith(
430 + expect.objectContaining({pointerType: 'mouse', type: 'press'}),
431 + );
432 });
433
364 - it.each(pointerTypesTable)(
365 - 'is called after pointer up: %s',
366 - pointerType => {
367 - const target = createEventTarget(ref.current);
368 - target.pointerdown({pointerType});
369 - target.pointerup({pointerType, x: 10, y: 10});
370 - expect(onPress).toHaveBeenCalledTimes(1);
371 - expect(onPress).toHaveBeenCalledWith(
372 - expect.objectContaining({pointerType, type: 'press'}),
373 - );
374 - },
375 - );
434 + // @gate experimental
435 + it('is called after pointer up: touch', () => {
436 + componentInit();
437 + const target = createEventTarget(ref.current);
438 + target.pointerdown({pointerType: 'touch'});
439 + target.pointerup({pointerType: 'touch', x: 10, y: 10});
440 + expect(onPress).toHaveBeenCalledTimes(1);
441 + expect(onPress).toHaveBeenCalledWith(
442 + expect.objectContaining({pointerType: 'touch', type: 'press'}),
443 + );
444 + });
445
446 + // @gate experimental
447 it('is not called after middle-button press', () => {
448 + componentInit();
449 const target = createEventTarget(ref.current);
450 target.pointerdown({
451 buttons: buttonsType.auxiliary,
@@ -384,7 +455,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
455 expect(onPress).not.toHaveBeenCalled();
456 });
457
458 + // @gate experimental
459 it('is not called after virtual middle-button press', () => {
460 + componentInit();
461 const target = createEventTarget(ref.current);
462 target.pointerdown({
463 button: buttonType.auxiliary,
@@ -395,7 +468,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
468 expect(onPress).not.toHaveBeenCalled();
469 });
470
471 + // @gate experimental
472 it('is called after valid "keyup" event', () => {
473 + componentInit();
474 const target = createEventTarget(ref.current);
475 target.keydown({key: 'Enter'});
476 target.keyup({key: 'Enter'});
@@ -405,7 +480,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
480 );
481 });
482
483 + // @gate experimental
484 it('is not called after invalid "keyup" event', () => {
485 + componentInit();
486 const inputRef = React.createRef();
487 const Component = () => {
488 const listener = usePress({onPress});
@@ -420,7 +497,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
497 expect(onPress).not.toBeCalled();
498 });
499
500 + // @gate experimental
501 it('is called with modifier keys', () => {
502 + componentInit();
503 const target = createEventTarget(ref.current);
504 target.pointerdown({metaKey: true, pointerType: 'mouse'});
505 target.pointerup({metaKey: true, pointerType: 'mouse'});
@@ -433,7 +512,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
512 );
513 });
514
515 + // @gate experimental
516 it('is called if target rect is not right but the target is (for mouse events)', () => {
517 + componentInit();
518 const buttonRef = React.createRef();
519 const divRef = React.createRef();
520
@@ -455,7 +536,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
536 expect(onPress).toBeCalled();
537 });
538
539 + // @gate experimental
540 it('is called once after virtual screen reader "click" event', () => {
541 + componentInit();
542 const target = createEventTarget(ref.current);
543 const preventDefault = jest.fn();
544 target.virtualclick({preventDefault});
@@ -473,7 +556,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
556 describe('onPressMove', () => {
557 let onPressMove, ref;
558
476 - beforeEach(() => {
559 + const componentInit = () => {
560 onPressMove = jest.fn();
561 ref = React.createRef();
562 const Component = () => {
@@ -490,25 +573,41 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
573 right: 100,
574 });
575 document.elementFromPoint = () => ref.current;
576 + };
577 +
578 + // @gate experimental
579 + it('is called after pointer move: mouse', () => {
580 + componentInit();
581 + const node = ref.current;
582 + const target = createEventTarget(node);
583 + target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
584 + target.pointerdown({pointerType: 'mouse'});
585 + target.pointermove({pointerType: 'mouse', x: 10, y: 10});
586 + target.pointermove({pointerType: 'mouse', x: 20, y: 20});
587 + expect(onPressMove).toHaveBeenCalledTimes(2);
588 + expect(onPressMove).toHaveBeenCalledWith(
589 + expect.objectContaining({pointerType: 'mouse', type: 'pressmove'}),
590 + );
591 });
592
495 - it.each(pointerTypesTable)(
496 - 'is called after pointer move: %s',
497 - pointerType => {
498 - const node = ref.current;
499 - const target = createEventTarget(node);
500 - target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
501 - target.pointerdown({pointerType});
502 - target.pointermove({pointerType, x: 10, y: 10});
503 - target.pointermove({pointerType, x: 20, y: 20});
504 - expect(onPressMove).toHaveBeenCalledTimes(2);
505 - expect(onPressMove).toHaveBeenCalledWith(
506 - expect.objectContaining({pointerType, type: 'pressmove'}),
507 - );
508 - },
509 - );
593 + // @gate experimental
594 + it('is called after pointer move: touch', () => {
595 + componentInit();
596 + const node = ref.current;
597 + const target = createEventTarget(node);
598 + target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
599 + target.pointerdown({pointerType: 'touch'});
600 + target.pointermove({pointerType: 'touch', x: 10, y: 10});
601 + target.pointermove({pointerType: 'touch', x: 20, y: 20});
602 + expect(onPressMove).toHaveBeenCalledTimes(2);
603 + expect(onPressMove).toHaveBeenCalledWith(
604 + expect.objectContaining({pointerType: 'touch', type: 'pressmove'}),
605 + );
606 + });
607
608 + // @gate experimental
609 it('is not called if pointer move occurs during keyboard press', () => {
610 + componentInit();
611 const target = createEventTarget(ref.current);
612 target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
613 target.keydown({key: 'Enter'});
@@ -525,7 +624,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
624 describe.each(pointerTypesTable)('press with movement: %s', pointerType => {
625 let events, ref, outerRef;
626
528 - beforeEach(() => {
627 + const componentInit = () => {
628 events = [];
629 ref = React.createRef();
630 outerRef = React.createRef();
@@ -548,7 +647,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
647 };
648 ReactDOM.render(<Component />, container);
649 document.elementFromPoint = () => ref.current;
551 - });
650 + };
651
652 const rectMock = {width: 100, height: 100, x: 50, y: 50};
653 const pressRectOffset = 20;
@@ -569,7 +668,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
668 * │ HitRect X │ <= Move to X and release
669 * └──────────────────┘
670 */
671 + // @gate experimental
672 it('"onPress*" events are called immediately', () => {
673 + componentInit();
674 const target = createEventTarget(ref.current);
675 target.setBoundingClientRect(rectMock);
676 target.pointerdown({pointerType});
@@ -585,7 +686,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
686 ]);
687 });
688
689 + // @gate experimental
690 it('"onPress*" events are correctly called with target change', () => {
691 + componentInit();
692 const target = createEventTarget(ref.current);
693 const outerTarget = createEventTarget(outerRef.current);
694 target.setBoundingClientRect(rectMock);
@@ -614,7 +717,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
717 ]);
718 });
719
720 + // @gate experimental
721 it('press retention offset can be configured', () => {
722 + componentInit();
723 const localEvents = [];
724 const localRef = React.createRef();
725 const createEventHandler = msg => () => {
@@ -654,7 +759,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
759 ]);
760 });
761
762 + // @gate experimental
763 it('responder region accounts for decrease in element dimensions', () => {
764 + componentInit();
765 const target = createEventTarget(ref.current);
766 target.setBoundingClientRect(rectMock);
767 target.pointerdown({pointerType});
@@ -674,7 +781,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
781 ]);
782 });
783
784 + // @gate experimental
785 it('responder region accounts for increase in element dimensions', () => {
786 + componentInit();
787 const target = createEventTarget(ref.current);
788 target.setBoundingClientRect(rectMock);
789 target.pointerdown({pointerType});
@@ -704,7 +813,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
813 * └──────────────────┘
814 * X <= Move to X and release
815 */
816 + // @gate experimental
817 it('"onPress" is not called on release', () => {
818 + componentInit();
819 const target = createEventTarget(ref.current);
820 const targetContainer = createEventTarget(container);
821 target.setBoundingClientRect(rectMock);
@@ -727,7 +838,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
838 });
839 });
840
841 + // @gate experimental
842 it('"onPress" is called on re-entry to hit rect', () => {
843 + componentInit();
844 const target = createEventTarget(ref.current);
845 const targetContainer = createEventTarget(container);
846 target.setBoundingClientRect(rectMock);
@@ -759,6 +872,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
872
873 describe('nested responders', () => {
874 if (hasPointerEvents) {
875 + // @gate experimental
876 it('dispatch events in the correct order', () => {
877 const events = [];
878 const ref = React.createRef();
@@ -819,6 +933,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
933 }
934
935 describe('correctly not propagate', () => {
936 + // @gate experimental
937 it('for onPress', () => {
938 const ref = React.createRef();
939 const onPress = jest.fn();
@@ -845,6 +960,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
960 expect(onPress).toHaveBeenCalledTimes(1);
961 });
962
963 + // @gate experimental
964 it('for onPressStart/onPressEnd', () => {
965 const ref = React.createRef();
966 const onPressStart = jest.fn();
@@ -874,6 +990,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
990 expect(onPressEnd).toHaveBeenCalledTimes(1);
991 });
992
993 + // @gate experimental
994 it('for onPressChange', () => {
995 const ref = React.createRef();
996 const onPressChange = jest.fn();
@@ -903,6 +1020,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1020 });
1021
1022 describe('link components', () => {
1023 + // @gate experimental
1024 it('prevents native behavior by default', () => {
1025 const onPress = jest.fn();
1026 const preventDefault = jest.fn();
@@ -923,6 +1041,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1041 );
1042 });
1043
1044 + // @gate experimental
1045 it('prevents native behaviour for keyboard events by default', () => {
1046 const onPress = jest.fn();
1047 const preventDefault = jest.fn();
@@ -943,6 +1062,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1062 );
1063 });
1064
1065 + // @gate experimental
1066 it('deeply prevents native behaviour by default', () => {
1067 const onPress = jest.fn();
1068 const preventDefault = jest.fn();
@@ -964,6 +1084,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1084 expect(preventDefault).toBeCalled();
1085 });
1086
1087 + // @gate experimental
1088 it('prevents native behaviour by default with nested elements', () => {
1089 const onPress = jest.fn();
1090 const preventDefault = jest.fn();
@@ -988,6 +1109,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1109 );
1110 });
1111
1112 + // @gate experimental
1113 it('uses native behaviour for interactions with modifier keys', () => {
1114 const onPress = jest.fn();
1115 const preventDefault = jest.fn();
@@ -1010,6 +1132,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1132 });
1133 });
1134
1135 + // @gate experimental
1136 it('uses native behaviour for pointer events if preventDefault is false', () => {
1137 const onPress = jest.fn();
1138 const preventDefault = jest.fn();
@@ -1030,6 +1153,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1153 );
1154 });
1155
1156 + // @gate experimental
1157 it('uses native behaviour for keyboard events if preventDefault is false', () => {
1158 const onPress = jest.fn();
1159 const preventDefault = jest.fn();
@@ -1053,7 +1177,8 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1177 });
1178
1179 describe('responder cancellation', () => {
1056 - it.each(pointerTypesTable)('ends on pointer cancel', pointerType => {
1180 + // @gate experimental
1181 + it('ends on pointer cancel: mouse', () => {
1182 const onPressEnd = jest.fn();
1183 const ref = React.createRef();
1184
@@ -1064,12 +1189,30 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1189 ReactDOM.render(<Component />, container);
1190
1191 const target = createEventTarget(ref.current);
1067 - target.pointerdown({pointerType});
1068 - target.pointercancel({pointerType});
1192 + target.pointerdown({pointerType: 'mouse'});
1193 + target.pointercancel({pointerType: 'mouse'});
1194 + expect(onPressEnd).toHaveBeenCalledTimes(1);
1195 + });
1196 +
1197 + // @gate experimental
1198 + it('ends on pointer cancel: touch', () => {
1199 + const onPressEnd = jest.fn();
1200 + const ref = React.createRef();
1201 +
1202 + const Component = () => {
1203 + const listener = usePress({onPressEnd});
1204 + return <a href="#" ref={ref} DEPRECATED_flareListeners={listener} />;
1205 + };
1206 + ReactDOM.render(<Component />, container);
1207 +
1208 + const target = createEventTarget(ref.current);
1209 + target.pointerdown({pointerType: 'touch'});
1210 + target.pointercancel({pointerType: 'touch'});
1211 expect(onPressEnd).toHaveBeenCalledTimes(1);
1212 });
1213 });
1214
1215 + // @gate experimental
1216 it('does end on "scroll" to document (not mouse)', () => {
1217 const onPressEnd = jest.fn();
1218 const ref = React.createRef();
@@ -1087,6 +1230,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1230 expect(onPressEnd).toHaveBeenCalledTimes(1);
1231 });
1232
1233 + // @gate experimental
1234 it('does end on "scroll" to a parent container (not mouse)', () => {
1235 const onPressEnd = jest.fn();
1236 const ref = React.createRef();
@@ -1109,6 +1253,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1253 expect(onPressEnd).toHaveBeenCalledTimes(1);
1254 });
1255
1256 + // @gate experimental
1257 it('does not end on "scroll" to an element outside', () => {
1258 const onPressEnd = jest.fn();
1259 const ref = React.createRef();
@@ -1132,10 +1277,12 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1277 expect(onPressEnd).not.toBeCalled();
1278 });
1279
1280 + // @gate experimental
1281 it('expect displayName to show up for event component', () => {
1282 expect(PressResponder.displayName).toBe('Press');
1283 });
1284
1285 + // @gate experimental
1286 it('should not trigger an invariant in addRootEventTypes()', () => {
1287 const ref = React.createRef();
1288
@@ -1152,6 +1299,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1299 target.pointerdown();
1300 });
1301
1302 + // @gate experimental
1303 it('event.preventDefault works as expected', () => {
1304 const onPress = jest.fn(e => e.preventDefault());
1305 const onPressStart = jest.fn(e => e.preventDefault());
@@ -1173,6 +1321,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
1321 expect(onPressEnd).toBeCalled();
1322 });
1323
1324 + // @gate experimental
1325 it('when blur occurs on a pressed target, we should disengage press', () => {
1326 const onPress = jest.fn();
1327 const onPressStart = jest.fn();
packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js
+417 -331
@@ -31,7 +31,12 @@ function initializeModules(hasPointerEvents) {
31 ReactFeatureFlags.enableDeprecatedFlareAPI = true;
32 React = require('react');
33 ReactDOM = require('react-dom');
34 - useTap = require('react-interactions/events/tap').useTap;
34 +
35 + // TODO: This import throws outside of experimental mode. Figure out better
36 + // strategy for gated imports.
37 + if (__EXPERIMENTAL__) {
38 + useTap = require('react-interactions/events/tap').useTap;
39 + }
40 }
41
42 const coordinatesInside = {x: 51, y: 51};
@@ -73,11 +78,6 @@ function tapAndReleaseOutside({
78 describeWithPointerEvent('Tap responder', hasPointerEvents => {
79 let container;
80
76 - if (!__EXPERIMENTAL__) {
77 - it("empty test so Jest doesn't complain", () => {});
78 - return;
79 - }
80 -
81 beforeEach(() => {
82 initializeModules(hasPointerEvents);
83 container = document.createElement('div');
@@ -91,6 +91,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
91 resetActivePointers();
92 });
93
94 + // @gate experimental
95 test('supports repeated use', () => {
96 const ref = React.createRef();
97 const Component = () => {
@@ -114,7 +115,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
115 describe('disabled', () => {
116 let onTapStart, onTapChange, onTapUpdate, onTapCancel, onTapEnd, ref;
117
117 - beforeEach(() => {
118 + const componentInit = () => {
119 onTapStart = jest.fn();
120 onTapChange = jest.fn();
121 onTapUpdate = jest.fn();
@@ -133,9 +134,11 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
134 return <div ref={ref} DEPRECATED_flareListeners={listener} />;
135 };
136 ReactDOM.render(<Component />, container);
136 - });
137 + };
138
139 + // @gate experimental
140 test('does not call callbacks', () => {
141 + componentInit();
142 const target = createEventTarget(ref.current);
143 target.pointerdown();
144 target.pointerup();
@@ -159,7 +162,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
162 document.elementFromPoint = () => ref.current;
163 }
164
162 - beforeEach(() => {
165 + const componentInit = () => {
166 onTapCancel = jest.fn();
167 onTapUpdate = jest.fn();
168 ref = React.createRef();
@@ -168,9 +171,11 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
171 onTapCancel,
172 onTapUpdate,
173 });
171 - });
174 + };
175
176 + // @gate experimental
177 test('ignores values less than 10', () => {
178 + componentInit();
179 render({
180 maximumDistance: 5,
181 onTapCancel,
@@ -184,27 +189,33 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
189 expect(onTapCancel).toHaveBeenCalledTimes(0);
190 });
191
187 - testWithPointerType('below threshold', pointerType => {
188 - const target = createEventTarget(ref.current);
189 - target.pointerdown({pointerType, x: 0, y: 0});
190 - target.pointermove({pointerType, x: 10, y: 10});
191 - expect(onTapUpdate).toHaveBeenCalledTimes(1);
192 - expect(onTapCancel).toHaveBeenCalledTimes(0);
193 - });
192 + // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
193 + // the @gate pragma.
194 + if (__EXPERIMENTAL__) {
195 + testWithPointerType('below threshold', pointerType => {
196 + componentInit();
197 + const target = createEventTarget(ref.current);
198 + target.pointerdown({pointerType, x: 0, y: 0});
199 + target.pointermove({pointerType, x: 10, y: 10});
200 + expect(onTapUpdate).toHaveBeenCalledTimes(1);
201 + expect(onTapCancel).toHaveBeenCalledTimes(0);
202 + });
203
195 - testWithPointerType('above threshold', pointerType => {
196 - const target = createEventTarget(ref.current);
197 - target.pointerdown({pointerType, x: 0, y: 0});
198 - target.pointermove({pointerType, x: 15, y: 14});
199 - expect(onTapUpdate).toHaveBeenCalledTimes(0);
200 - expect(onTapCancel).toHaveBeenCalledTimes(1);
201 - });
204 + testWithPointerType('above threshold', pointerType => {
205 + componentInit();
206 + const target = createEventTarget(ref.current);
207 + target.pointerdown({pointerType, x: 0, y: 0});
208 + target.pointermove({pointerType, x: 15, y: 14});
209 + expect(onTapUpdate).toHaveBeenCalledTimes(0);
210 + expect(onTapCancel).toHaveBeenCalledTimes(1);
211 + });
212 + }
213 });
214
215 describe('onAuxiliaryTap', () => {
216 let onAuxiliaryTap, ref;
217
207 - beforeEach(() => {
218 + const componentInit = () => {
219 onAuxiliaryTap = jest.fn();
220 ref = React.createRef();
221 const Component = () => {
@@ -213,9 +224,11 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
224 };
225 ReactDOM.render(<Component />, container);
226 document.elementFromPoint = () => ref.current;
216 - });
227 + };
228
229 + // @gate experimental
230 test('auxiliary-button pointer up', () => {
231 + componentInit();
232 const pointerType = 'mouse';
233 const button = buttonType.auxiliary;
234 const buttons = buttonsType.auxiliary;
@@ -225,7 +238,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
238 expect(onAuxiliaryTap).toHaveBeenCalledTimes(1);
239 });
240
241 + // @gate experimental
242 test('modifier-button pointer up', () => {
243 + componentInit();
244 const pointerType = 'mouse';
245 const button = buttonType.primary;
246 const buttons = buttonsType.primary;
@@ -239,7 +254,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
254 describe('onTapStart', () => {
255 let onTapStart, ref;
256
242 - beforeEach(() => {
257 + const componentInit = () => {
258 onTapStart = jest.fn();
259 ref = React.createRef();
260 const Component = () => {
@@ -248,54 +263,61 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
263 };
264 ReactDOM.render(<Component />, container);
265 document.elementFromPoint = () => ref.current;
251 - });
266 + };
267
253 - testWithPointerType('pointer down', pointerType => {
254 - const target = createEventTarget(ref.current);
255 - const nativeEvent = {
256 - button: buttonType.primary,
257 - buttons: buttonsType.primary,
258 - pageX: 10,
259 - pageY: 10,
260 - pointerType,
261 - x: 10,
262 - y: 10,
263 - };
264 - target.pointerdown(nativeEvent);
265 - // 'pointerup' is only for the MouseEvent/TouchEvent fallback
266 - // implementation. We also dispatch 'pointerup' so that this test covers
267 - // the case where browsers dispatch an emulated mousedown (and mouseup)
268 - // event *after* a touch ends.
269 - target.pointerup(nativeEvent);
270 - expect(onTapStart).toHaveBeenCalledTimes(1);
271 - expect(onTapStart).toHaveBeenCalledWith(
272 - expect.objectContaining({
273 - altKey: false,
274 - ctrlKey: false,
275 - height: pointerType === 'mouse' ? 1 : 23,
276 - metaKey: false,
268 + // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
269 + // the @gate pragma.
270 + if (__EXPERIMENTAL__) {
271 + testWithPointerType('pointer down', pointerType => {
272 + componentInit();
273 + const target = createEventTarget(ref.current);
274 + const nativeEvent = {
275 + button: buttonType.primary,
276 + buttons: buttonsType.primary,
277 pageX: 10,
278 pageY: 10,
279 pointerType,
280 - pressure: pointerType === 'touch' ? 1 : 0.5,
281 - screenX: 10,
282 - screenY: 60,
283 - shiftKey: false,
284 - tangentialPressure: 0,
285 - target: target.node,
286 - tiltX: 0,
287 - tiltY: 0,
288 - timeStamp: expect.any(Number),
289 - twist: 0,
290 - type: 'tap:start',
291 - width: pointerType === 'mouse' ? 1 : 23,
280 x: 10,
281 y: 10,
294 - }),
295 - );
296 - });
282 + };
283 + target.pointerdown(nativeEvent);
284 + // 'pointerup' is only for the MouseEvent/TouchEvent fallback
285 + // implementation. We also dispatch 'pointerup' so that this test covers
286 + // the case where browsers dispatch an emulated mousedown (and mouseup)
287 + // event *after* a touch ends.
288 + target.pointerup(nativeEvent);
289 + expect(onTapStart).toHaveBeenCalledTimes(1);
290 + expect(onTapStart).toHaveBeenCalledWith(
291 + expect.objectContaining({
292 + altKey: false,
293 + ctrlKey: false,
294 + height: pointerType === 'mouse' ? 1 : 23,
295 + metaKey: false,
296 + pageX: 10,
297 + pageY: 10,
298 + pointerType,
299 + pressure: pointerType === 'touch' ? 1 : 0.5,
300 + screenX: 10,
301 + screenY: 60,
302 + shiftKey: false,
303 + tangentialPressure: 0,
304 + target: target.node,
305 + tiltX: 0,
306 + tiltY: 0,
307 + timeStamp: expect.any(Number),
308 + twist: 0,
309 + type: 'tap:start',
310 + width: pointerType === 'mouse' ? 1 : 23,
311 + x: 10,
312 + y: 10,
313 + }),
314 + );
315 + });
316 + }
317
318 + // @gate experimental
319 test('second pointer on target', () => {
320 + componentInit();
321 const pointerType = 'touch';
322 const target = createEventTarget(ref.current);
323 const button = buttonType.primary;
@@ -306,79 +328,84 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
328 expect(onTapStart).toHaveBeenCalledTimes(1);
329 });
330
309 - testWithPointerType('ignored buttons and modifiers', pointerType => {
310 - const target = createEventTarget(ref.current);
311 - if (pointerType !== 'touch') {
312 - // right-click
331 + // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
332 + // the @gate pragma.
333 + if (__EXPERIMENTAL__) {
334 + testWithPointerType('ignored buttons and modifiers', pointerType => {
335 + componentInit();
336 + const target = createEventTarget(ref.current);
337 + if (pointerType !== 'touch') {
338 + // right-click
339 + target.pointerdown({
340 + button: buttonType.secondary,
341 + buttons: buttonsType.secondary,
342 + pointerType,
343 + });
344 + target.pointerup({pointerType});
345 + // middle-click
346 + target.pointerdown({
347 + button: buttonType.auxiliary,
348 + buttons: buttonsType.auxiliary,
349 + pointerType,
350 + });
351 + target.pointerup({pointerType});
352 + // virtual middle-click with misleading 'buttons' value
353 + target.pointerdown({
354 + button: buttonType.auxiliary,
355 + buttons: 0,
356 + pointerType,
357 + });
358 + target.pointerup({pointerType});
359 + // pen eraser
360 + target.pointerdown({
361 + button: buttonType.eraser,
362 + buttons: buttonsType.eraser,
363 + pointerType,
364 + });
365 + target.pointerup({pointerType});
366 + }
367 + // alt-click
368 target.pointerdown({
314 - button: buttonType.secondary,
315 - buttons: buttonsType.secondary,
369 + button: buttonType.primary,
370 + buttons: buttonsType.primary,
371 + altKey: true,
372 pointerType,
373 });
374 target.pointerup({pointerType});
319 - // middle-click
375 + // ctrl-click
376 target.pointerdown({
321 - button: buttonType.auxiliary,
322 - buttons: buttonsType.auxiliary,
377 + button: buttonType.primary,
378 + buttons: buttonsType.primary,
379 + ctrlKey: true,
380 pointerType,
381 });
382 target.pointerup({pointerType});
326 - // virtual middle-click with misleading 'buttons' value
383 + // meta-click
384 target.pointerdown({
328 - button: buttonType.auxiliary,
329 - buttons: 0,
385 + button: buttonType.primary,
386 + buttons: buttonsType.primary,
387 + metaKey: true,
388 pointerType,
389 });
390 target.pointerup({pointerType});
333 - // pen eraser
391 + // shift-click
392 target.pointerdown({
335 - button: buttonType.eraser,
336 - buttons: buttonsType.eraser,
393 + button: buttonType.primary,
394 + buttons: buttonsType.primary,
395 + shiftKey: true,
396 pointerType,
397 });
398 target.pointerup({pointerType});
340 - }
341 - // alt-click
342 - target.pointerdown({
343 - button: buttonType.primary,
344 - buttons: buttonsType.primary,
345 - altKey: true,
346 - pointerType,
347 - });
348 - target.pointerup({pointerType});
349 - // ctrl-click
350 - target.pointerdown({
351 - button: buttonType.primary,
352 - buttons: buttonsType.primary,
353 - ctrlKey: true,
354 - pointerType,
355 - });
356 - target.pointerup({pointerType});
357 - // meta-click
358 - target.pointerdown({
359 - button: buttonType.primary,
360 - buttons: buttonsType.primary,
361 - metaKey: true,
362 - pointerType,
363 - });
364 - target.pointerup({pointerType});
365 - // shift-click
366 - target.pointerdown({
367 - button: buttonType.primary,
368 - buttons: buttonsType.primary,
369 - shiftKey: true,
370 - pointerType,
371 - });
372 - target.pointerup({pointerType});
399
374 - expect(onTapStart).toHaveBeenCalledTimes(0);
375 - });
400 + expect(onTapStart).toHaveBeenCalledTimes(0);
401 + });
402 + }
403 });
404
405 describe('onTapEnd', () => {
406 let onTapEnd, ref;
407
381 - beforeEach(() => {
408 + const componentInit = () => {
409 onTapEnd = jest.fn();
410 ref = React.createRef();
411 const Component = () => {
@@ -387,87 +414,96 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
414 };
415 ReactDOM.render(<Component />, container);
416 document.elementFromPoint = () => ref.current;
390 - });
417 + };
418
392 - testWithPointerType('pointer up', pointerType => {
393 - const target = createEventTarget(ref.current);
394 - const button = buttonType.primary;
395 - const buttons = buttonsType.primary;
396 - target.pointerdown({button, buttons, pointerType});
397 - target.pointerup({
398 - button,
399 - buttons,
400 - pageX: 10,
401 - pageY: 10,
402 - pointerType,
403 - x: 10,
404 - y: 10,
405 - });
406 - expect(onTapEnd).toHaveBeenCalledTimes(1);
407 - expect(onTapEnd).toHaveBeenCalledWith(
408 - expect.objectContaining({
409 - altKey: false,
410 - ctrlKey: false,
411 - height: pointerType === 'mouse' ? 1 : 23,
412 - metaKey: false,
419 + // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
420 + // the @gate pragma.
421 + if (__EXPERIMENTAL__) {
422 + testWithPointerType('pointer up', pointerType => {
423 + componentInit();
424 + const target = createEventTarget(ref.current);
425 + const button = buttonType.primary;
426 + const buttons = buttonsType.primary;
427 + target.pointerdown({button, buttons, pointerType});
428 + target.pointerup({
429 + button,
430 + buttons,
431 pageX: 10,
432 pageY: 10,
433 pointerType,
416 - pressure: 0,
417 - screenX: 10,
418 - screenY: 60,
419 - shiftKey: false,
420 - tangentialPressure: 0,
421 - target: target.node,
422 - tiltX: 0,
423 - tiltY: 0,
424 - timeStamp: expect.any(Number),
425 - twist: 0,
426 - type: 'tap:end',
427 - width: pointerType === 'mouse' ? 1 : 23,
434 x: 10,
435 y: 10,
430 - }),
431 - );
432 - });
433 -
434 - testWithPointerType('zero-dimension hit rect', pointerType => {
435 - const targetRef = React.createRef();
436 - const innerRef = React.createRef();
437 -
438 - const Component = () => {
439 - const listener = useTap({onTapEnd});
440 - return (
441 - <div ref={targetRef} DEPRECATED_flareListeners={listener}>
442 - <button ref={innerRef} />
443 - </div>
436 + });
437 + expect(onTapEnd).toHaveBeenCalledTimes(1);
438 + expect(onTapEnd).toHaveBeenCalledWith(
439 + expect.objectContaining({
440 + altKey: false,
441 + ctrlKey: false,
442 + height: pointerType === 'mouse' ? 1 : 23,
443 + metaKey: false,
444 + pageX: 10,
445 + pageY: 10,
446 + pointerType,
447 + pressure: 0,
448 + screenX: 10,
449 + screenY: 60,
450 + shiftKey: false,
451 + tangentialPressure: 0,
452 + target: target.node,
453 + tiltX: 0,
454 + tiltY: 0,
455 + timeStamp: expect.any(Number),
456 + twist: 0,
457 + type: 'tap:end',
458 + width: pointerType === 'mouse' ? 1 : 23,
459 + x: 10,
460 + y: 10,
461 + }),
462 );
445 - };
446 - ReactDOM.render(<Component />, container);
447 - document.elementFromPoint = () => innerRef.current;
463 + });
464
449 - const target = createEventTarget(targetRef.current);
450 - target.setBoundingClientRect({x: 0, y: 0, width: 0, height: 0});
451 - const innerTarget = createEventTarget(innerRef.current);
452 - innerTarget.pointerdown({pointerType});
453 - innerTarget.pointerup({pointerType});
454 - expect(onTapEnd).toBeCalled();
455 - });
465 + testWithPointerType('zero-dimension hit rect', pointerType => {
466 + componentInit();
467 + const targetRef = React.createRef();
468 + const innerRef = React.createRef();
469 +
470 + const Component = () => {
471 + const listener = useTap({onTapEnd});
472 + return (
473 + <div ref={targetRef} DEPRECATED_flareListeners={listener}>
474 + <button ref={innerRef} />
475 + </div>
476 + );
477 + };
478 + ReactDOM.render(<Component />, container);
479 + document.elementFromPoint = () => innerRef.current;
480 +
481 + const target = createEventTarget(targetRef.current);
482 + target.setBoundingClientRect({x: 0, y: 0, width: 0, height: 0});
483 + const innerTarget = createEventTarget(innerRef.current);
484 + innerTarget.pointerdown({pointerType});
485 + innerTarget.pointerup({pointerType});
486 + expect(onTapEnd).toBeCalled();
487 + });
488
457 - testWithPointerType('pointer up outside target', pointerType => {
458 - const downTarget = createEventTarget(ref.current);
459 - const upTarget = createEventTarget(container);
460 - tapAndReleaseOutside({
461 - hasPointerEvents,
462 - downTarget,
463 - upTarget,
464 - pointerType,
489 + testWithPointerType('pointer up outside target', pointerType => {
490 + componentInit();
491 + const downTarget = createEventTarget(ref.current);
492 + const upTarget = createEventTarget(container);
493 + tapAndReleaseOutside({
494 + hasPointerEvents,
495 + downTarget,
496 + upTarget,
497 + pointerType,
498 + });
499 + expect(onTapEnd).not.toBeCalled();
500 });
466 - expect(onTapEnd).not.toBeCalled();
467 - });
501 + }
502
503 if (hasPointerEvents) {
504 + // @gate experimental
505 test('second pointer up off target', () => {
506 + componentInit();
507 const pointerType = 'touch';
508 const target = createEventTarget(ref.current);
509 const offTarget = createEventTarget(container);
@@ -490,7 +526,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
526 });
527 }
528
529 + // @gate experimental
530 test('ignored buttons and modifiers', () => {
531 + componentInit();
532 const target = createEventTarget(ref.current);
533 // right-click
534 target.pointerdown({
@@ -553,7 +591,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
591 const rect = {x: 0, y: 0, width: 100, height: 100};
592 const coordinates = {x: 10, y: 10};
593
556 - beforeEach(() => {
594 + const componentInit = () => {
595 onTapUpdate = jest.fn();
596 ref = React.createRef();
597 const Component = () => {
@@ -562,81 +600,90 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
600 };
601 ReactDOM.render(<Component />, container);
602 document.elementFromPoint = () => ref.current;
565 - });
566 -
567 - testWithPointerType('requires activation', pointerType => {
568 - const target = createEventTarget(ref.current);
569 - target.setBoundingClientRect(rect);
570 - if (pointerType !== 'touch') {
571 - target.pointerhover({pointerType, ...coordinates});
572 - target.pointermove({pointerType, ...coordinates});
573 - }
574 - expect(onTapUpdate).not.toBeCalled();
575 - });
603 + };
604
577 - testWithPointerType('pointer move', pointerType => {
578 - const target = createEventTarget(ref.current);
579 - target.setBoundingClientRect(rect);
580 - target.pointerdown({pointerType});
605 + // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
606 + // the @gate pragma.
607 + if (__EXPERIMENTAL__) {
608 + testWithPointerType('requires activation', pointerType => {
609 + componentInit();
610 + const target = createEventTarget(ref.current);
611 + target.setBoundingClientRect(rect);
612 + if (pointerType !== 'touch') {
613 + target.pointerhover({pointerType, ...coordinates});
614 + target.pointermove({pointerType, ...coordinates});
615 + }
616 + expect(onTapUpdate).not.toBeCalled();
617 + });
618
582 - target.pointermove({pointerType, x: 10, y: 10});
583 - expect(onTapUpdate).toHaveBeenCalledWith(
584 - expect.objectContaining({
585 - altKey: false,
586 - ctrlKey: false,
587 - height: pointerType === 'mouse' ? 1 : 23,
588 - metaKey: false,
589 - pageX: 10,
590 - pageY: 10,
591 - pointerType,
592 - pressure: pointerType === 'touch' ? 1 : 0.5,
593 - screenX: 10,
594 - screenY: 60,
595 - shiftKey: false,
596 - tangentialPressure: 0,
597 - target: target.node,
598 - tiltX: 0,
599 - tiltY: 0,
600 - timeStamp: expect.any(Number),
601 - twist: 0,
602 - type: 'tap:update',
603 - width: pointerType === 'mouse' ? 1 : 23,
604 - x: 10,
605 - y: 10,
606 - }),
607 - );
619 + testWithPointerType('pointer move', pointerType => {
620 + componentInit();
621 + const target = createEventTarget(ref.current);
622 + target.setBoundingClientRect(rect);
623 + target.pointerdown({pointerType});
624 +
625 + target.pointermove({pointerType, x: 10, y: 10});
626 + expect(onTapUpdate).toHaveBeenCalledWith(
627 + expect.objectContaining({
628 + altKey: false,
629 + ctrlKey: false,
630 + height: pointerType === 'mouse' ? 1 : 23,
631 + metaKey: false,
632 + pageX: 10,
633 + pageY: 10,
634 + pointerType,
635 + pressure: pointerType === 'touch' ? 1 : 0.5,
636 + screenX: 10,
637 + screenY: 60,
638 + shiftKey: false,
639 + tangentialPressure: 0,
640 + target: target.node,
641 + tiltX: 0,
642 + tiltY: 0,
643 + timeStamp: expect.any(Number),
644 + twist: 0,
645 + type: 'tap:update',
646 + width: pointerType === 'mouse' ? 1 : 23,
647 + x: 10,
648 + y: 10,
649 + }),
650 + );
651
609 - target.pointermove({pointerType, x: 20, y: 20});
610 - expect(onTapUpdate).toHaveBeenCalledWith(
611 - expect.objectContaining({pointerType, x: 20, y: 20}),
612 - );
652 + target.pointermove({pointerType, x: 20, y: 20});
653 + expect(onTapUpdate).toHaveBeenCalledWith(
654 + expect.objectContaining({pointerType, x: 20, y: 20}),
655 + );
656
614 - expect(onTapUpdate).toHaveBeenCalledTimes(2);
615 - });
657 + expect(onTapUpdate).toHaveBeenCalledTimes(2);
658 + });
659
617 - testWithPointerType('pointer moves outside target', pointerType => {
618 - const target = createEventTarget(ref.current);
619 - const containerTarget = createEventTarget(container);
620 - target.setBoundingClientRect(rect);
621 - target.pointerdown({pointerType});
622 - target.pointermove({pointerType, x: 10, y: 10});
623 - expect(onTapUpdate).toHaveBeenCalledTimes(1);
660 + testWithPointerType('pointer moves outside target', pointerType => {
661 + componentInit();
662 + const target = createEventTarget(ref.current);
663 + const containerTarget = createEventTarget(container);
664 + target.setBoundingClientRect(rect);
665 + target.pointerdown({pointerType});
666 + target.pointermove({pointerType, x: 10, y: 10});
667 + expect(onTapUpdate).toHaveBeenCalledTimes(1);
668
625 - // NOTE: this assumes the PointerEvent implementation calls
626 - // 'releasePointerCapture' for touch pointers
627 - if (!hasPointerEvents && pointerType === 'touch') {
628 - document.elementFromPoint = () => containerTarget.node;
629 - target.pointermove({pointerType, x: 101, y: 101});
630 - } else {
631 - containerTarget.pointermove({pointerType, x: 101, y: 101});
632 - }
669 + // NOTE: this assumes the PointerEvent implementation calls
670 + // 'releasePointerCapture' for touch pointers
671 + if (!hasPointerEvents && pointerType === 'touch') {
672 + document.elementFromPoint = () => containerTarget.node;
673 + target.pointermove({pointerType, x: 101, y: 101});
674 + } else {
675 + containerTarget.pointermove({pointerType, x: 101, y: 101});
676 + }
677
634 - // No extra 'onTapUpdate' calls when the pointer is outside the target
635 - expect(onTapUpdate).toHaveBeenCalledTimes(1);
636 - });
678 + // No extra 'onTapUpdate' calls when the pointer is outside the target
679 + expect(onTapUpdate).toHaveBeenCalledTimes(1);
680 + });
681 + }
682
683 if (hasPointerEvents) {
684 + // @gate experimental
685 test('second pointer off target', () => {
686 + componentInit();
687 const pointerType = 'touch';
688 const target = createEventTarget(ref.current);
689 const offTarget = createEventTarget(container);
@@ -659,7 +706,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
706 eventsLog.push(msg);
707 };
708
662 - beforeEach(() => {
709 + const componentInit = () => {
710 eventsLog = [];
711 onTapChange = jest.fn();
712 ref = React.createRef();
@@ -677,47 +724,54 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
724 };
725 ReactDOM.render(<Component />, container);
726 document.elementFromPoint = () => ref.current;
680 - });
727 + };
728
682 - testWithPointerType('pointer down/up', pointerType => {
683 - const target = createEventTarget(ref.current);
684 - target.pointerdown({pointerType});
685 - expect(onTapChange).toHaveBeenCalledTimes(1);
686 - expect(onTapChange).toHaveBeenCalledWith(true);
687 - target.pointerup({pointerType, x: 0, y: 0});
688 - expect(onTapChange).toHaveBeenCalledTimes(2);
689 - expect(onTapChange).toHaveBeenCalledWith(false);
690 - expect(eventsLog).toEqual(['start', 'change', 'change', 'end']);
691 - });
729 + // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
730 + // the @gate pragma.
731 + if (__EXPERIMENTAL__) {
732 + testWithPointerType('pointer down/up', pointerType => {
733 + componentInit();
734 + const target = createEventTarget(ref.current);
735 + target.pointerdown({pointerType});
736 + expect(onTapChange).toHaveBeenCalledTimes(1);
737 + expect(onTapChange).toHaveBeenCalledWith(true);
738 + target.pointerup({pointerType, x: 0, y: 0});
739 + expect(onTapChange).toHaveBeenCalledTimes(2);
740 + expect(onTapChange).toHaveBeenCalledWith(false);
741 + expect(eventsLog).toEqual(['start', 'change', 'change', 'end']);
742 + });
743
693 - testWithPointerType('pointer cancel', pointerType => {
694 - const target = createEventTarget(ref.current);
695 - target.pointerdown({pointerType});
696 - expect(onTapChange).toHaveBeenCalledTimes(1);
697 - expect(onTapChange).toHaveBeenCalledWith(true);
698 - target.pointercancel({pointerType});
699 - expect(onTapChange).toHaveBeenCalledTimes(2);
700 - expect(onTapChange).toHaveBeenCalledWith(false);
701 - expect(eventsLog).toEqual(['start', 'change', 'change', 'cancel']);
702 - });
744 + testWithPointerType('pointer cancel', pointerType => {
745 + componentInit();
746 + const target = createEventTarget(ref.current);
747 + target.pointerdown({pointerType});
748 + expect(onTapChange).toHaveBeenCalledTimes(1);
749 + expect(onTapChange).toHaveBeenCalledWith(true);
750 + target.pointercancel({pointerType});
751 + expect(onTapChange).toHaveBeenCalledTimes(2);
752 + expect(onTapChange).toHaveBeenCalledWith(false);
753 + expect(eventsLog).toEqual(['start', 'change', 'change', 'cancel']);
754 + });
755
704 - testWithPointerType('pointer move outside target', pointerType => {
705 - const downTarget = createEventTarget(ref.current);
706 - const upTarget = createEventTarget(container);
707 - tapAndMoveOutside({
708 - hasPointerEvents,
709 - downTarget,
710 - upTarget,
711 - pointerType,
756 + testWithPointerType('pointer move outside target', pointerType => {
757 + componentInit();
758 + const downTarget = createEventTarget(ref.current);
759 + const upTarget = createEventTarget(container);
760 + tapAndMoveOutside({
761 + hasPointerEvents,
762 + downTarget,
763 + upTarget,
764 + pointerType,
765 + });
766 + expect(onTapChange).toHaveBeenCalledTimes(2);
767 });
713 - expect(onTapChange).toHaveBeenCalledTimes(2);
714 - });
768 + }
769 });
770
771 describe('onTapCancel', () => {
772 let onTapCancel, onTapUpdate, parentRef, ref, siblingRef;
773
720 - beforeEach(() => {
774 + const componentInit = () => {
775 onTapCancel = jest.fn();
776 onTapUpdate = jest.fn();
777 parentRef = React.createRef();
@@ -733,43 +787,50 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
787 );
788 };
789 ReactDOM.render(<Component />, container);
736 - });
790 + };
791
738 - testWithPointerType('pointer cancel', pointerType => {
739 - const target = createEventTarget(ref.current);
740 - target.pointerdown({pointerType});
741 - target.pointercancel({pointerType});
742 - expect(onTapCancel).toHaveBeenCalledTimes(1);
743 - expect(onTapCancel).toHaveBeenCalledWith(
744 - expect.objectContaining({
745 - altKey: false,
746 - ctrlKey: false,
747 - height: 1,
748 - metaKey: false,
749 - pageX: 0,
750 - pageY: 0,
751 - pointerType,
752 - pressure: 0,
753 - screenX: 0,
754 - screenY: 0,
755 - shiftKey: false,
756 - tangentialPressure: 0,
757 - target: target.node,
758 - tiltX: 0,
759 - tiltY: 0,
760 - timeStamp: expect.any(Number),
761 - twist: 0,
762 - type: 'tap:cancel',
763 - width: 1,
764 - x: 0,
765 - y: 0,
766 - }),
767 - );
768 - target.pointermove({pointerType, x: 5, y: 5});
769 - expect(onTapUpdate).not.toBeCalled();
770 - });
792 + // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
793 + // the @gate pragma.
794 + if (__EXPERIMENTAL__) {
795 + testWithPointerType('pointer cancel', pointerType => {
796 + componentInit();
797 + const target = createEventTarget(ref.current);
798 + target.pointerdown({pointerType});
799 + target.pointercancel({pointerType});
800 + expect(onTapCancel).toHaveBeenCalledTimes(1);
801 + expect(onTapCancel).toHaveBeenCalledWith(
802 + expect.objectContaining({
803 + altKey: false,
804 + ctrlKey: false,
805 + height: 1,
806 + metaKey: false,
807 + pageX: 0,
808 + pageY: 0,
809 + pointerType,
810 + pressure: 0,
811 + screenX: 0,
812 + screenY: 0,
813 + shiftKey: false,
814 + tangentialPressure: 0,
815 + target: target.node,
816 + tiltX: 0,
817 + tiltY: 0,
818 + timeStamp: expect.any(Number),
819 + twist: 0,
820 + type: 'tap:cancel',
821 + width: 1,
822 + x: 0,
823 + y: 0,
824 + }),
825 + );
826 + target.pointermove({pointerType, x: 5, y: 5});
827 + expect(onTapUpdate).not.toBeCalled();
828 + });
829 + }
830
831 + // @gate experimental
832 test('second pointer on target', () => {
833 + componentInit();
834 const pointerType = 'touch';
835 const target = createEventTarget(ref.current);
836 const button = buttonType.primary;
@@ -780,7 +841,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
841 });
842
843 if (hasPointerEvents) {
844 + // @gate experimental
845 test('second pointer off target', () => {
846 + componentInit();
847 const pointerType = 'touch';
848 const target = createEventTarget(ref.current);
849 const offTarget = createEventTarget(container);
@@ -792,19 +855,26 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
855 });
856 }
857
795 - testWithPointerType('pointer move outside target', pointerType => {
796 - const downTarget = createEventTarget(ref.current);
797 - const upTarget = createEventTarget(container);
798 - tapAndMoveOutside({
799 - hasPointerEvents,
800 - downTarget,
801 - upTarget,
802 - pointerType,
858 + // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
859 + // the @gate pragma.
860 + if (__EXPERIMENTAL__) {
861 + testWithPointerType('pointer move outside target', pointerType => {
862 + componentInit();
863 + const downTarget = createEventTarget(ref.current);
864 + const upTarget = createEventTarget(container);
865 + tapAndMoveOutside({
866 + hasPointerEvents,
867 + downTarget,
868 + upTarget,
869 + pointerType,
870 + });
871 + expect(onTapCancel).toBeCalled();
872 });
804 - expect(onTapCancel).toBeCalled();
805 - });
873 + }
874
875 + // @gate experimental
876 test('ignored modifiers', () => {
877 + componentInit();
878 const target = createEventTarget(ref.current);
879 const button = buttonType.primary;
880 const buttons = buttonsType.primary;
@@ -824,13 +894,17 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
894 expect(onTapCancel).toHaveBeenCalledTimes(4);
895 });
896
897 + // @gate experimental
898 test('long press context menu', () => {
899 + componentInit();
900 const target = createEventTarget(ref.current);
901 target.contextmenu({}, {pointerType: 'touch'});
902 expect(onTapCancel).toHaveBeenCalledTimes(1);
903 });
904
905 + // @gate experimental
906 test('parent scroll (non-mouse)', () => {
907 + componentInit();
908 const target = createEventTarget(ref.current);
909 const parentTarget = createEventTarget(parentRef.current);
910 target.pointerdown({pointerType: 'touch'});
@@ -838,7 +912,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
912 expect(onTapCancel).toHaveBeenCalledTimes(1);
913 });
914
915 + // @gate experimental
916 test('sibling scroll', () => {
917 + componentInit();
918 const target = createEventTarget(ref.current);
919 const siblingTarget = createEventTarget(siblingRef.current);
920 target.pointerdown();
@@ -846,7 +922,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
922 expect(onTapCancel).not.toBeCalled();
923 });
924
925 + // @gate experimental
926 test('document scroll (non-mouse)', () => {
927 + componentInit();
928 const target = createEventTarget(ref.current);
929 const documentTarget = createEventTarget(document);
930 target.pointerdown({pointerType: 'touch'});
@@ -855,7 +933,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
933 });
934
935 // Scroll on an element not managed by React
936 + // @gate experimental
937 test('root container scroll (non-mouse)', () => {
938 + componentInit();
939 const target = createEventTarget(ref.current);
940 const containerTarget = createEventTarget(container);
941 target.pointerdown({pointerType: 'touch'});
@@ -867,7 +947,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
947 describe('preventDefault', () => {
948 let onTapEnd, ref, innerRef, preventDefault, remount;
949
870 - beforeEach(() => {
950 + const componentInit = () => {
951 remount = function(shouldPreventDefault) {
952 onTapEnd = jest.fn();
953 preventDefault = jest.fn();
@@ -887,9 +967,11 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
967 ReactDOM.render(<Component />, container);
968 };
969 remount();
890 - });
970 + };
971
972 + // @gate experimental
973 test('prevents native behavior by default', () => {
974 + componentInit();
975 const target = createEventTarget(ref.current);
976 target.pointerdown();
977 target.pointerup({preventDefault});
@@ -899,7 +981,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
981 );
982 });
983
984 + // @gate experimental
985 test('prevents native behaviour by default (inner target)', () => {
986 + componentInit();
987 const innerTarget = createEventTarget(innerRef.current);
988 innerTarget.pointerdown();
989 innerTarget.pointerup({preventDefault});
@@ -909,7 +993,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
993 );
994 });
995
996 + // @gate experimental
997 test('allows native behaviour if false', () => {
998 + componentInit();
999 remount(false);
1000
1001 const target = createEventTarget(ref.current);
packages/react-noop-renderer/src/ReactNoopPersistent.js
+1
@@ -24,6 +24,7 @@ export const {
24 getOrCreateRootContainer,
25 createRoot,
26 createBlockingRoot,
27 + createLegacyRoot,
28 getChildrenAsJSX,
29 getPendingChildrenAsJSX,
30 createPortal,
packages/react-reconciler/src/__tests__/ReactFiberFundamental-test.internal.js
+6 -5
@@ -50,16 +50,12 @@ function initReactDOMServer() {
50 }
51
52 describe('ReactFiberFundamental', () => {
53 - if (!__EXPERIMENTAL__) {
54 - it("empty test so Jest doesn't complain", () => {});
55 - return;
56 - }
57 -
53 describe('NoopRenderer', () => {
54 beforeEach(() => {
55 initNoopRenderer();
56 });
57
58 + // @gate experimental
59 it('should render a simple fundamental component with a single child', () => {
60 const FundamentalComponent = createReactFundamentalComponent({
61 reconcileChildren: true,
@@ -94,6 +90,7 @@ describe('ReactFiberFundamental', () => {
90 initTestRenderer();
91 });
92
93 + // @gate experimental
94 it('should render a simple fundamental component with a single child', () => {
95 const FundamentalComponent = createReactFundamentalComponent({
96 reconcileChildren: true,
@@ -130,6 +127,7 @@ describe('ReactFiberFundamental', () => {
127 initReactDOM();
128 });
129
130 + // @gate experimental
131 it('should render a simple fundamental component with a single child', () => {
132 const FundamentalComponent = createReactFundamentalComponent({
133 reconcileChildren: true,
@@ -155,6 +153,7 @@ describe('ReactFiberFundamental', () => {
153 expect(container.innerHTML).toBe('');
154 });
155
156 + // @gate experimental
157 it('should render a simple fundamental component without reconcileChildren', () => {
158 const FundamentalComponent = createReactFundamentalComponent({
159 reconcileChildren: false,
@@ -186,6 +185,7 @@ describe('ReactFiberFundamental', () => {
185 initReactDOMServer();
186 });
187
188 + // @gate experimental
189 it('should render a simple fundamental component with a single child', () => {
190 const getInstance = jest.fn();
191 const FundamentalComponent = createReactFundamentalComponent({
@@ -210,6 +210,7 @@ describe('ReactFiberFundamental', () => {
210 expect(output).toBe('<div>Hello world again</div>');
211 });
212
213 + // @gate experimental
214 it('should render a simple fundamental component without reconcileChildren', () => {
215 const FundamentalComponent = createReactFundamentalComponent({
216 reconcileChildren: false,
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+332 -336
@@ -1121,384 +1121,380 @@ describe('ReactHooksWithNoopRenderer', () => {
1121 },
1122 );
1123
1124 - if (
1125 - require('shared/ReactFeatureFlags')
1126 - .deferPassiveEffectCleanupDuringUnmount &&
1127 - require('shared/ReactFeatureFlags')
1128 - .runAllPassiveEffectDestroysBeforeCreates
1129 - ) {
1130 - it('defers passive effect destroy functions during unmount', () => {
1131 - function Child({bar, foo}) {
1132 - React.useEffect(() => {
1133 - Scheduler.unstable_yieldValue('passive bar create');
1134 - return () => {
1135 - Scheduler.unstable_yieldValue('passive bar destroy');
1136 - };
1137 - }, [bar]);
1138 - React.useLayoutEffect(() => {
1139 - Scheduler.unstable_yieldValue('layout bar create');
1140 - return () => {
1141 - Scheduler.unstable_yieldValue('layout bar destroy');
1142 - };
1143 - }, [bar]);
1144 - React.useEffect(() => {
1145 - Scheduler.unstable_yieldValue('passive foo create');
1146 - return () => {
1147 - Scheduler.unstable_yieldValue('passive foo destroy');
1148 - };
1149 - }, [foo]);
1150 - React.useLayoutEffect(() => {
1151 - Scheduler.unstable_yieldValue('layout foo create');
1152 - return () => {
1153 - Scheduler.unstable_yieldValue('layout foo destroy');
1154 - };
1155 - }, [foo]);
1156 - Scheduler.unstable_yieldValue('render');
1157 - return null;
1158 - }
1124 + // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates
1125 + it('defers passive effect destroy functions during unmount', () => {
1126 + function Child({bar, foo}) {
1127 + React.useEffect(() => {
1128 + Scheduler.unstable_yieldValue('passive bar create');
1129 + return () => {
1130 + Scheduler.unstable_yieldValue('passive bar destroy');
1131 + };
1132 + }, [bar]);
1133 + React.useLayoutEffect(() => {
1134 + Scheduler.unstable_yieldValue('layout bar create');
1135 + return () => {
1136 + Scheduler.unstable_yieldValue('layout bar destroy');
1137 + };
1138 + }, [bar]);
1139 + React.useEffect(() => {
1140 + Scheduler.unstable_yieldValue('passive foo create');
1141 + return () => {
1142 + Scheduler.unstable_yieldValue('passive foo destroy');
1143 + };
1144 + }, [foo]);
1145 + React.useLayoutEffect(() => {
1146 + Scheduler.unstable_yieldValue('layout foo create');
1147 + return () => {
1148 + Scheduler.unstable_yieldValue('layout foo destroy');
1149 + };
1150 + }, [foo]);
1151 + Scheduler.unstable_yieldValue('render');
1152 + return null;
1153 + }
1154
1160 - act(() => {
1161 - ReactNoop.render(<Child bar={1} foo={1} />, () =>
1162 - Scheduler.unstable_yieldValue('Sync effect'),
1163 - );
1164 - expect(Scheduler).toFlushAndYieldThrough([
1165 - 'render',
1166 - 'layout bar create',
1167 - 'layout foo create',
1168 - 'Sync effect',
1169 - ]);
1170 - // Effects are deferred until after the commit
1171 - expect(Scheduler).toFlushAndYield([
1172 - 'passive bar create',
1173 - 'passive foo create',
1174 - ]);
1175 - });
1155 + act(() => {
1156 + ReactNoop.render(<Child bar={1} foo={1} />, () =>
1157 + Scheduler.unstable_yieldValue('Sync effect'),
1158 + );
1159 + expect(Scheduler).toFlushAndYieldThrough([
1160 + 'render',
1161 + 'layout bar create',
1162 + 'layout foo create',
1163 + 'Sync effect',
1164 + ]);
1165 + // Effects are deferred until after the commit
1166 + expect(Scheduler).toFlushAndYield([
1167 + 'passive bar create',
1168 + 'passive foo create',
1169 + ]);
1170 + });
1171
1177 - // This update is exists to test an internal implementation detail:
1178 - // Effects without updating dependencies lose their layout/passive tag during an update.
1179 - act(() => {
1180 - ReactNoop.render(<Child bar={1} foo={2} />, () =>
1181 - Scheduler.unstable_yieldValue('Sync effect'),
1182 - );
1183 - expect(Scheduler).toFlushAndYieldThrough([
1184 - 'render',
1185 - 'layout foo destroy',
1186 - 'layout foo create',
1187 - 'Sync effect',
1188 - ]);
1189 - // Effects are deferred until after the commit
1190 - expect(Scheduler).toFlushAndYield([
1191 - 'passive foo destroy',
1192 - 'passive foo create',
1193 - ]);
1194 - });
1172 + // This update is exists to test an internal implementation detail:
1173 + // Effects without updating dependencies lose their layout/passive tag during an update.
1174 + act(() => {
1175 + ReactNoop.render(<Child bar={1} foo={2} />, () =>
1176 + Scheduler.unstable_yieldValue('Sync effect'),
1177 + );
1178 + expect(Scheduler).toFlushAndYieldThrough([
1179 + 'render',
1180 + 'layout foo destroy',
1181 + 'layout foo create',
1182 + 'Sync effect',
1183 + ]);
1184 + // Effects are deferred until after the commit
1185 + expect(Scheduler).toFlushAndYield([
1186 + 'passive foo destroy',
1187 + 'passive foo create',
1188 + ]);
1189 + });
1190
1196 - // Unmount the component and verify that passive destroy functions are deferred until post-commit.
1197 - act(() => {
1198 - ReactNoop.render(null, () =>
1199 - Scheduler.unstable_yieldValue('Sync effect'),
1200 - );
1201 - expect(Scheduler).toFlushAndYieldThrough([
1202 - 'layout bar destroy',
1203 - 'layout foo destroy',
1204 - 'Sync effect',
1205 - ]);
1206 - // Effects are deferred until after the commit
1207 - expect(Scheduler).toFlushAndYield([
1208 - 'passive bar destroy',
1209 - 'passive foo destroy',
1210 - ]);
1211 - });
1191 + // Unmount the component and verify that passive destroy functions are deferred until post-commit.
1192 + act(() => {
1193 + ReactNoop.render(null, () =>
1194 + Scheduler.unstable_yieldValue('Sync effect'),
1195 + );
1196 + expect(Scheduler).toFlushAndYieldThrough([
1197 + 'layout bar destroy',
1198 + 'layout foo destroy',
1199 + 'Sync effect',
1200 + ]);
1201 + // Effects are deferred until after the commit
1202 + expect(Scheduler).toFlushAndYield([
1203 + 'passive bar destroy',
1204 + 'passive foo destroy',
1205 + ]);
1206 });
1207 + });
1208
1214 - it('does not warn about state updates for unmounted components with pending passive unmounts', () => {
1215 - let completePendingRequest = null;
1216 - function Component() {
1217 - Scheduler.unstable_yieldValue('Component');
1218 - const [didLoad, setDidLoad] = React.useState(false);
1219 - React.useLayoutEffect(() => {
1220 - Scheduler.unstable_yieldValue('layout create');
1221 - return () => {
1222 - Scheduler.unstable_yieldValue('layout destroy');
1223 - };
1224 - }, []);
1225 - React.useEffect(() => {
1226 - Scheduler.unstable_yieldValue('passive create');
1227 - // Mimic an XHR request with a complete handler that updates state.
1228 - completePendingRequest = () => setDidLoad(true);
1229 - return () => {
1230 - Scheduler.unstable_yieldValue('passive destroy');
1231 - };
1232 - }, []);
1233 - return didLoad;
1234 - }
1209 + // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates
1210 + it('does not warn about state updates for unmounted components with pending passive unmounts', () => {
1211 + let completePendingRequest = null;
1212 + function Component() {
1213 + Scheduler.unstable_yieldValue('Component');
1214 + const [didLoad, setDidLoad] = React.useState(false);
1215 + React.useLayoutEffect(() => {
1216 + Scheduler.unstable_yieldValue('layout create');
1217 + return () => {
1218 + Scheduler.unstable_yieldValue('layout destroy');
1219 + };
1220 + }, []);
1221 + React.useEffect(() => {
1222 + Scheduler.unstable_yieldValue('passive create');
1223 + // Mimic an XHR request with a complete handler that updates state.
1224 + completePendingRequest = () => setDidLoad(true);
1225 + return () => {
1226 + Scheduler.unstable_yieldValue('passive destroy');
1227 + };
1228 + }, []);
1229 + return didLoad;
1230 + }
1231
1236 - act(() => {
1237 - ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1238 - Scheduler.unstable_yieldValue('Sync effect'),
1239 - );
1240 - expect(Scheduler).toFlushAndYieldThrough([
1241 - 'Component',
1242 - 'layout create',
1243 - 'Sync effect',
1244 - ]);
1245 - ReactNoop.flushPassiveEffects();
1246 - expect(Scheduler).toHaveYielded(['passive create']);
1232 + act(() => {
1233 + ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1234 + Scheduler.unstable_yieldValue('Sync effect'),
1235 + );
1236 + expect(Scheduler).toFlushAndYieldThrough([
1237 + 'Component',
1238 + 'layout create',
1239 + 'Sync effect',
1240 + ]);
1241 + ReactNoop.flushPassiveEffects();
1242 + expect(Scheduler).toHaveYielded(['passive create']);
1243
1248 - // Unmount but don't process pending passive destroy function
1249 - ReactNoop.unmountRootWithID('root');
1250 - expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
1244 + // Unmount but don't process pending passive destroy function
1245 + ReactNoop.unmountRootWithID('root');
1246 + expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
1247
1252 - // Simulate an XHR completing, which will cause a state update-
1253 - // but should not log a warning.
1254 - completePendingRequest();
1248 + // Simulate an XHR completing, which will cause a state update-
1249 + // but should not log a warning.
1250 + completePendingRequest();
1251
1256 - ReactNoop.flushPassiveEffects();
1257 - expect(Scheduler).toHaveYielded(['passive destroy']);
1258 - });
1252 + ReactNoop.flushPassiveEffects();
1253 + expect(Scheduler).toHaveYielded(['passive destroy']);
1254 });
1255 + });
1256
1261 - it('still warns about state updates for unmounted components with no pending passive unmounts', () => {
1262 - let completePendingRequest = null;
1263 - function Component() {
1264 - Scheduler.unstable_yieldValue('Component');
1265 - const [didLoad, setDidLoad] = React.useState(false);
1266 - React.useLayoutEffect(() => {
1267 - Scheduler.unstable_yieldValue('layout create');
1268 - // Mimic an XHR request with a complete handler that updates state.
1269 - completePendingRequest = () => setDidLoad(true);
1270 - return () => {
1271 - Scheduler.unstable_yieldValue('layout destroy');
1272 - };
1273 - }, []);
1274 - return didLoad;
1275 - }
1257 + it('warns about state updates for unmounted components with no pending passive unmounts', () => {
1258 + let completePendingRequest = null;
1259 + function Component() {
1260 + Scheduler.unstable_yieldValue('Component');
1261 + const [didLoad, setDidLoad] = React.useState(false);
1262 + React.useLayoutEffect(() => {
1263 + Scheduler.unstable_yieldValue('layout create');
1264 + // Mimic an XHR request with a complete handler that updates state.
1265 + completePendingRequest = () => setDidLoad(true);
1266 + return () => {
1267 + Scheduler.unstable_yieldValue('layout destroy');
1268 + };
1269 + }, []);
1270 + return didLoad;
1271 + }
1272
1277 - act(() => {
1278 - ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1279 - Scheduler.unstable_yieldValue('Sync effect'),
1280 - );
1281 - expect(Scheduler).toFlushAndYieldThrough([
1282 - 'Component',
1283 - 'layout create',
1284 - 'Sync effect',
1285 - ]);
1273 + act(() => {
1274 + ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1275 + Scheduler.unstable_yieldValue('Sync effect'),
1276 + );
1277 + expect(Scheduler).toFlushAndYieldThrough([
1278 + 'Component',
1279 + 'layout create',
1280 + 'Sync effect',
1281 + ]);
1282
1287 - // Unmount but don't process pending passive destroy function
1288 - ReactNoop.unmountRootWithID('root');
1289 - expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
1283 + // Unmount but don't process pending passive destroy function
1284 + ReactNoop.unmountRootWithID('root');
1285 + expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
1286
1291 - // Simulate an XHR completing.
1292 - expect(completePendingRequest).toErrorDev(
1293 - "Warning: Can't perform a React state update on an unmounted component.",
1294 - );
1295 - });
1287 + // Simulate an XHR completing.
1288 + expect(completePendingRequest).toErrorDev(
1289 + "Warning: Can't perform a React state update on an unmounted component.",
1290 + );
1291 });
1292 + });
1293
1298 - it('still warns if there are pending passive unmount effects but not for the current fiber', () => {
1299 - let completePendingRequest = null;
1300 - function ComponentWithXHR() {
1301 - Scheduler.unstable_yieldValue('Component');
1302 - const [didLoad, setDidLoad] = React.useState(false);
1303 - React.useLayoutEffect(() => {
1304 - Scheduler.unstable_yieldValue('a:layout create');
1305 - return () => {
1306 - Scheduler.unstable_yieldValue('a:layout destroy');
1307 - };
1308 - }, []);
1309 - React.useEffect(() => {
1310 - Scheduler.unstable_yieldValue('a:passive create');
1311 - // Mimic an XHR request with a complete handler that updates state.
1312 - completePendingRequest = () => setDidLoad(true);
1313 - }, []);
1314 - return didLoad;
1315 - }
1294 + // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates
1295 + it('still warns if there are pending passive unmount effects but not for the current fiber', () => {
1296 + let completePendingRequest = null;
1297 + function ComponentWithXHR() {
1298 + Scheduler.unstable_yieldValue('Component');
1299 + const [didLoad, setDidLoad] = React.useState(false);
1300 + React.useLayoutEffect(() => {
1301 + Scheduler.unstable_yieldValue('a:layout create');
1302 + return () => {
1303 + Scheduler.unstable_yieldValue('a:layout destroy');
1304 + };
1305 + }, []);
1306 + React.useEffect(() => {
1307 + Scheduler.unstable_yieldValue('a:passive create');
1308 + // Mimic an XHR request with a complete handler that updates state.
1309 + completePendingRequest = () => setDidLoad(true);
1310 + }, []);
1311 + return didLoad;
1312 + }
1313
1317 - function ComponentWithPendingPassiveUnmount() {
1318 - React.useEffect(() => {
1319 - Scheduler.unstable_yieldValue('b:passive create');
1320 - return () => {
1321 - Scheduler.unstable_yieldValue('b:passive destroy');
1322 - };
1323 - }, []);
1324 - return null;
1325 - }
1314 + function ComponentWithPendingPassiveUnmount() {
1315 + React.useEffect(() => {
1316 + Scheduler.unstable_yieldValue('b:passive create');
1317 + return () => {
1318 + Scheduler.unstable_yieldValue('b:passive destroy');
1319 + };
1320 + }, []);
1321 + return null;
1322 + }
1323
1327 - act(() => {
1328 - ReactNoop.renderToRootWithID(
1329 - <>
1330 - <ComponentWithXHR />
1331 - <ComponentWithPendingPassiveUnmount />
1332 - </>,
1333 - 'root',
1334 - () => Scheduler.unstable_yieldValue('Sync effect'),
1335 - );
1336 - expect(Scheduler).toFlushAndYieldThrough([
1337 - 'Component',
1338 - 'a:layout create',
1339 - 'Sync effect',
1340 - ]);
1341 - ReactNoop.flushPassiveEffects();
1342 - expect(Scheduler).toHaveYielded([
1343 - 'a:passive create',
1344 - 'b:passive create',
1345 - ]);
1324 + act(() => {
1325 + ReactNoop.renderToRootWithID(
1326 + <>
1327 + <ComponentWithXHR />
1328 + <ComponentWithPendingPassiveUnmount />
1329 + </>,
1330 + 'root',
1331 + () => Scheduler.unstable_yieldValue('Sync effect'),
1332 + );
1333 + expect(Scheduler).toFlushAndYieldThrough([
1334 + 'Component',
1335 + 'a:layout create',
1336 + 'Sync effect',
1337 + ]);
1338 + ReactNoop.flushPassiveEffects();
1339 + expect(Scheduler).toHaveYielded([
1340 + 'a:passive create',
1341 + 'b:passive create',
1342 + ]);
1343
1347 - // Unmount but don't process pending passive destroy function
1348 - ReactNoop.unmountRootWithID('root');
1349 - expect(Scheduler).toFlushAndYieldThrough(['a:layout destroy']);
1344 + // Unmount but don't process pending passive destroy function
1345 + ReactNoop.unmountRootWithID('root');
1346 + expect(Scheduler).toFlushAndYieldThrough(['a:layout destroy']);
1347
1351 - // Simulate an XHR completing in the component without a pending passive effect..
1352 - expect(completePendingRequest).toErrorDev(
1353 - "Warning: Can't perform a React state update on an unmounted component.",
1354 - );
1355 - });
1348 + // Simulate an XHR completing in the component without a pending passive effect..
1349 + expect(completePendingRequest).toErrorDev(
1350 + "Warning: Can't perform a React state update on an unmounted component.",
1351 + );
1352 });
1353 + });
1354
1358 - it('still warns if there are updates after pending passive unmount effects have been flushed', () => {
1359 - let updaterFunction;
1355 + it('warns if there are updates after pending passive unmount effects have been flushed', () => {
1356 + let updaterFunction;
1357
1361 - function Component() {
1362 - Scheduler.unstable_yieldValue('Component');
1363 - const [state, setState] = React.useState(false);
1364 - updaterFunction = setState;
1365 - React.useEffect(() => {
1366 - Scheduler.unstable_yieldValue('passive create');
1367 - return () => {
1368 - Scheduler.unstable_yieldValue('passive destroy');
1369 - };
1370 - }, []);
1371 - return state;
1372 - }
1358 + function Component() {
1359 + Scheduler.unstable_yieldValue('Component');
1360 + const [state, setState] = React.useState(false);
1361 + updaterFunction = setState;
1362 + React.useEffect(() => {
1363 + Scheduler.unstable_yieldValue('passive create');
1364 + return () => {
1365 + Scheduler.unstable_yieldValue('passive destroy');
1366 + };
1367 + }, []);
1368 + return state;
1369 + }
1370
1374 - act(() => {
1375 - ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1376 - Scheduler.unstable_yieldValue('Sync effect'),
1377 - );
1378 - });
1379 - expect(Scheduler).toHaveYielded([
1371 + act(() => {
1372 + ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1373 + Scheduler.unstable_yieldValue('Sync effect'),
1374 + );
1375 + });
1376 + expect(Scheduler).toHaveYielded([
1377 + 'Component',
1378 + 'Sync effect',
1379 + 'passive create',
1380 + ]);
1381 +
1382 + ReactNoop.unmountRootWithID('root');
1383 + expect(Scheduler).toFlushAndYield(['passive destroy']);
1384 +
1385 + act(() => {
1386 + expect(() => {
1387 + updaterFunction(true);
1388 + }).toErrorDev(
1389 + "Warning: Can't perform a React state update on an unmounted component. " +
1390 + 'This is a no-op, but it indicates a memory leak in your application. ' +
1391 + 'To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.\n' +
1392 + ' in Component (at **)',
1393 + );
1394 + });
1395 + });
1396 +
1397 + it('does not show a warning when a component updates its own state from within passive unmount function', () => {
1398 + function Component() {
1399 + Scheduler.unstable_yieldValue('Component');
1400 + const [didLoad, setDidLoad] = React.useState(false);
1401 + React.useEffect(() => {
1402 + Scheduler.unstable_yieldValue('passive create');
1403 + return () => {
1404 + setDidLoad(true);
1405 + Scheduler.unstable_yieldValue('passive destroy');
1406 + };
1407 + }, []);
1408 + return didLoad;
1409 + }
1410 +
1411 + act(() => {
1412 + ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1413 + Scheduler.unstable_yieldValue('Sync effect'),
1414 + );
1415 + expect(Scheduler).toFlushAndYieldThrough([
1416 'Component',
1417 'Sync effect',
1418 'passive create',
1419 ]);
1420
1421 + // Unmount but don't process pending passive destroy function
1422 ReactNoop.unmountRootWithID('root');
1423 expect(Scheduler).toFlushAndYield(['passive destroy']);
1387 -
1388 - act(() => {
1389 - expect(() => {
1390 - updaterFunction(true);
1391 - }).toErrorDev(
1392 - "Warning: Can't perform a React state update on an unmounted component. " +
1393 - 'This is a no-op, but it indicates a memory leak in your application. ' +
1394 - 'To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.\n' +
1395 - ' in Component (at **)',
1396 - );
1397 - });
1398 - });
1399 -
1400 - it('does not show a warning when a component updates its own state from within passive unmount function', () => {
1401 - function Component() {
1402 - Scheduler.unstable_yieldValue('Component');
1403 - const [didLoad, setDidLoad] = React.useState(false);
1404 - React.useEffect(() => {
1405 - Scheduler.unstable_yieldValue('passive create');
1406 - return () => {
1407 - setDidLoad(true);
1408 - Scheduler.unstable_yieldValue('passive destroy');
1409 - };
1410 - }, []);
1411 - return didLoad;
1412 - }
1413 -
1414 - act(() => {
1415 - ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1416 - Scheduler.unstable_yieldValue('Sync effect'),
1417 - );
1418 - expect(Scheduler).toFlushAndYieldThrough([
1419 - 'Component',
1420 - 'Sync effect',
1421 - 'passive create',
1422 - ]);
1423 -
1424 - // Unmount but don't process pending passive destroy function
1425 - ReactNoop.unmountRootWithID('root');
1426 - expect(Scheduler).toFlushAndYield(['passive destroy']);
1427 - });
1424 });
1425 + });
1426
1430 - it('does not show a warning when a component updates a childs state from within passive unmount function', () => {
1431 - function Parent() {
1432 - Scheduler.unstable_yieldValue('Parent');
1433 - const updaterRef = React.useRef(null);
1434 - React.useEffect(() => {
1435 - Scheduler.unstable_yieldValue('Parent passive create');
1436 - return () => {
1437 - updaterRef.current(true);
1438 - Scheduler.unstable_yieldValue('Parent passive destroy');
1439 - };
1440 - }, []);
1441 - return <Child updaterRef={updaterRef} />;
1442 - }
1427 + it('does not show a warning when a component updates a childs state from within passive unmount function', () => {
1428 + function Parent() {
1429 + Scheduler.unstable_yieldValue('Parent');
1430 + const updaterRef = React.useRef(null);
1431 + React.useEffect(() => {
1432 + Scheduler.unstable_yieldValue('Parent passive create');
1433 + return () => {
1434 + updaterRef.current(true);
1435 + Scheduler.unstable_yieldValue('Parent passive destroy');
1436 + };
1437 + }, []);
1438 + return <Child updaterRef={updaterRef} />;
1439 + }
1440
1444 - function Child({updaterRef}) {
1445 - Scheduler.unstable_yieldValue('Child');
1446 - const [state, setState] = React.useState(false);
1447 - React.useEffect(() => {
1448 - Scheduler.unstable_yieldValue('Child passive create');
1449 - updaterRef.current = setState;
1450 - }, []);
1451 - return state;
1452 - }
1441 + function Child({updaterRef}) {
1442 + Scheduler.unstable_yieldValue('Child');
1443 + const [state, setState] = React.useState(false);
1444 + React.useEffect(() => {
1445 + Scheduler.unstable_yieldValue('Child passive create');
1446 + updaterRef.current = setState;
1447 + }, []);
1448 + return state;
1449 + }
1450
1454 - act(() => {
1455 - ReactNoop.renderToRootWithID(<Parent />, 'root');
1456 - expect(Scheduler).toFlushAndYieldThrough([
1457 - 'Parent',
1458 - 'Child',
1459 - 'Child passive create',
1460 - 'Parent passive create',
1461 - ]);
1451 + act(() => {
1452 + ReactNoop.renderToRootWithID(<Parent />, 'root');
1453 + expect(Scheduler).toFlushAndYieldThrough([
1454 + 'Parent',
1455 + 'Child',
1456 + 'Child passive create',
1457 + 'Parent passive create',
1458 + ]);
1459
1463 - // Unmount but don't process pending passive destroy function
1464 - ReactNoop.unmountRootWithID('root');
1465 - expect(Scheduler).toFlushAndYield(['Parent passive destroy']);
1466 - });
1460 + // Unmount but don't process pending passive destroy function
1461 + ReactNoop.unmountRootWithID('root');
1462 + expect(Scheduler).toFlushAndYield(['Parent passive destroy']);
1463 });
1464 + });
1465
1469 - it('does not show a warning when a component updates a parents state from within passive unmount function', () => {
1470 - function Parent() {
1471 - const [state, setState] = React.useState(false);
1472 - Scheduler.unstable_yieldValue('Parent');
1473 - return <Child setState={setState} state={state} />;
1474 - }
1466 + it('does not show a warning when a component updates a parents state from within passive unmount function', () => {
1467 + function Parent() {
1468 + const [state, setState] = React.useState(false);
1469 + Scheduler.unstable_yieldValue('Parent');
1470 + return <Child setState={setState} state={state} />;
1471 + }
1472
1476 - function Child({setState, state}) {
1477 - Scheduler.unstable_yieldValue('Child');
1478 - React.useEffect(() => {
1479 - Scheduler.unstable_yieldValue('Child passive create');
1480 - return () => {
1481 - Scheduler.unstable_yieldValue('Child passive destroy');
1482 - setState(true);
1483 - };
1484 - }, []);
1485 - return state;
1486 - }
1473 + function Child({setState, state}) {
1474 + Scheduler.unstable_yieldValue('Child');
1475 + React.useEffect(() => {
1476 + Scheduler.unstable_yieldValue('Child passive create');
1477 + return () => {
1478 + Scheduler.unstable_yieldValue('Child passive destroy');
1479 + setState(true);
1480 + };
1481 + }, []);
1482 + return state;
1483 + }
1484
1488 - act(() => {
1489 - ReactNoop.renderToRootWithID(<Parent />, 'root');
1490 - expect(Scheduler).toFlushAndYieldThrough([
1491 - 'Parent',
1492 - 'Child',
1493 - 'Child passive create',
1494 - ]);
1485 + act(() => {
1486 + ReactNoop.renderToRootWithID(<Parent />, 'root');
1487 + expect(Scheduler).toFlushAndYieldThrough([
1488 + 'Parent',
1489 + 'Child',
1490 + 'Child passive create',
1491 + ]);
1492
1496 - // Unmount but don't process pending passive destroy function
1497 - ReactNoop.unmountRootWithID('root');
1498 - expect(Scheduler).toFlushAndYield(['Child passive destroy']);
1499 - });
1493 + // Unmount but don't process pending passive destroy function
1494 + ReactNoop.unmountRootWithID('root');
1495 + expect(Scheduler).toFlushAndYield(['Child passive destroy']);
1496 });
1501 - }
1497 + });
1498
1499 it('updates have async priority', () => {
1500 function Counter(props) {
packages/react-reconciler/src/__tests__/ReactScope-test.internal.js
+11 -5
@@ -26,11 +26,6 @@ describe('ReactScope', () => {
26 Scheduler = require('scheduler');
27 });
28
29 - if (!__EXPERIMENTAL__) {
30 - it("empty test so Jest doesn't complain", () => {});
31 - return;
32 - }
33 -
29 describe('ReactDOM', () => {
30 let ReactDOM;
31 let container;
@@ -47,6 +42,7 @@ describe('ReactScope', () => {
42 container = null;
43 });
44
45 + // @gate experimental
46 it('DO_NOT_USE_queryAllNodes() works as intended', () => {
47 const testScopeQuery = (type, props) => true;
48 const TestScope = React.unstable_createScope();
@@ -81,6 +77,7 @@ describe('ReactScope', () => {
77 expect(scopeRef.current).toBe(null);
78 });
79
80 + // @gate experimental
81 it('DO_NOT_USE_queryAllNodes() provides the correct host instance', () => {
82 const testScopeQuery = (type, props) => type === 'div';
83 const TestScope = React.unstable_createScope();
@@ -127,6 +124,7 @@ describe('ReactScope', () => {
124 expect(scopeRef.current).toBe(null);
125 });
126
127 + // @gate experimental
128 it('DO_NOT_USE_queryFirstNode() works as intended', () => {
129 const testScopeQuery = (type, props) => true;
130 const TestScope = React.unstable_createScope();
@@ -161,6 +159,7 @@ describe('ReactScope', () => {
159 expect(scopeRef.current).toBe(null);
160 });
161
162 + // @gate experimental
163 it('containsNode() works as intended', () => {
164 const TestScope = React.unstable_createScope();
165 const scopeRef = React.createRef();
@@ -210,6 +209,7 @@ describe('ReactScope', () => {
209 expect(scopeRef.current.containsNode(emRef.current)).toBe(false);
210 });
211
212 + // @gate experimental
213 it('scopes support server-side rendering and hydration', () => {
214 const TestScope = React.unstable_createScope();
215 const scopeRef = React.createRef();
@@ -240,6 +240,7 @@ describe('ReactScope', () => {
240 expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]);
241 });
242
243 + // @gate experimental
244 it('event responders can be attached to scopes', () => {
245 let onKeyDown = jest.fn();
246 const TestScope = React.unstable_createScope();
@@ -282,6 +283,7 @@ describe('ReactScope', () => {
283 expect(onKeyDown).toHaveBeenCalledTimes(1);
284 });
285
286 + // @gate experimental
287 it('getChildContextValues() works as intended', () => {
288 const TestContext = React.createContext();
289 const TestScope = React.unstable_createScope();
@@ -310,6 +312,7 @@ describe('ReactScope', () => {
312 expect(scopeRef.current).toBe(null);
313 });
314
315 + // @gate experimental
316 it('correctly works with suspended boundaries that are hydrated', async () => {
317 let suspend = false;
318 let resolve;
@@ -384,6 +387,7 @@ describe('ReactScope', () => {
387 ReactTestRenderer = require('react-test-renderer');
388 });
389
390 + // @gate experimental
391 it('DO_NOT_USE_queryAllNodes() works as intended', () => {
392 const testScopeQuery = (type, props) => true;
393 const TestScope = React.unstable_createScope();
@@ -420,6 +424,7 @@ describe('ReactScope', () => {
424 expect(nodes).toEqual([aRef.current, divRef.current, spanRef.current]);
425 });
426
427 + // @gate experimental
428 it('DO_NOT_USE_queryFirstNode() works as intended', () => {
429 const testScopeQuery = (type, props) => true;
430 const TestScope = React.unstable_createScope();
@@ -456,6 +461,7 @@ describe('ReactScope', () => {
461 expect(node).toEqual(aRef.current);
462 });
463
464 + // @gate experimental
465 it('containsNode() works as intended', () => {
466 const TestScope = React.unstable_createScope();
467 const scopeRef = React.createRef();
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js
+35 -5
@@ -6,11 +6,6 @@ let Suspense;
6 let SuspenseList;
7
8 describe('ReactSuspenseList', () => {
9 - if (!__EXPERIMENTAL__) {
10 - it("empty test so Jest doesn't complain", () => {});
11 - return;
12 - }
13 -
9 beforeEach(() => {
10 jest.resetModules();
11 ReactFeatureFlags = require('shared/ReactFeatureFlags');
@@ -47,6 +42,7 @@ describe('ReactSuspenseList', () => {
42 return Component;
43 }
44
45 + // @gate experimental
46 it('warns if an unsupported revealOrder option is used', () => {
47 function Foo() {
48 return (
@@ -66,6 +62,7 @@ describe('ReactSuspenseList', () => {
62 ]);
63 });
64
65 + // @gate experimental
66 it('warns if a upper case revealOrder option is used', () => {
67 function Foo() {
68 return (
@@ -85,6 +82,7 @@ describe('ReactSuspenseList', () => {
82 ]);
83 });
84
85 + // @gate experimental
86 it('warns if a misspelled revealOrder option is used', () => {
87 function Foo() {
88 return (
@@ -105,6 +103,7 @@ describe('ReactSuspenseList', () => {
103 ]);
104 });
105
106 + // @gate experimental
107 it('warns if a single element is passed to a "forwards" list', () => {
108 function Foo({children}) {
109 return <SuspenseList revealOrder="forwards">{children}</SuspenseList>;
@@ -137,6 +136,7 @@ describe('ReactSuspenseList', () => {
136 ]);
137 });
138
139 + // @gate experimental
140 it('warns if a single fragment is passed to a "backwards" list', () => {
141 function Foo() {
142 return (
@@ -157,6 +157,7 @@ describe('ReactSuspenseList', () => {
157 ]);
158 });
159
160 + // @gate experimental
161 it('warns if a nested array is passed to a "forwards" list', () => {
162 function Foo({items}) {
163 return (
@@ -184,6 +185,7 @@ describe('ReactSuspenseList', () => {
185 ]);
186 });
187
188 + // @gate experimental
189 it('shows content independently by default', async () => {
190 const A = createAsyncText('A');
191 const B = createAsyncText('B');
@@ -250,6 +252,7 @@ describe('ReactSuspenseList', () => {
252 );
253 });
254
255 + // @gate experimental
256 it('shows content independently in legacy mode regardless of option', async () => {
257 const A = createAsyncText('A');
258 const B = createAsyncText('B');
@@ -322,6 +325,7 @@ describe('ReactSuspenseList', () => {
325 );
326 });
327
328 + // @gate experimental
329 it('displays all "together"', async () => {
330 const A = createAsyncText('A');
331 const B = createAsyncText('B');
@@ -391,6 +395,7 @@ describe('ReactSuspenseList', () => {
395 );
396 });
397
398 + // @gate experimental
399 it('displays all "together" even when nested as siblings', async () => {
400 const A = createAsyncText('A');
401 const B = createAsyncText('B');
@@ -476,6 +481,7 @@ describe('ReactSuspenseList', () => {
481 );
482 });
483
484 + // @gate experimental
485 it('displays all "together" in nested SuspenseLists', async () => {
486 const A = createAsyncText('A');
487 const B = createAsyncText('B');
@@ -537,6 +543,7 @@ describe('ReactSuspenseList', () => {
543 );
544 });
545
546 + // @gate experimental
547 it('displays all "together" in nested SuspenseLists where the inner is default', async () => {
548 const A = createAsyncText('A');
549 const B = createAsyncText('B');
@@ -596,6 +603,7 @@ describe('ReactSuspenseList', () => {
603 );
604 });
605
606 + // @gate experimental
607 it('displays all "together" during an update', async () => {
608 const A = createAsyncText('A');
609 const B = createAsyncText('B');
@@ -680,6 +688,7 @@ describe('ReactSuspenseList', () => {
688 );
689 });
690
691 + // @gate experimental
692 it('avoided boundaries can be coordinate with SuspenseList', async () => {
693 const A = createAsyncText('A');
694 const B = createAsyncText('B');
@@ -778,6 +787,7 @@ describe('ReactSuspenseList', () => {
787 );
788 });
789
790 + // @gate experimental
791 it('displays each items in "forwards" order', async () => {
792 const A = createAsyncText('A');
793 const B = createAsyncText('B');
@@ -843,6 +853,7 @@ describe('ReactSuspenseList', () => {
853 );
854 });
855
856 + // @gate experimental
857 it('displays each items in "backwards" order', async () => {
858 const A = createAsyncText('A');
859 const B = createAsyncText('B');
@@ -908,6 +919,7 @@ describe('ReactSuspenseList', () => {
919 );
920 });
921
922 + // @gate experimental
923 it('displays added row at the top "together" and the bottom in "forwards" order', async () => {
924 const A = createAsyncText('A');
925 const B = createAsyncText('B');
@@ -1062,6 +1074,7 @@ describe('ReactSuspenseList', () => {
1074 );
1075 });
1076
1077 + // @gate experimental
1078 it('displays added row at the top "together" and the bottom in "backwards" order', async () => {
1079 const A = createAsyncText('A');
1080 const B = createAsyncText('B');
@@ -1246,6 +1259,7 @@ describe('ReactSuspenseList', () => {
1259 );
1260 });
1261
1262 + // @gate experimental
1263 it('switches to rendering fallbacks if the tail takes long CPU time', async () => {
1264 function Foo() {
1265 return (
@@ -1308,6 +1322,7 @@ describe('ReactSuspenseList', () => {
1322 );
1323 });
1324
1325 + // @gate experimental
1326 it('only shows one loading state at a time for "collapsed" tail insertions', async () => {
1327 const A = createAsyncText('A');
1328 const B = createAsyncText('B');
@@ -1377,6 +1392,7 @@ describe('ReactSuspenseList', () => {
1392 );
1393 });
1394
1395 + // @gate experimental
1396 it('warns if an unsupported tail option is used', () => {
1397 function Foo() {
1398 return (
@@ -1397,6 +1413,7 @@ describe('ReactSuspenseList', () => {
1413 ]);
1414 });
1415
1416 + // @gate experimental
1417 it('warns if a tail option is used with "together"', () => {
1418 function Foo() {
1419 return (
@@ -1417,6 +1434,7 @@ describe('ReactSuspenseList', () => {
1434 ]);
1435 });
1436
1437 + // @gate experimental
1438 it('renders one "collapsed" fallback even if CPU time elapsed', async () => {
1439 function Foo() {
1440 return (
@@ -1483,6 +1501,7 @@ describe('ReactSuspenseList', () => {
1501 );
1502 });
1503
1504 + // @gate experimental
1505 it('adding to the middle does not collapse insertions (forwards)', async () => {
1506 const A = createAsyncText('A');
1507 const B = createAsyncText('B');
@@ -1625,6 +1644,7 @@ describe('ReactSuspenseList', () => {
1644 );
1645 });
1646
1647 + // @gate experimental
1648 it('adding to the middle does not collapse insertions (backwards)', async () => {
1649 const A = createAsyncText('A');
1650 const B = createAsyncText('B');
@@ -1772,6 +1792,7 @@ describe('ReactSuspenseList', () => {
1792 );
1793 });
1794
1795 + // @gate experimental
1796 it('adding to the middle of committed tail does not collapse insertions', async () => {
1797 const A = createAsyncText('A');
1798 const B = createAsyncText('B');
@@ -1929,6 +1950,7 @@ describe('ReactSuspenseList', () => {
1950 );
1951 });
1952
1953 + // @gate experimental
1954 it('only shows no initial loading state "hidden" tail insertions', async () => {
1955 const A = createAsyncText('A');
1956 const B = createAsyncText('B');
@@ -1992,6 +2014,7 @@ describe('ReactSuspenseList', () => {
2014 );
2015 });
2016
2017 + // @gate experimental
2018 it('eventually resolves a nested forwards suspense list', async () => {
2019 const B = createAsyncText('B');
2020
@@ -2054,6 +2077,7 @@ describe('ReactSuspenseList', () => {
2077 );
2078 });
2079
2080 + // @gate experimental
2081 it('eventually resolves a nested forwards suspense list with a hidden tail', async () => {
2082 const B = createAsyncText('B');
2083
@@ -2100,6 +2124,7 @@ describe('ReactSuspenseList', () => {
2124 );
2125 });
2126
2127 + // @gate experimental
2128 it('eventually resolves two nested forwards suspense lists with a hidden tail', async () => {
2129 const B = createAsyncText('B');
2130
@@ -2167,6 +2192,7 @@ describe('ReactSuspenseList', () => {
2192 );
2193 });
2194
2195 + // @gate experimental
2196 it('can do unrelated adjacent updates', async () => {
2197 let updateAdjacent;
2198 function Adjacent() {
@@ -2213,6 +2239,7 @@ describe('ReactSuspenseList', () => {
2239 );
2240 });
2241
2242 + // @gate experimental
2243 it('is able to re-suspend the last rows during an update with hidden', async () => {
2244 const AsyncB = createAsyncText('B');
2245
@@ -2301,6 +2328,7 @@ describe('ReactSuspenseList', () => {
2328 expect(previousInst).toBe(setAsyncB);
2329 });
2330
2331 + // @gate experimental
2332 it('is able to re-suspend the last rows during an update with hidden', async () => {
2333 const AsyncB = createAsyncText('B');
2334
@@ -2389,6 +2417,7 @@ describe('ReactSuspenseList', () => {
2417 expect(previousInst).toBe(setAsyncB);
2418 });
2419
2420 + // @gate experimental
2421 it('is able to interrupt a partially rendered tree and continue later', async () => {
2422 const AsyncA = createAsyncText('A');
2423
@@ -2486,6 +2515,7 @@ describe('ReactSuspenseList', () => {
2515 );
2516 });
2517
2518 + // @gate experimental
2519 it('can resume class components when revealed together', async () => {
2520 const A = createAsyncText('A');
2521 const B = createAsyncText('B');
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+11 -5
@@ -11,11 +11,6 @@ let resolveText;
11 let rejectText;
12
13 describe('ReactSuspenseWithNoopRenderer', () => {
14 - if (!__EXPERIMENTAL__) {
15 - it("empty test so Jest doesn't complain", () => {});
16 - return;
17 - }
18 -
14 beforeEach(() => {
15 jest.resetModules();
16
@@ -922,6 +917,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
917 expect(ReactNoop.getChildren()).toEqual([span('Async')]);
918 });
919
920 + // @gate experimental
921 it('starts working on an update even if its priority falls between two suspended levels', async () => {
922 function App(props) {
923 return (
@@ -2331,6 +2327,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2327 timeoutMs: 2000,
2328 };
2329
2330 + // @gate experimental
2331 it('top level render', async () => {
2332 function App({page}) {
2333 return (
@@ -2385,6 +2382,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2382 expect(ReactNoop.getChildren()).toEqual([span('B')]);
2383 });
2384
2385 + // @gate experimental
2386 it('hooks', async () => {
2387 let transitionToPage;
2388 function App() {
@@ -2452,6 +2450,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2450 expect(ReactNoop.getChildren()).toEqual([span('B')]);
2451 });
2452
2453 + // @gate experimental
2454 it('classes', async () => {
2455 let transitionToPage;
2456 class App extends React.Component {
@@ -2523,6 +2522,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2522 });
2523 });
2524
2525 + // @gate experimental
2526 it('disables suspense config when nothing is passed to withSuspenseConfig', async () => {
2527 function App({page}) {
2528 return (
@@ -2597,6 +2597,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2597 ]);
2598 });
2599
2600 + // @gate experimental
2601 it('withSuspenseConfig timeout applies when we use an updated avoided boundary', async () => {
2602 function App({page}) {
2603 return (
@@ -2645,6 +2646,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2646 ]);
2647 });
2648
2649 + // @gate experimental
2650 it('withSuspenseConfig timeout applies when we use a newly created avoided boundary', async () => {
2651 function App({page}) {
2652 return (
@@ -2692,6 +2694,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2694 ]);
2695 });
2696
2697 + // @gate experimental
2698 it('supports delaying a busy spinner from disappearing', async () => {
2699 const SUSPENSE_CONFIG = {
2700 timeoutMs: 10000,
@@ -2854,6 +2857,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2857 expect(root).toMatchRenderedOutput(<span prop="Foo" />);
2858 });
2859
2860 + // @gate experimental
2861 it('should not render hidden content while suspended on higher pri', async () => {
2862 function Offscreen() {
2863 Scheduler.unstable_yieldValue('Offscreen');
@@ -2908,6 +2912,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2912 );
2913 });
2914
2915 + // @gate experimental
2916 it('should be able to unblock higher pri content before suspended hidden', async () => {
2917 function Offscreen() {
2918 Scheduler.unstable_yieldValue('Offscreen');
@@ -3624,6 +3629,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3629 );
3630 });
3631
3632 + // @gate experimental
3633 it('regression: ping at high priority causes update to be dropped', async () => {
3634 const {useState, useTransition} = React;
3635
packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
+1345 -1337
@@ -140,339 +140,300 @@ describe('useMutableSource', () => {
140 return <div>{`${label}:${snapshot}`}</div>;
141 }
142
143 - if (!__EXPERIMENTAL__) {
144 - it("empty test so Jest doesn't complain", () => {});
145 - } else {
146 - beforeEach(loadModules);
143 + beforeEach(loadModules);
144
148 - it('should subscribe to a source and schedule updates when it changes', () => {
149 - const source = createSource('one');
150 - const mutableSource = createMutableSource(source);
145 + // @gate experimental
146 + it('should subscribe to a source and schedule updates when it changes', () => {
147 + const source = createSource('one');
148 + const mutableSource = createMutableSource(source);
149
152 - act(() => {
153 - ReactNoop.renderToRootWithID(
154 - <>
155 - <Component
156 - label="a"
157 - getSnapshot={defaultGetSnapshot}
158 - mutableSource={mutableSource}
159 - subscribe={defaultSubscribe}
160 - />
161 - <Component
162 - label="b"
163 - getSnapshot={defaultGetSnapshot}
164 - mutableSource={mutableSource}
165 - subscribe={defaultSubscribe}
166 - />
167 - </>,
168 - 'root',
169 - () => Scheduler.unstable_yieldValue('Sync effect'),
170 - );
171 - expect(Scheduler).toFlushAndYieldThrough([
172 - 'a:one',
173 - 'b:one',
174 - 'Sync effect',
175 - ]);
176 -
177 - // Subscriptions should be passive
178 - expect(source.listenerCount).toBe(0);
179 - ReactNoop.flushPassiveEffects();
180 - expect(source.listenerCount).toBe(2);
181 -
182 - // Changing values should schedule an update with React
183 - source.value = 'two';
184 - expect(Scheduler).toFlushAndYieldThrough(['a:two', 'b:two']);
185 -
186 - // Umounting a component should remove its subscriptino.
187 - ReactNoop.renderToRootWithID(
188 - <>
189 - <Component
190 - label="a"
191 - getSnapshot={defaultGetSnapshot}
192 - mutableSource={mutableSource}
193 - subscribe={defaultSubscribe}
194 - />
195 - </>,
196 - 'root',
197 - () => Scheduler.unstable_yieldValue('Sync effect'),
198 - );
199 - expect(Scheduler).toFlushAndYield(['a:two', 'Sync effect']);
200 - ReactNoop.flushPassiveEffects();
201 - expect(source.listenerCount).toBe(1);
202 -
203 - // Umounting a root should remove the remaining event listeners
204 - ReactNoop.unmountRootWithID('root');
205 - expect(Scheduler).toFlushAndYield([]);
206 - ReactNoop.flushPassiveEffects();
207 - expect(source.listenerCount).toBe(0);
208 -
209 - // Changes to source should not trigger an updates or warnings.
210 - source.value = 'three';
211 - expect(Scheduler).toFlushAndYield([]);
212 - });
213 - });
214 -
215 - it('should restart work if a new source is mutated during render', () => {
216 - const source = createSource('one');
217 - const mutableSource = createMutableSource(source);
218 -
219 - act(() => {
220 - ReactNoop.render(
221 - <>
222 - <Component
223 - label="a"
224 - getSnapshot={defaultGetSnapshot}
225 - mutableSource={mutableSource}
226 - subscribe={defaultSubscribe}
227 - />
228 - <Component
229 - label="b"
230 - getSnapshot={defaultGetSnapshot}
231 - mutableSource={mutableSource}
232 - subscribe={defaultSubscribe}
233 - />
234 - </>,
235 - () => Scheduler.unstable_yieldValue('Sync effect'),
236 - );
237 -
238 - // Do enough work to read from one component
239 - expect(Scheduler).toFlushAndYieldThrough(['a:one']);
240 -
241 - // Mutate source before continuing work
242 - source.value = 'two';
243 -
244 - // Render work should restart and the updated value should be used
245 - expect(Scheduler).toFlushAndYield(['a:two', 'b:two', 'Sync effect']);
246 - });
247 - });
150 + act(() => {
151 + ReactNoop.renderToRootWithID(
152 + <>
153 + <Component
154 + label="a"
155 + getSnapshot={defaultGetSnapshot}
156 + mutableSource={mutableSource}
157 + subscribe={defaultSubscribe}
158 + />
159 + <Component
160 + label="b"
161 + getSnapshot={defaultGetSnapshot}
162 + mutableSource={mutableSource}
163 + subscribe={defaultSubscribe}
164 + />
165 + </>,
166 + 'root',
167 + () => Scheduler.unstable_yieldValue('Sync effect'),
168 + );
169 + expect(Scheduler).toFlushAndYieldThrough([
170 + 'a:one',
171 + 'b:one',
172 + 'Sync effect',
173 + ]);
174
249 - it('should schedule an update if a new source is mutated between render and commit (subscription)', () => {
250 - const source = createSource('one');
251 - const mutableSource = createMutableSource(source);
175 + // Subscriptions should be passive
176 + expect(source.listenerCount).toBe(0);
177 + ReactNoop.flushPassiveEffects();
178 + expect(source.listenerCount).toBe(2);
179
253 - act(() => {
254 - ReactNoop.render(
255 - <>
256 - <Component
257 - label="a"
258 - getSnapshot={defaultGetSnapshot}
259 - mutableSource={mutableSource}
260 - subscribe={defaultSubscribe}
261 - />
262 - <Component
263 - label="b"
264 - getSnapshot={defaultGetSnapshot}
265 - mutableSource={mutableSource}
266 - subscribe={defaultSubscribe}
267 - />
268 - </>,
269 - () => Scheduler.unstable_yieldValue('Sync effect'),
270 - );
180 + // Changing values should schedule an update with React
181 + source.value = 'two';
182 + expect(Scheduler).toFlushAndYieldThrough(['a:two', 'b:two']);
183
272 - // Finish rendering
273 - expect(Scheduler).toFlushAndYieldThrough([
274 - 'a:one',
275 - 'b:one',
276 - 'Sync effect',
277 - ]);
184 + // Umounting a component should remove its subscriptino.
185 + ReactNoop.renderToRootWithID(
186 + <>
187 + <Component
188 + label="a"
189 + getSnapshot={defaultGetSnapshot}
190 + mutableSource={mutableSource}
191 + subscribe={defaultSubscribe}
192 + />
193 + </>,
194 + 'root',
195 + () => Scheduler.unstable_yieldValue('Sync effect'),
196 + );
197 + expect(Scheduler).toFlushAndYield(['a:two', 'Sync effect']);
198 + ReactNoop.flushPassiveEffects();
199 + expect(source.listenerCount).toBe(1);
200
279 - // Mutate source before subscriptions are attached
280 - expect(source.listenerCount).toBe(0);
281 - source.value = 'two';
201 + // Umounting a root should remove the remaining event listeners
202 + ReactNoop.unmountRootWithID('root');
203 + expect(Scheduler).toFlushAndYield([]);
204 + ReactNoop.flushPassiveEffects();
205 + expect(source.listenerCount).toBe(0);
206
283 - // Mutation should be detected, and a new render should be scheduled
284 - expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
285 - });
207 + // Changes to source should not trigger an updates or warnings.
208 + source.value = 'three';
209 + expect(Scheduler).toFlushAndYield([]);
210 });
211 + });
212
288 - it('should unsubscribe and resubscribe if a new source is used', () => {
289 - const sourceA = createSource('a-one');
290 - const mutableSourceA = createMutableSource(sourceA);
213 + // @gate experimental
214 + it('should restart work if a new source is mutated during render', () => {
215 + const source = createSource('one');
216 + const mutableSource = createMutableSource(source);
217
292 - const sourceB = createSource('b-one');
293 - const mutableSourceB = createMutableSource(sourceB);
294 -
295 - act(() => {
296 - ReactNoop.render(
218 + act(() => {
219 + ReactNoop.render(
220 + <>
221 <Component
298 - label="only"
222 + label="a"
223 getSnapshot={defaultGetSnapshot}
300 - mutableSource={mutableSourceA}
224 + mutableSource={mutableSource}
225 subscribe={defaultSubscribe}
302 - />,
303 - () => Scheduler.unstable_yieldValue('Sync effect'),
304 - );
305 - expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
306 - ReactNoop.flushPassiveEffects();
307 - expect(sourceA.listenerCount).toBe(1);
308 -
309 - // Changing values should schedule an update with React
310 - sourceA.value = 'a-two';
311 - expect(Scheduler).toFlushAndYield(['only:a-two']);
312 -
313 - // If we re-render with a new source, the old one should be unsubscribed.
314 - ReactNoop.render(
226 + />
227 <Component
316 - label="only"
228 + label="b"
229 getSnapshot={defaultGetSnapshot}
318 - mutableSource={mutableSourceB}
230 + mutableSource={mutableSource}
231 subscribe={defaultSubscribe}
320 - />,
321 - () => Scheduler.unstable_yieldValue('Sync effect'),
322 - );
323 - expect(Scheduler).toFlushAndYield(['only:b-one', 'Sync effect']);
324 - ReactNoop.flushPassiveEffects();
325 - expect(sourceA.listenerCount).toBe(0);
326 - expect(sourceB.listenerCount).toBe(1);
327 -
328 - // Changing to original source should not schedule updates with React
329 - sourceA.value = 'a-three';
330 - expect(Scheduler).toFlushAndYield([]);
331 -
332 - // Changing new source value should schedule an update with React
333 - sourceB.value = 'b-two';
334 - expect(Scheduler).toFlushAndYield(['only:b-two']);
335 - });
336 - });
232 + />
233 + </>,
234 + () => Scheduler.unstable_yieldValue('Sync effect'),
235 + );
236
338 - it('should unsubscribe and resubscribe if a new subscribe function is provided', () => {
339 - const source = createSource('a-one');
340 - const mutableSource = createMutableSource(source);
237 + // Do enough work to read from one component
238 + expect(Scheduler).toFlushAndYieldThrough(['a:one']);
239
342 - const unsubscribeA = jest.fn();
343 - const subscribeA = jest.fn(s => {
344 - const unsubscribe = defaultSubscribe(s);
345 - return () => {
346 - unsubscribe();
347 - unsubscribeA();
348 - };
349 - });
350 - const unsubscribeB = jest.fn();
351 - const subscribeB = jest.fn(s => {
352 - const unsubscribe = defaultSubscribe(s);
353 - return () => {
354 - unsubscribe();
355 - unsubscribeB();
356 - };
357 - });
240 + // Mutate source before continuing work
241 + source.value = 'two';
242 +
243 + // Render work should restart and the updated value should be used
244 + expect(Scheduler).toFlushAndYield(['a:two', 'b:two', 'Sync effect']);
245 + });
246 + });
247 +
248 + // @gate experimental
249 + it('should schedule an update if a new source is mutated between render and commit (subscription)', () => {
250 + const source = createSource('one');
251 + const mutableSource = createMutableSource(source);
252
359 - act(() => {
360 - ReactNoop.renderToRootWithID(
253 + act(() => {
254 + ReactNoop.render(
255 + <>
256 <Component
362 - label="only"
257 + label="a"
258 getSnapshot={defaultGetSnapshot}
259 mutableSource={mutableSource}
365 - subscribe={subscribeA}
366 - />,
367 - 'root',
368 - () => Scheduler.unstable_yieldValue('Sync effect'),
369 - );
370 - expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
371 - ReactNoop.flushPassiveEffects();
372 - expect(source.listenerCount).toBe(1);
373 - expect(subscribeA).toHaveBeenCalledTimes(1);
374 -
375 - // If we re-render with a new subscription function,
376 - // the old unsubscribe function should be called.
377 - ReactNoop.renderToRootWithID(
260 + subscribe={defaultSubscribe}
261 + />
262 <Component
379 - label="only"
263 + label="b"
264 getSnapshot={defaultGetSnapshot}
265 mutableSource={mutableSource}
382 - subscribe={subscribeB}
383 - />,
384 - 'root',
385 - () => Scheduler.unstable_yieldValue('Sync effect'),
386 - );
387 - expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
388 - ReactNoop.flushPassiveEffects();
389 - expect(source.listenerCount).toBe(1);
390 - expect(unsubscribeA).toHaveBeenCalledTimes(1);
391 - expect(subscribeB).toHaveBeenCalledTimes(1);
392 -
393 - // Unmounting should call the newer unsunscribe.
394 - ReactNoop.unmountRootWithID('root');
395 - expect(Scheduler).toFlushAndYield([]);
396 - ReactNoop.flushPassiveEffects();
397 - expect(source.listenerCount).toBe(0);
398 - expect(unsubscribeB).toHaveBeenCalledTimes(1);
399 - });
400 - });
266 + subscribe={defaultSubscribe}
267 + />
268 + </>,
269 + () => Scheduler.unstable_yieldValue('Sync effect'),
270 + );
271 +
272 + // Finish rendering
273 + expect(Scheduler).toFlushAndYieldThrough([
274 + 'a:one',
275 + 'b:one',
276 + 'Sync effect',
277 + ]);
278
402 - it('should re-use previously read snapshot value when reading is unsafe', () => {
403 - const source = createSource('one');
404 - const mutableSource = createMutableSource(source);
279 + // Mutate source before subscriptions are attached
280 + expect(source.listenerCount).toBe(0);
281 + source.value = 'two';
282
406 - act(() => {
407 - ReactNoop.render(
408 - <>
409 - <Component
410 - label="a"
411 - getSnapshot={defaultGetSnapshot}
412 - mutableSource={mutableSource}
413 - subscribe={defaultSubscribe}
414 - />
415 - <Component
416 - label="b"
417 - getSnapshot={defaultGetSnapshot}
418 - mutableSource={mutableSource}
419 - subscribe={defaultSubscribe}
420 - />
421 - </>,
422 - () => Scheduler.unstable_yieldValue('Sync effect'),
423 - );
424 - expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
283 + // Mutation should be detected, and a new render should be scheduled
284 + expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
285 + });
286 + });
287 +
288 + // @gate experimental
289 + it('should unsubscribe and resubscribe if a new source is used', () => {
290 + const sourceA = createSource('a-one');
291 + const mutableSourceA = createMutableSource(sourceA);
292 +
293 + const sourceB = createSource('b-one');
294 + const mutableSourceB = createMutableSource(sourceB);
295 +
296 + act(() => {
297 + ReactNoop.render(
298 + <Component
299 + label="only"
300 + getSnapshot={defaultGetSnapshot}
301 + mutableSource={mutableSourceA}
302 + subscribe={defaultSubscribe}
303 + />,
304 + () => Scheduler.unstable_yieldValue('Sync effect'),
305 + );
306 + expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
307 + ReactNoop.flushPassiveEffects();
308 + expect(sourceA.listenerCount).toBe(1);
309 +
310 + // Changing values should schedule an update with React
311 + sourceA.value = 'a-two';
312 + expect(Scheduler).toFlushAndYield(['only:a-two']);
313 +
314 + // If we re-render with a new source, the old one should be unsubscribed.
315 + ReactNoop.render(
316 + <Component
317 + label="only"
318 + getSnapshot={defaultGetSnapshot}
319 + mutableSource={mutableSourceB}
320 + subscribe={defaultSubscribe}
321 + />,
322 + () => Scheduler.unstable_yieldValue('Sync effect'),
323 + );
324 + expect(Scheduler).toFlushAndYield(['only:b-one', 'Sync effect']);
325 + ReactNoop.flushPassiveEffects();
326 + expect(sourceA.listenerCount).toBe(0);
327 + expect(sourceB.listenerCount).toBe(1);
328 +
329 + // Changing to original source should not schedule updates with React
330 + sourceA.value = 'a-three';
331 + expect(Scheduler).toFlushAndYield([]);
332 +
333 + // Changing new source value should schedule an update with React
334 + sourceB.value = 'b-two';
335 + expect(Scheduler).toFlushAndYield(['only:b-two']);
336 + });
337 + });
338
426 - // Changing values should schedule an update with React.
427 - // Start working on this update but don't finish it.
428 - source.value = 'two';
429 - expect(Scheduler).toFlushAndYieldThrough(['a:two']);
339 + // @gate experimental
340 + it('should unsubscribe and resubscribe if a new subscribe function is provided', () => {
341 + const source = createSource('a-one');
342 + const mutableSource = createMutableSource(source);
343
431 - // Re-renders that occur before the udpate is processed
432 - // should reuse snapshot so long as the config has not changed
433 - ReactNoop.flushSync(() => {
434 - ReactNoop.render(
435 - <>
436 - <Component
437 - label="a"
438 - getSnapshot={defaultGetSnapshot}
439 - mutableSource={mutableSource}
440 - subscribe={defaultSubscribe}
441 - />
442 - <Component
443 - label="b"
444 - getSnapshot={defaultGetSnapshot}
445 - mutableSource={mutableSource}
446 - subscribe={defaultSubscribe}
447 - />
448 - </>,
449 - () => Scheduler.unstable_yieldValue('Sync effect'),
450 - );
451 - });
452 - expect(Scheduler).toHaveYielded(['a:one', 'b:one', 'Sync effect']);
453 -
454 - expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
455 - });
344 + const unsubscribeA = jest.fn();
345 + const subscribeA = jest.fn(s => {
346 + const unsubscribe = defaultSubscribe(s);
347 + return () => {
348 + unsubscribe();
349 + unsubscribeA();
350 + };
351 + });
352 + const unsubscribeB = jest.fn();
353 + const subscribeB = jest.fn(s => {
354 + const unsubscribe = defaultSubscribe(s);
355 + return () => {
356 + unsubscribe();
357 + unsubscribeB();
358 + };
359 });
360
458 - it('should read from source on newly mounted subtree if no pending updates are scheduled for source', () => {
459 - const source = createSource('one');
460 - const mutableSource = createMutableSource(source);
361 + act(() => {
362 + ReactNoop.renderToRootWithID(
363 + <Component
364 + label="only"
365 + getSnapshot={defaultGetSnapshot}
366 + mutableSource={mutableSource}
367 + subscribe={subscribeA}
368 + />,
369 + 'root',
370 + () => Scheduler.unstable_yieldValue('Sync effect'),
371 + );
372 + expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
373 + ReactNoop.flushPassiveEffects();
374 + expect(source.listenerCount).toBe(1);
375 + expect(subscribeA).toHaveBeenCalledTimes(1);
376 +
377 + // If we re-render with a new subscription function,
378 + // the old unsubscribe function should be called.
379 + ReactNoop.renderToRootWithID(
380 + <Component
381 + label="only"
382 + getSnapshot={defaultGetSnapshot}
383 + mutableSource={mutableSource}
384 + subscribe={subscribeB}
385 + />,
386 + 'root',
387 + () => Scheduler.unstable_yieldValue('Sync effect'),
388 + );
389 + expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
390 + ReactNoop.flushPassiveEffects();
391 + expect(source.listenerCount).toBe(1);
392 + expect(unsubscribeA).toHaveBeenCalledTimes(1);
393 + expect(subscribeB).toHaveBeenCalledTimes(1);
394 +
395 + // Unmounting should call the newer unsunscribe.
396 + ReactNoop.unmountRootWithID('root');
397 + expect(Scheduler).toFlushAndYield([]);
398 + ReactNoop.flushPassiveEffects();
399 + expect(source.listenerCount).toBe(0);
400 + expect(unsubscribeB).toHaveBeenCalledTimes(1);
401 + });
402 + });
403
462 - act(() => {
463 - ReactNoop.render(
464 - <>
465 - <Component
466 - label="a"
467 - getSnapshot={defaultGetSnapshot}
468 - mutableSource={mutableSource}
469 - subscribe={defaultSubscribe}
470 - />
471 - </>,
472 - () => Scheduler.unstable_yieldValue('Sync effect'),
473 - );
474 - expect(Scheduler).toFlushAndYield(['a:one', 'Sync effect']);
404 + // @gate experimental
405 + it('should re-use previously read snapshot value when reading is unsafe', () => {
406 + const source = createSource('one');
407 + const mutableSource = createMutableSource(source);
408
409 + act(() => {
410 + ReactNoop.render(
411 + <>
412 + <Component
413 + label="a"
414 + getSnapshot={defaultGetSnapshot}
415 + mutableSource={mutableSource}
416 + subscribe={defaultSubscribe}
417 + />
418 + <Component
419 + label="b"
420 + getSnapshot={defaultGetSnapshot}
421 + mutableSource={mutableSource}
422 + subscribe={defaultSubscribe}
423 + />
424 + </>,
425 + () => Scheduler.unstable_yieldValue('Sync effect'),
426 + );
427 + expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
428 +
429 + // Changing values should schedule an update with React.
430 + // Start working on this update but don't finish it.
431 + source.value = 'two';
432 + expect(Scheduler).toFlushAndYieldThrough(['a:two']);
433 +
434 + // Re-renders that occur before the udpate is processed
435 + // should reuse snapshot so long as the config has not changed
436 + ReactNoop.flushSync(() => {
437 ReactNoop.render(
438 <>
439 <Component
@@ -490,119 +451,93 @@ describe('useMutableSource', () => {
451 </>,
452 () => Scheduler.unstable_yieldValue('Sync effect'),
453 );
493 - expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
454 });
495 - });
455 + expect(Scheduler).toHaveYielded(['a:one', 'b:one', 'Sync effect']);
456
497 - it('should throw and restart render if source and snapshot are unavailable during an update', () => {
498 - const source = createSource('one');
499 - const mutableSource = createMutableSource(source);
457 + expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
458 + });
459 + });
460
501 - act(() => {
502 - ReactNoop.render(
503 - <>
504 - <Component
505 - label="a"
506 - getSnapshot={defaultGetSnapshot}
507 - mutableSource={mutableSource}
508 - subscribe={defaultSubscribe}
509 - />
510 - <Component
511 - label="b"
512 - getSnapshot={defaultGetSnapshot}
513 - mutableSource={mutableSource}
514 - subscribe={defaultSubscribe}
515 - />
516 - </>,
517 - () => Scheduler.unstable_yieldValue('Sync effect'),
518 - );
519 - expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
520 - ReactNoop.flushPassiveEffects();
521 -
522 - // Changing values should schedule an update with React.
523 - // Start working on this update but don't finish it.
524 - Scheduler.unstable_runWithPriority(
525 - Scheduler.unstable_LowPriority,
526 - () => {
527 - source.value = 'two';
528 - expect(Scheduler).toFlushAndYieldThrough(['a:two']);
529 - },
530 - );
461 + // @gate experimental
462 + it('should read from source on newly mounted subtree if no pending updates are scheduled for source', () => {
463 + const source = createSource('one');
464 + const mutableSource = createMutableSource(source);
465
532 - const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
466 + act(() => {
467 + ReactNoop.render(
468 + <>
469 + <Component
470 + label="a"
471 + getSnapshot={defaultGetSnapshot}
472 + mutableSource={mutableSource}
473 + subscribe={defaultSubscribe}
474 + />
475 + </>,
476 + () => Scheduler.unstable_yieldValue('Sync effect'),
477 + );
478 + expect(Scheduler).toFlushAndYield(['a:one', 'Sync effect']);
479
534 - // Force a higher priority render with a new config.
535 - // This should signal that the snapshot is not safe and trigger a full re-render.
536 - Scheduler.unstable_runWithPriority(
537 - Scheduler.unstable_UserBlockingPriority,
538 - () => {
539 - ReactNoop.render(
540 - <>
541 - <Component
542 - label="a"
543 - getSnapshot={newGetSnapshot}
544 - mutableSource={mutableSource}
545 - subscribe={defaultSubscribe}
546 - />
547 - <Component
548 - label="b"
549 - getSnapshot={newGetSnapshot}
550 - mutableSource={mutableSource}
551 - subscribe={defaultSubscribe}
552 - />
553 - </>,
554 - () => Scheduler.unstable_yieldValue('Sync effect'),
555 - );
556 - },
557 - );
558 - expect(Scheduler).toFlushAndYieldThrough([
559 - 'a:new:two',
560 - 'b:new:two',
561 - 'Sync effect',
562 - ]);
563 - });
480 + ReactNoop.render(
481 + <>
482 + <Component
483 + label="a"
484 + getSnapshot={defaultGetSnapshot}
485 + mutableSource={mutableSource}
486 + subscribe={defaultSubscribe}
487 + />
488 + <Component
489 + label="b"
490 + getSnapshot={defaultGetSnapshot}
491 + mutableSource={mutableSource}
492 + subscribe={defaultSubscribe}
493 + />
494 + </>,
495 + () => Scheduler.unstable_yieldValue('Sync effect'),
496 + );
497 + expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
498 });
499 + });
500
566 - it('should throw and restart render if source and snapshot are unavailable during a sync update', () => {
567 - const source = createSource('one');
568 - const mutableSource = createMutableSource(source);
501 + // @gate experimental
502 + it('should throw and restart render if source and snapshot are unavailable during an update', () => {
503 + const source = createSource('one');
504 + const mutableSource = createMutableSource(source);
505
570 - act(() => {
571 - ReactNoop.render(
572 - <>
573 - <Component
574 - label="a"
575 - getSnapshot={defaultGetSnapshot}
576 - mutableSource={mutableSource}
577 - subscribe={defaultSubscribe}
578 - />
579 - <Component
580 - label="b"
581 - getSnapshot={defaultGetSnapshot}
582 - mutableSource={mutableSource}
583 - subscribe={defaultSubscribe}
584 - />
585 - </>,
586 - () => Scheduler.unstable_yieldValue('Sync effect'),
587 - );
588 - expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
589 - ReactNoop.flushPassiveEffects();
590 -
591 - // Changing values should schedule an update with React.
592 - // Start working on this update but don't finish it.
593 - Scheduler.unstable_runWithPriority(
594 - Scheduler.unstable_LowPriority,
595 - () => {
596 - source.value = 'two';
597 - expect(Scheduler).toFlushAndYieldThrough(['a:two']);
598 - },
599 - );
506 + act(() => {
507 + ReactNoop.render(
508 + <>
509 + <Component
510 + label="a"
511 + getSnapshot={defaultGetSnapshot}
512 + mutableSource={mutableSource}
513 + subscribe={defaultSubscribe}
514 + />
515 + <Component
516 + label="b"
517 + getSnapshot={defaultGetSnapshot}
518 + mutableSource={mutableSource}
519 + subscribe={defaultSubscribe}
520 + />
521 + </>,
522 + () => Scheduler.unstable_yieldValue('Sync effect'),
523 + );
524 + expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
525 + ReactNoop.flushPassiveEffects();
526 +
527 + // Changing values should schedule an update with React.
528 + // Start working on this update but don't finish it.
529 + Scheduler.unstable_runWithPriority(Scheduler.unstable_LowPriority, () => {
530 + source.value = 'two';
531 + expect(Scheduler).toFlushAndYieldThrough(['a:two']);
532 + });
533
601 - const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
534 + const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
535
603 - // Force a higher priority render with a new config.
604 - // This should signal that the snapshot is not safe and trigger a full re-render.
605 - ReactNoop.flushSync(() => {
536 + // Force a higher priority render with a new config.
537 + // This should signal that the snapshot is not safe and trigger a full re-render.
538 + Scheduler.unstable_runWithPriority(
539 + Scheduler.unstable_UserBlockingPriority,
540 + () => {
541 ReactNoop.render(
542 <>
543 <Component
@@ -620,1099 +555,1172 @@ describe('useMutableSource', () => {
555 </>,
556 () => Scheduler.unstable_yieldValue('Sync effect'),
557 );
623 - });
624 - expect(Scheduler).toHaveYielded([
625 - 'a:new:two',
626 - 'b:new:two',
627 - 'Sync effect',
628 - ]);
629 - });
630 - });
631 -
632 - it('should only update components whose subscriptions fire', () => {
633 - const source = createComplexSource('a:one', 'b:one');
634 - const mutableSource = createMutableSource(source);
635 -
636 - // Subscribe to part of the store.
637 - const getSnapshotA = s => s.valueA;
638 - const subscribeA = (s, callback) => s.subscribeA(callback);
639 - const getSnapshotB = s => s.valueB;
640 - const subscribeB = (s, callback) => s.subscribeB(callback);
641 -
642 - act(() => {
643 - ReactNoop.render(
644 - <>
645 - <Component
646 - label="a"
647 - getSnapshot={getSnapshotA}
648 - mutableSource={mutableSource}
649 - subscribe={subscribeA}
650 - />
651 - <Component
652 - label="b"
653 - getSnapshot={getSnapshotB}
654 - mutableSource={mutableSource}
655 - subscribe={subscribeB}
656 - />
657 - </>,
658 - () => Scheduler.unstable_yieldValue('Sync effect'),
659 - );
660 - expect(Scheduler).toFlushAndYield([
661 - 'a:a:one',
662 - 'b:b:one',
663 - 'Sync effect',
664 - ]);
665 -
666 - // Changes to part of the store (e.g. A) should not render other parts.
667 - source.valueA = 'a:two';
668 - expect(Scheduler).toFlushAndYield(['a:a:two']);
669 - source.valueB = 'b:two';
670 - expect(Scheduler).toFlushAndYield(['b:b:two']);
671 - });
672 - });
673 -
674 - it('should detect tearing in part of the store not yet subscribed to', () => {
675 - const source = createComplexSource('a:one', 'b:one');
676 - const mutableSource = createMutableSource(source);
677 -
678 - // Subscribe to part of the store.
679 - const getSnapshotA = s => s.valueA;
680 - const subscribeA = (s, callback) => s.subscribeA(callback);
681 - const getSnapshotB = s => s.valueB;
682 - const subscribeB = (s, callback) => s.subscribeB(callback);
683 -
684 - act(() => {
685 - ReactNoop.render(
686 - <>
687 - <Component
688 - label="a"
689 - getSnapshot={getSnapshotA}
690 - mutableSource={mutableSource}
691 - subscribe={subscribeA}
692 - />
693 - </>,
694 - () => Scheduler.unstable_yieldValue('Sync effect'),
695 - );
696 - expect(Scheduler).toFlushAndYield(['a:a:one', 'Sync effect']);
697 -
698 - // Because the store has not chagned yet, there are no pending updates,
699 - // so it is considered safe to read from when we start this render.
700 - ReactNoop.render(
701 - <>
702 - <Component
703 - label="a"
704 - getSnapshot={getSnapshotA}
705 - mutableSource={mutableSource}
706 - subscribe={subscribeA}
707 - />
708 - <Component
709 - label="b"
710 - getSnapshot={getSnapshotB}
711 - mutableSource={mutableSource}
712 - subscribe={subscribeB}
713 - />
714 - <Component
715 - label="c"
716 - getSnapshot={getSnapshotB}
717 - mutableSource={mutableSource}
718 - subscribe={subscribeB}
719 - />
720 - </>,
721 - () => Scheduler.unstable_yieldValue('Sync effect'),
722 - );
723 - expect(Scheduler).toFlushAndYieldThrough(['a:a:one', 'b:b:one']);
724 -
725 - // Mutating the source should trigger a tear detection on the next read,
726 - // which should throw and re-render the entire tree.
727 - source.valueB = 'b:two';
728 -
729 - expect(Scheduler).toFlushAndYield([
730 - 'a:a:one',
731 - 'b:b:two',
732 - 'c:b:two',
733 - 'Sync effect',
734 - ]);
735 - });
558 + },
559 + );
560 + expect(Scheduler).toFlushAndYieldThrough([
561 + 'a:new:two',
562 + 'b:new:two',
563 + 'Sync effect',
564 + ]);
565 });
566 + });
567
738 - it('does not schedule an update for subscriptions that fire with an unchanged snapshot', () => {
739 - const MockComponent = jest.fn(Component);
568 + // @gate experimental
569 + it('should throw and restart render if source and snapshot are unavailable during a sync update', () => {
570 + const source = createSource('one');
571 + const mutableSource = createMutableSource(source);
572
741 - const source = createSource('one');
742 - const mutableSource = createMutableSource(source);
743 -
744 - act(() => {
745 - ReactNoop.render(
746 - <MockComponent
747 - label="only"
573 + act(() => {
574 + ReactNoop.render(
575 + <>
576 + <Component
577 + label="a"
578 getSnapshot={defaultGetSnapshot}
579 mutableSource={mutableSource}
580 subscribe={defaultSubscribe}
751 - />,
752 - () => Scheduler.unstable_yieldValue('Sync effect'),
753 - );
754 - expect(Scheduler).toFlushAndYieldThrough(['only:one', 'Sync effect']);
755 - ReactNoop.flushPassiveEffects();
756 - expect(source.listenerCount).toBe(1);
757 -
758 - // Notify subscribe function but don't change the value
759 - source.value = 'one';
760 - expect(Scheduler).toFlushWithoutYielding();
761 - });
762 - });
763 -
764 - it('should throw and restart if getSnapshot changes between scheduled update and re-render', () => {
765 - const source = createSource('one');
766 - const mutableSource = createMutableSource(source);
767 -
768 - const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
769 -
770 - let updateGetSnapshot;
771 -
772 - function WrapperWithState() {
773 - const tuple = React.useState(() => defaultGetSnapshot);
774 - updateGetSnapshot = tuple[1];
775 - return (
581 + />
582 <Component
777 - label="only"
778 - getSnapshot={tuple[0]}
583 + label="b"
584 + getSnapshot={defaultGetSnapshot}
585 mutableSource={mutableSource}
586 subscribe={defaultSubscribe}
587 />
782 - );
783 - }
784 -
785 - act(() => {
786 - ReactNoop.render(<WrapperWithState />, () =>
787 - Scheduler.unstable_yieldValue('Sync effect'),
788 - );
789 - expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
790 - ReactNoop.flushPassiveEffects();
791 -
792 - // Change the source (and schedule an update).
793 - Scheduler.unstable_runWithPriority(
794 - Scheduler.unstable_LowPriority,
795 - () => {
796 - source.value = 'two';
797 - },
798 - );
799 -
800 - // Schedule a higher priority update that changes getSnapshot.
801 - Scheduler.unstable_runWithPriority(
802 - Scheduler.unstable_UserBlockingPriority,
803 - () => {
804 - updateGetSnapshot(() => newGetSnapshot);
805 - },
806 - );
807 -
808 - expect(Scheduler).toFlushAndYield(['only:new:two']);
588 + </>,
589 + () => Scheduler.unstable_yieldValue('Sync effect'),
590 + );
591 + expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
592 + ReactNoop.flushPassiveEffects();
593 +
594 + // Changing values should schedule an update with React.
595 + // Start working on this update but don't finish it.
596 + Scheduler.unstable_runWithPriority(Scheduler.unstable_LowPriority, () => {
597 + source.value = 'two';
598 + expect(Scheduler).toFlushAndYieldThrough(['a:two']);
599 });
810 - });
600
812 - it('should recover from a mutation during yield when other work is scheduled', () => {
813 - const source = createSource('one');
814 - const mutableSource = createMutableSource(source);
601 + const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
602
816 - act(() => {
817 - // Start a render that uses the mutable source.
603 + // Force a higher priority render with a new config.
604 + // This should signal that the snapshot is not safe and trigger a full re-render.
605 + ReactNoop.flushSync(() => {
606 ReactNoop.render(
607 <>
608 <Component
609 label="a"
822 - getSnapshot={defaultGetSnapshot}
610 + getSnapshot={newGetSnapshot}
611 mutableSource={mutableSource}
612 subscribe={defaultSubscribe}
613 />
614 <Component
615 label="b"
828 - getSnapshot={defaultGetSnapshot}
616 + getSnapshot={newGetSnapshot}
617 mutableSource={mutableSource}
618 subscribe={defaultSubscribe}
619 />
620 </>,
621 + () => Scheduler.unstable_yieldValue('Sync effect'),
622 );
834 - expect(Scheduler).toFlushAndYieldThrough(['a:one']);
835 -
836 - // Mutate source
837 - source.value = 'two';
838 -
839 - // Now render something different.
840 - ReactNoop.render(<div />);
841 - expect(Scheduler).toFlushAndYield([]);
623 });
624 + expect(Scheduler).toHaveYielded([
625 + 'a:new:two',
626 + 'b:new:two',
627 + 'Sync effect',
628 + ]);
629 });
844 -
845 - it('should not throw if the new getSnapshot returns the same snapshot value', () => {
846 - const source = createSource('one');
847 - const mutableSource = createMutableSource(source);
848 -
849 - const onRenderA = jest.fn();
850 - const onRenderB = jest.fn();
851 -
852 - let updateGetSnapshot;
853 -
854 - function WrapperWithState() {
855 - const tuple = React.useState(() => defaultGetSnapshot);
856 - updateGetSnapshot = tuple[1];
857 - return (
630 + });
631 +
632 + // @gate experimental
633 + it('should only update components whose subscriptions fire', () => {
634 + const source = createComplexSource('a:one', 'b:one');
635 + const mutableSource = createMutableSource(source);
636 +
637 + // Subscribe to part of the store.
638 + const getSnapshotA = s => s.valueA;
639 + const subscribeA = (s, callback) => s.subscribeA(callback);
640 + const getSnapshotB = s => s.valueB;
641 + const subscribeB = (s, callback) => s.subscribeB(callback);
642 +
643 + act(() => {
644 + ReactNoop.render(
645 + <>
646 + <Component
647 + label="a"
648 + getSnapshot={getSnapshotA}
649 + mutableSource={mutableSource}
650 + subscribe={subscribeA}
651 + />
652 <Component
653 label="b"
860 - getSnapshot={tuple[0]}
654 + getSnapshot={getSnapshotB}
655 mutableSource={mutableSource}
862 - subscribe={defaultSubscribe}
656 + subscribe={subscribeB}
657 />
864 - );
865 - }
866 -
867 - act(() => {
868 - ReactNoop.render(
869 - <>
870 - <React.Profiler id="a" onRender={onRenderA}>
871 - <Component
872 - label="a"
873 - getSnapshot={defaultGetSnapshot}
874 - mutableSource={mutableSource}
875 - subscribe={defaultSubscribe}
876 - />
877 - </React.Profiler>
878 - <React.Profiler id="b" onRender={onRenderB}>
879 - <WrapperWithState />
880 - </React.Profiler>
881 - </>,
882 - () => Scheduler.unstable_yieldValue('Sync effect'),
883 - );
884 - expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
885 - ReactNoop.flushPassiveEffects();
886 - expect(onRenderA).toHaveBeenCalledTimes(1);
887 - expect(onRenderB).toHaveBeenCalledTimes(1);
888 -
889 - // If B's getSnapshot function updates, but the snapshot it returns is the same,
890 - // only B should re-render (to update its state).
891 - updateGetSnapshot(() => s => defaultGetSnapshot(s));
892 - expect(Scheduler).toFlushAndYield(['b:one']);
893 - ReactNoop.flushPassiveEffects();
894 - expect(onRenderA).toHaveBeenCalledTimes(1);
895 - expect(onRenderB).toHaveBeenCalledTimes(2);
896 - });
658 + </>,
659 + () => Scheduler.unstable_yieldValue('Sync effect'),
660 + );
661 + expect(Scheduler).toFlushAndYield(['a:a:one', 'b:b:one', 'Sync effect']);
662 +
663 + // Changes to part of the store (e.g. A) should not render other parts.
664 + source.valueA = 'a:two';
665 + expect(Scheduler).toFlushAndYield(['a:a:two']);
666 + source.valueB = 'b:two';
667 + expect(Scheduler).toFlushAndYield(['b:b:two']);
668 });
898 -
899 - it('should not throw if getSnapshot changes but the source can be safely read from anyway', () => {
900 - const source = createSource('one');
901 - const mutableSource = createMutableSource(source);
902 -
903 - const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
904 -
905 - let updateGetSnapshot;
906 -
907 - function WrapperWithState() {
908 - const tuple = React.useState(() => defaultGetSnapshot);
909 - updateGetSnapshot = tuple[1];
910 - return (
669 + });
670 +
671 + // @gate experimental
672 + it('should detect tearing in part of the store not yet subscribed to', () => {
673 + const source = createComplexSource('a:one', 'b:one');
674 + const mutableSource = createMutableSource(source);
675 +
676 + // Subscribe to part of the store.
677 + const getSnapshotA = s => s.valueA;
678 + const subscribeA = (s, callback) => s.subscribeA(callback);
679 + const getSnapshotB = s => s.valueB;
680 + const subscribeB = (s, callback) => s.subscribeB(callback);
681 +
682 + act(() => {
683 + ReactNoop.render(
684 + <>
685 <Component
912 - label="only"
913 - getSnapshot={tuple[0]}
686 + label="a"
687 + getSnapshot={getSnapshotA}
688 mutableSource={mutableSource}
915 - subscribe={defaultSubscribe}
689 + subscribe={subscribeA}
690 />
917 - );
918 - }
919 -
920 - act(() => {
921 - ReactNoop.render(<WrapperWithState />, () =>
922 - Scheduler.unstable_yieldValue('Sync effect'),
923 - );
924 - expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
925 - ReactNoop.flushPassiveEffects();
926 -
927 - // Change the source (and schedule an update)
928 - // but also change the snapshot function too.
929 - ReactNoop.batchedUpdates(() => {
930 - source.value = 'two';
931 - updateGetSnapshot(() => newGetSnapshot);
932 - });
691 + </>,
692 + () => Scheduler.unstable_yieldValue('Sync effect'),
693 + );
694 + expect(Scheduler).toFlushAndYield(['a:a:one', 'Sync effect']);
695 +
696 + // Because the store has not chagned yet, there are no pending updates,
697 + // so it is considered safe to read from when we start this render.
698 + ReactNoop.render(
699 + <>
700 + <Component
701 + label="a"
702 + getSnapshot={getSnapshotA}
703 + mutableSource={mutableSource}
704 + subscribe={subscribeA}
705 + />
706 + <Component
707 + label="b"
708 + getSnapshot={getSnapshotB}
709 + mutableSource={mutableSource}
710 + subscribe={subscribeB}
711 + />
712 + <Component
713 + label="c"
714 + getSnapshot={getSnapshotB}
715 + mutableSource={mutableSource}
716 + subscribe={subscribeB}
717 + />
718 + </>,
719 + () => Scheduler.unstable_yieldValue('Sync effect'),
720 + );
721 + expect(Scheduler).toFlushAndYieldThrough(['a:a:one', 'b:b:one']);
722 +
723 + // Mutating the source should trigger a tear detection on the next read,
724 + // which should throw and re-render the entire tree.
725 + source.valueB = 'b:two';
726 +
727 + expect(Scheduler).toFlushAndYield([
728 + 'a:a:one',
729 + 'b:b:two',
730 + 'c:b:two',
731 + 'Sync effect',
732 + ]);
733 + });
734 + });
735 +
736 + // @gate experimental
737 + it('does not schedule an update for subscriptions that fire with an unchanged snapshot', () => {
738 + const MockComponent = jest.fn(Component);
739 +
740 + const source = createSource('one');
741 + const mutableSource = createMutableSource(source);
742 +
743 + act(() => {
744 + ReactNoop.render(
745 + <MockComponent
746 + label="only"
747 + getSnapshot={defaultGetSnapshot}
748 + mutableSource={mutableSource}
749 + subscribe={defaultSubscribe}
750 + />,
751 + () => Scheduler.unstable_yieldValue('Sync effect'),
752 + );
753 + expect(Scheduler).toFlushAndYieldThrough(['only:one', 'Sync effect']);
754 + ReactNoop.flushPassiveEffects();
755 + expect(source.listenerCount).toBe(1);
756
934 - expect(Scheduler).toFlushAndYield(['only:new:two']);
935 - });
757 + // Notify subscribe function but don't change the value
758 + source.value = 'one';
759 + expect(Scheduler).toFlushWithoutYielding();
760 });
761 + });
762 +
763 + // @gate experimental
764 + it('should throw and restart if getSnapshot changes between scheduled update and re-render', () => {
765 + const source = createSource('one');
766 + const mutableSource = createMutableSource(source);
767 +
768 + const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
769 +
770 + let updateGetSnapshot;
771 +
772 + function WrapperWithState() {
773 + const tuple = React.useState(() => defaultGetSnapshot);
774 + updateGetSnapshot = tuple[1];
775 + return (
776 + <Component
777 + label="only"
778 + getSnapshot={tuple[0]}
779 + mutableSource={mutableSource}
780 + subscribe={defaultSubscribe}
781 + />
782 + );
783 + }
784
938 - it('should still schedule an update if an eager selector throws after a mutation', () => {
939 - const source = createSource({
940 - friends: [
941 - {id: 1, name: 'Foo'},
942 - {id: 2, name: 'Bar'},
943 - ],
944 - });
945 - const mutableSource = createMutableSource(source);
785 + act(() => {
786 + ReactNoop.render(<WrapperWithState />, () =>
787 + Scheduler.unstable_yieldValue('Sync effect'),
788 + );
789 + expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
790 + ReactNoop.flushPassiveEffects();
791
947 - function FriendsList() {
948 - const getSnapshot = React.useCallback(
949 - ({value}) => Array.from(value.friends),
950 - [],
951 - );
952 - const friends = useMutableSource(
953 - mutableSource,
954 - getSnapshot,
955 - defaultSubscribe,
956 - );
957 - return (
958 - <ul>
959 - {friends.map(friend => (
960 - <Friend key={friend.id} id={friend.id} />
961 - ))}
962 - </ul>
963 - );
964 - }
792 + // Change the source (and schedule an update).
793 + Scheduler.unstable_runWithPriority(Scheduler.unstable_LowPriority, () => {
794 + source.value = 'two';
795 + });
796
966 - function Friend({id}) {
967 - const getSnapshot = React.useCallback(
968 - ({value}) => {
969 - // This selector is intentionally written in a way that will throw
970 - // if no matching friend exists in the store.
971 - return value.friends.find(friend => friend.id === id).name;
972 - },
973 - [id],
974 - );
975 - const name = useMutableSource(
976 - mutableSource,
977 - getSnapshot,
978 - defaultSubscribe,
979 - );
980 - Scheduler.unstable_yieldValue(`${id}:${name}`);
981 - return <li>{name}</li>;
982 - }
797 + // Schedule a higher priority update that changes getSnapshot.
798 + Scheduler.unstable_runWithPriority(
799 + Scheduler.unstable_UserBlockingPriority,
800 + () => {
801 + updateGetSnapshot(() => newGetSnapshot);
802 + },
803 + );
804
984 - act(() => {
985 - ReactNoop.render(<FriendsList />, () =>
986 - Scheduler.unstable_yieldValue('Sync effect'),
987 - );
988 - expect(Scheduler).toFlushAndYield(['1:Foo', '2:Bar', 'Sync effect']);
989 -
990 - // This mutation will cause the "Bar" component to throw,
991 - // since its value will no longer be a part of the store.
992 - // Mutable source should still schedule an update though,
993 - // which should unmount "Bar" and mount "Baz".
994 - source.value = {
995 - friends: [
996 - {id: 1, name: 'Foo'},
997 - {id: 3, name: 'Baz'},
998 - ],
999 - };
1000 - expect(Scheduler).toFlushAndYield(['1:Foo', '3:Baz']);
1001 - });
805 + expect(Scheduler).toFlushAndYield(['only:new:two']);
806 });
807 + });
808
1004 - it('should not warn about updates that fire between unmount and passive unsubcribe', () => {
1005 - const source = createSource('one');
1006 - const mutableSource = createMutableSource(source);
809 + // @gate experimental
810 + it('should recover from a mutation during yield when other work is scheduled', () => {
811 + const source = createSource('one');
812 + const mutableSource = createMutableSource(source);
813
1008 - function Wrapper() {
1009 - React.useLayoutEffect(() => () => {
1010 - Scheduler.unstable_yieldValue('layout unmount');
1011 - });
1012 - return (
814 + act(() => {
815 + // Start a render that uses the mutable source.
816 + ReactNoop.render(
817 + <>
818 <Component
1014 - label="only"
819 + label="a"
820 + getSnapshot={defaultGetSnapshot}
821 + mutableSource={mutableSource}
822 + subscribe={defaultSubscribe}
823 + />
824 + <Component
825 + label="b"
826 getSnapshot={defaultGetSnapshot}
827 mutableSource={mutableSource}
828 subscribe={defaultSubscribe}
829 />
1019 - );
1020 - }
830 + </>,
831 + );
832 + expect(Scheduler).toFlushAndYieldThrough(['a:one']);
833
1022 - act(() => {
1023 - ReactNoop.renderToRootWithID(<Wrapper />, 'root', () =>
1024 - Scheduler.unstable_yieldValue('Sync effect'),
1025 - );
1026 - expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
1027 - ReactNoop.flushPassiveEffects();
834 + // Mutate source
835 + source.value = 'two';
836
1029 - // Umounting a root should remove the remaining event listeners in a passive effect
1030 - ReactNoop.unmountRootWithID('root');
1031 - expect(Scheduler).toFlushAndYieldThrough(['layout unmount']);
837 + // Now render something different.
838 + ReactNoop.render(<div />);
839 + expect(Scheduler).toFlushAndYield([]);
840 + });
841 + });
842 +
843 + // @gate experimental
844 + it('should not throw if the new getSnapshot returns the same snapshot value', () => {
845 + const source = createSource('one');
846 + const mutableSource = createMutableSource(source);
847 +
848 + const onRenderA = jest.fn();
849 + const onRenderB = jest.fn();
850 +
851 + let updateGetSnapshot;
852 +
853 + function WrapperWithState() {
854 + const tuple = React.useState(() => defaultGetSnapshot);
855 + updateGetSnapshot = tuple[1];
856 + return (
857 + <Component
858 + label="b"
859 + getSnapshot={tuple[0]}
860 + mutableSource={mutableSource}
861 + subscribe={defaultSubscribe}
862 + />
863 + );
864 + }
865
1033 - // Changes to source should not cause a warning,
1034 - // even though the unsubscribe hasn't run yet (since it's a pending passive effect).
1035 - source.value = 'two';
1036 - expect(Scheduler).toFlushAndYield([]);
1037 - });
866 + act(() => {
867 + ReactNoop.render(
868 + <>
869 + <React.Profiler id="a" onRender={onRenderA}>
870 + <Component
871 + label="a"
872 + getSnapshot={defaultGetSnapshot}
873 + mutableSource={mutableSource}
874 + subscribe={defaultSubscribe}
875 + />
876 + </React.Profiler>
877 + <React.Profiler id="b" onRender={onRenderB}>
878 + <WrapperWithState />
879 + </React.Profiler>
880 + </>,
881 + () => Scheduler.unstable_yieldValue('Sync effect'),
882 + );
883 + expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
884 + ReactNoop.flushPassiveEffects();
885 + expect(onRenderA).toHaveBeenCalledTimes(1);
886 + expect(onRenderB).toHaveBeenCalledTimes(1);
887 +
888 + // If B's getSnapshot function updates, but the snapshot it returns is the same,
889 + // only B should re-render (to update its state).
890 + updateGetSnapshot(() => s => defaultGetSnapshot(s));
891 + expect(Scheduler).toFlushAndYield(['b:one']);
892 + ReactNoop.flushPassiveEffects();
893 + expect(onRenderA).toHaveBeenCalledTimes(1);
894 + expect(onRenderB).toHaveBeenCalledTimes(2);
895 });
896 + });
897 +
898 + // @gate experimental
899 + it('should not throw if getSnapshot changes but the source can be safely read from anyway', () => {
900 + const source = createSource('one');
901 + const mutableSource = createMutableSource(source);
902 +
903 + const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
904 +
905 + let updateGetSnapshot;
906 +
907 + function WrapperWithState() {
908 + const tuple = React.useState(() => defaultGetSnapshot);
909 + updateGetSnapshot = tuple[1];
910 + return (
911 + <Component
912 + label="only"
913 + getSnapshot={tuple[0]}
914 + mutableSource={mutableSource}
915 + subscribe={defaultSubscribe}
916 + />
917 + );
918 + }
919
1040 - it('should support inline selectors and updates that are processed after selector change', async () => {
1041 - const source = createSource({
1042 - a: 'initial',
1043 - b: 'initial',
920 + act(() => {
921 + ReactNoop.render(<WrapperWithState />, () =>
922 + Scheduler.unstable_yieldValue('Sync effect'),
923 + );
924 + expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
925 + ReactNoop.flushPassiveEffects();
926 +
927 + // Change the source (and schedule an update)
928 + // but also change the snapshot function too.
929 + ReactNoop.batchedUpdates(() => {
930 + source.value = 'two';
931 + updateGetSnapshot(() => newGetSnapshot);
932 });
1045 - const mutableSource = createMutableSource(source);
933
1047 - const getSnapshotA = () => source.value.a;
1048 - const getSnapshotB = () => source.value.b;
934 + expect(Scheduler).toFlushAndYield(['only:new:two']);
935 + });
936 + });
937 +
938 + // @gate experimental
939 + it('should still schedule an update if an eager selector throws after a mutation', () => {
940 + const source = createSource({
941 + friends: [
942 + {id: 1, name: 'Foo'},
943 + {id: 2, name: 'Bar'},
944 + ],
945 + });
946 + const mutableSource = createMutableSource(source);
947 +
948 + function FriendsList() {
949 + const getSnapshot = React.useCallback(
950 + ({value}) => Array.from(value.friends),
951 + [],
952 + );
953 + const friends = useMutableSource(
954 + mutableSource,
955 + getSnapshot,
956 + defaultSubscribe,
957 + );
958 + return (
959 + <ul>
960 + {friends.map(friend => (
961 + <Friend key={friend.id} id={friend.id} />
962 + ))}
963 + </ul>
964 + );
965 + }
966
1050 - function mutateB(newB) {
1051 - source.value = {
1052 - ...source.value,
1053 - b: newB,
1054 - };
1055 - }
967 + function Friend({id}) {
968 + const getSnapshot = React.useCallback(
969 + ({value}) => {
970 + // This selector is intentionally written in a way that will throw
971 + // if no matching friend exists in the store.
972 + return value.friends.find(friend => friend.id === id).name;
973 + },
974 + [id],
975 + );
976 + const name = useMutableSource(
977 + mutableSource,
978 + getSnapshot,
979 + defaultSubscribe,
980 + );
981 + Scheduler.unstable_yieldValue(`${id}:${name}`);
982 + return <li>{name}</li>;
983 + }
984
1057 - function App({getSnapshot}) {
1058 - const state = useMutableSource(
1059 - mutableSource,
1060 - getSnapshot,
1061 - defaultSubscribe,
1062 - );
1063 - return state;
1064 - }
985 + act(() => {
986 + ReactNoop.render(<FriendsList />, () =>
987 + Scheduler.unstable_yieldValue('Sync effect'),
988 + );
989 + expect(Scheduler).toFlushAndYield(['1:Foo', '2:Bar', 'Sync effect']);
990 +
991 + // This mutation will cause the "Bar" component to throw,
992 + // since its value will no longer be a part of the store.
993 + // Mutable source should still schedule an update though,
994 + // which should unmount "Bar" and mount "Baz".
995 + source.value = {
996 + friends: [
997 + {id: 1, name: 'Foo'},
998 + {id: 3, name: 'Baz'},
999 + ],
1000 + };
1001 + expect(Scheduler).toFlushAndYield(['1:Foo', '3:Baz']);
1002 + });
1003 + });
1004
1066 - const root = ReactNoop.createRoot();
1067 - await act(async () => {
1068 - root.render(<App getSnapshot={getSnapshotA} />);
1069 - });
1070 - expect(root).toMatchRenderedOutput('initial');
1005 + // @gate experimental
1006 + it('should not warn about updates that fire between unmount and passive unsubcribe', () => {
1007 + const source = createSource('one');
1008 + const mutableSource = createMutableSource(source);
1009
1072 - await act(async () => {
1073 - mutateB('Updated B');
1074 - root.render(<App getSnapshot={getSnapshotB} />);
1010 + function Wrapper() {
1011 + React.useLayoutEffect(() => () => {
1012 + Scheduler.unstable_yieldValue('layout unmount');
1013 });
1076 - expect(root).toMatchRenderedOutput('Updated B');
1014 + return (
1015 + <Component
1016 + label="only"
1017 + getSnapshot={defaultGetSnapshot}
1018 + mutableSource={mutableSource}
1019 + subscribe={defaultSubscribe}
1020 + />
1021 + );
1022 + }
1023
1078 - await act(async () => {
1079 - mutateB('Another update');
1080 - });
1081 - expect(root).toMatchRenderedOutput('Another update');
1024 + act(() => {
1025 + ReactNoop.renderToRootWithID(<Wrapper />, 'root', () =>
1026 + Scheduler.unstable_yieldValue('Sync effect'),
1027 + );
1028 + expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
1029 + ReactNoop.flushPassiveEffects();
1030 +
1031 + // Umounting a root should remove the remaining event listeners in a passive effect
1032 + ReactNoop.unmountRootWithID('root');
1033 + expect(Scheduler).toFlushAndYieldThrough(['layout unmount']);
1034 +
1035 + // Changes to source should not cause a warning,
1036 + // even though the unsubscribe hasn't run yet (since it's a pending passive effect).
1037 + source.value = 'two';
1038 + expect(Scheduler).toFlushAndYield([]);
1039 });
1040 + });
1041
1084 - it('should clear the update queue when getSnapshot changes with pending lower priority updates', async () => {
1085 - const source = createSource({
1086 - a: 'initial',
1087 - b: 'initial',
1088 - });
1089 - const mutableSource = createMutableSource(source);
1042 + // @gate experimental
1043 + it('should support inline selectors and updates that are processed after selector change', async () => {
1044 + const source = createSource({
1045 + a: 'initial',
1046 + b: 'initial',
1047 + });
1048 + const mutableSource = createMutableSource(source);
1049
1091 - const getSnapshotA = () => source.value.a;
1092 - const getSnapshotB = () => source.value.b;
1050 + const getSnapshotA = () => source.value.a;
1051 + const getSnapshotB = () => source.value.b;
1052
1094 - function mutateA(newA) {
1095 - source.value = {
1096 - ...source.value,
1097 - a: newA,
1098 - };
1099 - }
1053 + function mutateB(newB) {
1054 + source.value = {
1055 + ...source.value,
1056 + b: newB,
1057 + };
1058 + }
1059
1101 - function mutateB(newB) {
1102 - source.value = {
1103 - ...source.value,
1104 - b: newB,
1105 - };
1106 - }
1060 + function App({getSnapshot}) {
1061 + const state = useMutableSource(
1062 + mutableSource,
1063 + getSnapshot,
1064 + defaultSubscribe,
1065 + );
1066 + return state;
1067 + }
1068
1108 - function App({toggle}) {
1109 - const state = useMutableSource(
1110 - mutableSource,
1111 - toggle ? getSnapshotB : getSnapshotA,
1112 - defaultSubscribe,
1113 - );
1114 - const result = (toggle ? 'B: ' : 'A: ') + state;
1115 - return result;
1116 - }
1069 + const root = ReactNoop.createRoot();
1070 + await act(async () => {
1071 + root.render(<App getSnapshot={getSnapshotA} />);
1072 + });
1073 + expect(root).toMatchRenderedOutput('initial');
1074
1118 - const root = ReactNoop.createRoot();
1119 - await act(async () => {
1120 - root.render(<App toggle={false} />);
1121 - });
1122 - expect(root).toMatchRenderedOutput('A: initial');
1123 -
1124 - await act(async () => {
1125 - ReactNoop.discreteUpdates(() => {
1126 - // Update both A and B to the same value
1127 - mutateA('Update');
1128 - mutateB('Update');
1129 - // Toggle to B in the same batch
1130 - root.render(<App toggle={true} />);
1131 - });
1132 - // Mutate A at lower priority. This should never be rendered, because
1133 - // by the time we get to the lower priority, we've already switched
1134 - // to B.
1135 - mutateA('OOPS! This mutation should be ignored');
1136 - });
1137 - expect(root).toMatchRenderedOutput('B: Update');
1075 + await act(async () => {
1076 + mutateB('Updated B');
1077 + root.render(<App getSnapshot={getSnapshotB} />);
1078 });
1079 + expect(root).toMatchRenderedOutput('Updated B');
1080
1140 - it('should clear the update queue when source changes with pending lower priority updates', async () => {
1141 - const sourceA = createSource('initial');
1142 - const sourceB = createSource('initial');
1143 - const mutableSourceA = createMutableSource(sourceA);
1144 - const mutableSourceB = createMutableSource(sourceB);
1145 -
1146 - function App({toggle}) {
1147 - const state = useMutableSource(
1148 - toggle ? mutableSourceB : mutableSourceA,
1149 - defaultGetSnapshot,
1150 - defaultSubscribe,
1151 - );
1152 - const result = (toggle ? 'B: ' : 'A: ') + state;
1153 - return result;
1154 - }
1081 + await act(async () => {
1082 + mutateB('Another update');
1083 + });
1084 + expect(root).toMatchRenderedOutput('Another update');
1085 + });
1086 +
1087 + // @gate experimental
1088 + it('should clear the update queue when getSnapshot changes with pending lower priority updates', async () => {
1089 + const source = createSource({
1090 + a: 'initial',
1091 + b: 'initial',
1092 + });
1093 + const mutableSource = createMutableSource(source);
1094
1156 - const root = ReactNoop.createRoot();
1157 - await act(async () => {
1158 - root.render(<App toggle={false} />);
1159 - });
1160 - expect(root).toMatchRenderedOutput('A: initial');
1161 -
1162 - await act(async () => {
1163 - ReactNoop.discreteUpdates(() => {
1164 - // Update both A and B to the same value
1165 - sourceA.value = 'Update';
1166 - sourceB.value = 'Update';
1167 - // Toggle to B in the same batch
1168 - root.render(<App toggle={true} />);
1169 - });
1170 - // Mutate A at lower priority. This should never be rendered, because
1171 - // by the time we get to the lower priority, we've already switched
1172 - // to B.
1173 - sourceA.value = 'OOPS! This mutation should be ignored';
1095 + const getSnapshotA = () => source.value.a;
1096 + const getSnapshotB = () => source.value.b;
1097 +
1098 + function mutateA(newA) {
1099 + source.value = {
1100 + ...source.value,
1101 + a: newA,
1102 + };
1103 + }
1104 +
1105 + function mutateB(newB) {
1106 + source.value = {
1107 + ...source.value,
1108 + b: newB,
1109 + };
1110 + }
1111 +
1112 + function App({toggle}) {
1113 + const state = useMutableSource(
1114 + mutableSource,
1115 + toggle ? getSnapshotB : getSnapshotA,
1116 + defaultSubscribe,
1117 + );
1118 + const result = (toggle ? 'B: ' : 'A: ') + state;
1119 + return result;
1120 + }
1121 +
1122 + const root = ReactNoop.createRoot();
1123 + await act(async () => {
1124 + root.render(<App toggle={false} />);
1125 + });
1126 + expect(root).toMatchRenderedOutput('A: initial');
1127 +
1128 + await act(async () => {
1129 + ReactNoop.discreteUpdates(() => {
1130 + // Update both A and B to the same value
1131 + mutateA('Update');
1132 + mutateB('Update');
1133 + // Toggle to B in the same batch
1134 + root.render(<App toggle={true} />);
1135 });
1175 - expect(root).toMatchRenderedOutput('B: Update');
1136 + // Mutate A at lower priority. This should never be rendered, because
1137 + // by the time we get to the lower priority, we've already switched
1138 + // to B.
1139 + mutateA('OOPS! This mutation should be ignored');
1140 });
1141 + expect(root).toMatchRenderedOutput('B: Update');
1142 + });
1143 +
1144 + // @gate experimental
1145 + it('should clear the update queue when source changes with pending lower priority updates', async () => {
1146 + const sourceA = createSource('initial');
1147 + const sourceB = createSource('initial');
1148 + const mutableSourceA = createMutableSource(sourceA);
1149 + const mutableSourceB = createMutableSource(sourceB);
1150 +
1151 + function App({toggle}) {
1152 + const state = useMutableSource(
1153 + toggle ? mutableSourceB : mutableSourceA,
1154 + defaultGetSnapshot,
1155 + defaultSubscribe,
1156 + );
1157 + const result = (toggle ? 'B: ' : 'A: ') + state;
1158 + return result;
1159 + }
1160
1178 - it('should always treat reading as potentially unsafe when getSnapshot changes between renders', async () => {
1179 - const source = createSource({
1180 - a: 'foo',
1181 - b: 'bar',
1161 + const root = ReactNoop.createRoot();
1162 + await act(async () => {
1163 + root.render(<App toggle={false} />);
1164 + });
1165 + expect(root).toMatchRenderedOutput('A: initial');
1166 +
1167 + await act(async () => {
1168 + ReactNoop.discreteUpdates(() => {
1169 + // Update both A and B to the same value
1170 + sourceA.value = 'Update';
1171 + sourceB.value = 'Update';
1172 + // Toggle to B in the same batch
1173 + root.render(<App toggle={true} />);
1174 });
1183 - const mutableSource = createMutableSource(source);
1184 -
1185 - const getSnapshotA = () => source.value.a;
1186 - const getSnapshotB = () => source.value.b;
1187 -
1188 - function mutateA(newA) {
1189 - source.value = {
1190 - ...source.value,
1191 - a: newA,
1192 - };
1193 - }
1175 + // Mutate A at lower priority. This should never be rendered, because
1176 + // by the time we get to the lower priority, we've already switched
1177 + // to B.
1178 + sourceA.value = 'OOPS! This mutation should be ignored';
1179 + });
1180 + expect(root).toMatchRenderedOutput('B: Update');
1181 + });
1182 +
1183 + // @gate experimental
1184 + it('should always treat reading as potentially unsafe when getSnapshot changes between renders', async () => {
1185 + const source = createSource({
1186 + a: 'foo',
1187 + b: 'bar',
1188 + });
1189 + const mutableSource = createMutableSource(source);
1190
1195 - function App({getSnapshotFirst, getSnapshotSecond}) {
1196 - const first = useMutableSource(
1197 - mutableSource,
1198 - getSnapshotFirst,
1199 - defaultSubscribe,
1200 - );
1201 - const second = useMutableSource(
1202 - mutableSource,
1203 - getSnapshotSecond,
1204 - defaultSubscribe,
1205 - );
1191 + const getSnapshotA = () => source.value.a;
1192 + const getSnapshotB = () => source.value.b;
1193
1207 - let result = `x: ${first}, y: ${second}`;
1194 + function mutateA(newA) {
1195 + source.value = {
1196 + ...source.value,
1197 + a: newA,
1198 + };
1199 + }
1200
1209 - if (getSnapshotFirst === getSnapshotSecond) {
1210 - // When both getSnapshot functions are equal,
1211 - // the two values must be consistent.
1212 - if (first !== second) {
1213 - result = 'Oops, tearing!';
1214 - }
1201 + function App({getSnapshotFirst, getSnapshotSecond}) {
1202 + const first = useMutableSource(
1203 + mutableSource,
1204 + getSnapshotFirst,
1205 + defaultSubscribe,
1206 + );
1207 + const second = useMutableSource(
1208 + mutableSource,
1209 + getSnapshotSecond,
1210 + defaultSubscribe,
1211 + );
1212 +
1213 + let result = `x: ${first}, y: ${second}`;
1214 +
1215 + if (getSnapshotFirst === getSnapshotSecond) {
1216 + // When both getSnapshot functions are equal,
1217 + // the two values must be consistent.
1218 + if (first !== second) {
1219 + result = 'Oops, tearing!';
1220 }
1221 + }
1222
1217 - React.useEffect(() => {
1218 - Scheduler.unstable_yieldValue(result);
1219 - }, [result]);
1223 + React.useEffect(() => {
1224 + Scheduler.unstable_yieldValue(result);
1225 + }, [result]);
1226
1221 - return result;
1222 - }
1227 + return result;
1228 + }
1229
1224 - const root = ReactNoop.createRoot();
1225 - await act(async () => {
1230 + const root = ReactNoop.createRoot();
1231 + await act(async () => {
1232 + root.render(
1233 + <App
1234 + getSnapshotFirst={getSnapshotA}
1235 + getSnapshotSecond={getSnapshotB}
1236 + />,
1237 + );
1238 + });
1239 + // x and y start out reading from different parts of the store.
1240 + expect(Scheduler).toHaveYielded(['x: foo, y: bar']);
1241 +
1242 + await act(async () => {
1243 + ReactNoop.discreteUpdates(() => {
1244 + // At high priority, toggle y so that it reads from A instead of B.
1245 + // Simultaneously, mutate A.
1246 + mutateA('baz');
1247 root.render(
1248 <App
1249 getSnapshotFirst={getSnapshotA}
1229 - getSnapshotSecond={getSnapshotB}
1250 + getSnapshotSecond={getSnapshotA}
1251 />,
1252 );
1232 - });
1233 - // x and y start out reading from different parts of the store.
1234 - expect(Scheduler).toHaveYielded(['x: foo, y: bar']);
1235 -
1236 - await act(async () => {
1237 - ReactNoop.discreteUpdates(() => {
1238 - // At high priority, toggle y so that it reads from A instead of B.
1239 - // Simultaneously, mutate A.
1240 - mutateA('baz');
1241 - root.render(
1242 - <App
1243 - getSnapshotFirst={getSnapshotA}
1244 - getSnapshotSecond={getSnapshotA}
1245 - />,
1246 - );
1253
1248 - // If this update were processed before the next mutation,
1249 - // it would be expected to yield "baz" and "baz".
1250 - });
1251 -
1252 - // At lower priority, mutate A again.
1253 - // This happens to match the initial value of B.
1254 - mutateA('bar');
1255 -
1256 - // When this update is processed,
1257 - // it is expected to yield "bar" and "bar".
1254 + // If this update were processed before the next mutation,
1255 + // it would be expected to yield "baz" and "baz".
1256 });
1257
1260 - // Check that we didn't commit any inconsistent states.
1261 - // The actual sequence of work will be:
1262 - // 1. React renders the high-pri update, sees a new getSnapshot, detects the source has been further mutated, and throws
1263 - // 2. React re-renders with all pending updates, including the second mutation, and renders "bar" and "bar".
1264 - expect(Scheduler).toHaveYielded(['x: bar, y: bar']);
1258 + // At lower priority, mutate A again.
1259 + // This happens to match the initial value of B.
1260 + mutateA('bar');
1261 +
1262 + // When this update is processed,
1263 + // it is expected to yield "bar" and "bar".
1264 });
1265
1267 - it('getSnapshot changes and then source is mutated in between paint and passive effect phase', async () => {
1268 - const source = createSource({
1269 - a: 'foo',
1270 - b: 'bar',
1271 - });
1272 - const mutableSource = createMutableSource(source);
1266 + // Check that we didn't commit any inconsistent states.
1267 + // The actual sequence of work will be:
1268 + // 1. React renders the high-pri update, sees a new getSnapshot, detects the source has been further mutated, and throws
1269 + // 2. React re-renders with all pending updates, including the second mutation, and renders "bar" and "bar".
1270 + expect(Scheduler).toHaveYielded(['x: bar, y: bar']);
1271 + });
1272 +
1273 + // @gate experimental
1274 + it('getSnapshot changes and then source is mutated in between paint and passive effect phase', async () => {
1275 + const source = createSource({
1276 + a: 'foo',
1277 + b: 'bar',
1278 + });
1279 + const mutableSource = createMutableSource(source);
1280
1274 - function mutateB(newB) {
1275 - source.value = {
1276 - ...source.value,
1277 - b: newB,
1278 - };
1279 - }
1281 + function mutateB(newB) {
1282 + source.value = {
1283 + ...source.value,
1284 + b: newB,
1285 + };
1286 + }
1287
1281 - const getSnapshotA = () => source.value.a;
1282 - const getSnapshotB = () => source.value.b;
1288 + const getSnapshotA = () => source.value.a;
1289 + const getSnapshotB = () => source.value.b;
1290
1284 - function App({getSnapshot}) {
1285 - const value = useMutableSource(
1286 - mutableSource,
1287 - getSnapshot,
1288 - defaultSubscribe,
1289 - );
1291 + function App({getSnapshot}) {
1292 + const value = useMutableSource(
1293 + mutableSource,
1294 + getSnapshot,
1295 + defaultSubscribe,
1296 + );
1297
1291 - Scheduler.unstable_yieldValue('Render: ' + value);
1292 - React.useEffect(() => {
1293 - Scheduler.unstable_yieldValue('Commit: ' + value);
1294 - }, [value]);
1298 + Scheduler.unstable_yieldValue('Render: ' + value);
1299 + React.useEffect(() => {
1300 + Scheduler.unstable_yieldValue('Commit: ' + value);
1301 + }, [value]);
1302
1296 - return value;
1297 - }
1303 + return value;
1304 + }
1305
1299 - const root = ReactNoop.createRoot();
1300 - await act(async () => {
1301 - root.render(<App getSnapshot={getSnapshotA} />);
1302 - });
1303 - expect(Scheduler).toHaveYielded(['Render: foo', 'Commit: foo']);
1304 -
1305 - await act(async () => {
1306 - // Switch getSnapshot to read from B instead
1307 - root.render(<App getSnapshot={getSnapshotB} />);
1308 - // Render and finish the tree, but yield right after paint, before
1309 - // the passive effects have fired.
1310 - expect(Scheduler).toFlushUntilNextPaint(['Render: bar']);
1311 - // Then mutate B.
1312 - mutateB('baz');
1313 - });
1314 - expect(Scheduler).toHaveYielded([
1315 - // Fires the effect from the previous render
1316 - 'Commit: bar',
1317 - // During that effect, it should detect that the snapshot has changed
1318 - // and re-render.
1319 - 'Render: baz',
1320 - 'Commit: baz',
1321 - ]);
1322 - expect(root).toMatchRenderedOutput('baz');
1306 + const root = ReactNoop.createRoot();
1307 + await act(async () => {
1308 + root.render(<App getSnapshot={getSnapshotA} />);
1309 });
1310 + expect(Scheduler).toHaveYielded(['Render: foo', 'Commit: foo']);
1311 +
1312 + await act(async () => {
1313 + // Switch getSnapshot to read from B instead
1314 + root.render(<App getSnapshot={getSnapshotB} />);
1315 + // Render and finish the tree, but yield right after paint, before
1316 + // the passive effects have fired.
1317 + expect(Scheduler).toFlushUntilNextPaint(['Render: bar']);
1318 + // Then mutate B.
1319 + mutateB('baz');
1320 + });
1321 + expect(Scheduler).toHaveYielded([
1322 + // Fires the effect from the previous render
1323 + 'Commit: bar',
1324 + // During that effect, it should detect that the snapshot has changed
1325 + // and re-render.
1326 + 'Render: baz',
1327 + 'Commit: baz',
1328 + ]);
1329 + expect(root).toMatchRenderedOutput('baz');
1330 + });
1331 +
1332 + // @gate experimental
1333 + it('getSnapshot changes and then source is mutated in between paint and passive effect phase, case 2', async () => {
1334 + const source = createSource({
1335 + a: 'a0',
1336 + b: 'b0',
1337 + });
1338 + const mutableSource = createMutableSource(source);
1339
1325 - it('getSnapshot changes and then source is mutated in between paint and passive effect phase, case 2', async () => {
1326 - const source = createSource({
1327 - a: 'a0',
1328 - b: 'b0',
1329 - });
1330 - const mutableSource = createMutableSource(source);
1331 -
1332 - const getSnapshotA = () => source.value.a;
1333 - const getSnapshotB = () => source.value.b;
1334 -
1335 - function mutateA(newA) {
1336 - source.value = {
1337 - ...source.value,
1338 - a: newA,
1339 - };
1340 - }
1341 -
1342 - function App({getSnapshotFirst, getSnapshotSecond}) {
1343 - const first = useMutableSource(
1344 - mutableSource,
1345 - getSnapshotFirst,
1346 - defaultSubscribe,
1347 - );
1348 - const second = useMutableSource(
1349 - mutableSource,
1350 - getSnapshotSecond,
1351 - defaultSubscribe,
1352 - );
1353 -
1354 - return `first: ${first}, second: ${second}`;
1355 - }
1340 + const getSnapshotA = () => source.value.a;
1341 + const getSnapshotB = () => source.value.b;
1342
1357 - const root = ReactNoop.createRoot();
1358 - await act(async () => {
1359 - root.render(
1360 - <App
1361 - getSnapshotFirst={getSnapshotA}
1362 - getSnapshotSecond={getSnapshotB}
1363 - />,
1364 - );
1365 - });
1366 - expect(root.getChildrenAsJSX()).toEqual('first: a0, second: b0');
1343 + function mutateA(newA) {
1344 + source.value = {
1345 + ...source.value,
1346 + a: newA,
1347 + };
1348 + }
1349
1368 - await act(async () => {
1369 - // Switch the second getSnapshot to also read from A
1370 - root.render(
1371 - <App
1372 - getSnapshotFirst={getSnapshotA}
1373 - getSnapshotSecond={getSnapshotA}
1374 - />,
1375 - );
1376 - // Render and finish the tree, but yield right after paint, before
1377 - // the passive effects have fired.
1378 - expect(Scheduler).toFlushUntilNextPaint([]);
1379 -
1380 - // Now mutate A. Both hooks should update.
1381 - // This is at high priority so that it doesn't get batched with default
1382 - // priority updates that might fire during the passive effect
1383 - ReactNoop.discreteUpdates(() => {
1384 - mutateA('a1');
1385 - });
1386 - expect(Scheduler).toFlushUntilNextPaint([]);
1350 + function App({getSnapshotFirst, getSnapshotSecond}) {
1351 + const first = useMutableSource(
1352 + mutableSource,
1353 + getSnapshotFirst,
1354 + defaultSubscribe,
1355 + );
1356 + const second = useMutableSource(
1357 + mutableSource,
1358 + getSnapshotSecond,
1359 + defaultSubscribe,
1360 + );
1361 +
1362 + return `first: ${first}, second: ${second}`;
1363 + }
1364
1388 - expect(root.getChildrenAsJSX()).toEqual('first: a1, second: a1');
1365 + const root = ReactNoop.createRoot();
1366 + await act(async () => {
1367 + root.render(
1368 + <App
1369 + getSnapshotFirst={getSnapshotA}
1370 + getSnapshotSecond={getSnapshotB}
1371 + />,
1372 + );
1373 + });
1374 + expect(root.getChildrenAsJSX()).toEqual('first: a0, second: b0');
1375 +
1376 + await act(async () => {
1377 + // Switch the second getSnapshot to also read from A
1378 + root.render(
1379 + <App
1380 + getSnapshotFirst={getSnapshotA}
1381 + getSnapshotSecond={getSnapshotA}
1382 + />,
1383 + );
1384 + // Render and finish the tree, but yield right after paint, before
1385 + // the passive effects have fired.
1386 + expect(Scheduler).toFlushUntilNextPaint([]);
1387 +
1388 + // Now mutate A. Both hooks should update.
1389 + // This is at high priority so that it doesn't get batched with default
1390 + // priority updates that might fire during the passive effect
1391 + ReactNoop.discreteUpdates(() => {
1392 + mutateA('a1');
1393 });
1394 + expect(Scheduler).toFlushUntilNextPaint([]);
1395
1396 expect(root.getChildrenAsJSX()).toEqual('first: a1, second: a1');
1397 });
1398
1394 - it('getSnapshot changes and then source is mutated during interleaved event', async () => {
1395 - const {useEffect} = React;
1396 -
1397 - const source = createComplexSource('1', '2');
1398 - const mutableSource = createMutableSource(source);
1399 -
1400 - // Subscribe to part of the store.
1401 - const getSnapshotA = s => s.valueA;
1402 - const subscribeA = (s, callback) => s.subscribeA(callback);
1403 - const configA = [getSnapshotA, subscribeA];
1404 -
1405 - const getSnapshotB = s => s.valueB;
1406 - const subscribeB = (s, callback) => s.subscribeB(callback);
1407 - const configB = [getSnapshotB, subscribeB];
1408 -
1409 - function App({parentConfig, childConfig}) {
1410 - const [getSnapshot, subscribe] = parentConfig;
1411 - const parentValue = useMutableSource(
1412 - mutableSource,
1413 - getSnapshot,
1414 - subscribe,
1415 - );
1416 -
1417 - Scheduler.unstable_yieldValue('Parent: ' + parentValue);
1418 -
1419 - return (
1420 - <Child
1421 - parentConfig={parentConfig}
1422 - childConfig={childConfig}
1423 - parentValue={parentValue}
1424 - />
1425 - );
1426 - }
1399 + expect(root.getChildrenAsJSX()).toEqual('first: a1, second: a1');
1400 + });
1401 +
1402 + // @gate experimental
1403 + it('getSnapshot changes and then source is mutated during interleaved event', async () => {
1404 + const {useEffect} = React;
1405 +
1406 + const source = createComplexSource('1', '2');
1407 + const mutableSource = createMutableSource(source);
1408 +
1409 + // Subscribe to part of the store.
1410 + const getSnapshotA = s => s.valueA;
1411 + const subscribeA = (s, callback) => s.subscribeA(callback);
1412 + const configA = [getSnapshotA, subscribeA];
1413 +
1414 + const getSnapshotB = s => s.valueB;
1415 + const subscribeB = (s, callback) => s.subscribeB(callback);
1416 + const configB = [getSnapshotB, subscribeB];
1417 +
1418 + function App({parentConfig, childConfig}) {
1419 + const [getSnapshot, subscribe] = parentConfig;
1420 + const parentValue = useMutableSource(
1421 + mutableSource,
1422 + getSnapshot,
1423 + subscribe,
1424 + );
1425 +
1426 + Scheduler.unstable_yieldValue('Parent: ' + parentValue);
1427 +
1428 + return (
1429 + <Child
1430 + parentConfig={parentConfig}
1431 + childConfig={childConfig}
1432 + parentValue={parentValue}
1433 + />
1434 + );
1435 + }
1436
1428 - function Child({parentConfig, childConfig, parentValue}) {
1429 - const [getSnapshot, subscribe] = childConfig;
1430 - const childValue = useMutableSource(
1431 - mutableSource,
1432 - getSnapshot,
1433 - subscribe,
1434 - );
1437 + function Child({parentConfig, childConfig, parentValue}) {
1438 + const [getSnapshot, subscribe] = childConfig;
1439 + const childValue = useMutableSource(
1440 + mutableSource,
1441 + getSnapshot,
1442 + subscribe,
1443 + );
1444
1436 - Scheduler.unstable_yieldValue('Child: ' + childValue);
1445 + Scheduler.unstable_yieldValue('Child: ' + childValue);
1446
1438 - let result = `${parentValue}, ${childValue}`;
1447 + let result = `${parentValue}, ${childValue}`;
1448
1440 - if (parentConfig === childConfig) {
1441 - // When both components read using the same config, the two values
1442 - // must be consistent.
1443 - if (parentValue !== childValue) {
1444 - result = 'Oops, tearing!';
1445 - }
1449 + if (parentConfig === childConfig) {
1450 + // When both components read using the same config, the two values
1451 + // must be consistent.
1452 + if (parentValue !== childValue) {
1453 + result = 'Oops, tearing!';
1454 }
1447 -
1448 - useEffect(() => {
1449 - Scheduler.unstable_yieldValue('Commit: ' + result);
1450 - }, [result]);
1451 -
1452 - return result;
1455 }
1456
1455 - const root = ReactNoop.createRoot();
1456 - await act(async () => {
1457 - root.render(<App parentConfig={configA} childConfig={configB} />);
1458 - });
1459 - expect(Scheduler).toHaveYielded([
1460 - 'Parent: 1',
1461 - 'Child: 2',
1462 - 'Commit: 1, 2',
1463 - ]);
1457 + useEffect(() => {
1458 + Scheduler.unstable_yieldValue('Commit: ' + result);
1459 + }, [result]);
1460
1465 - await act(async () => {
1466 - // Switch the parent and the child to read using the same config
1467 - root.render(<App parentConfig={configB} childConfig={configB} />);
1468 - // Start rendering the parent, but yield before rendering the child
1469 - expect(Scheduler).toFlushAndYieldThrough(['Parent: 2']);
1470 -
1471 - // Mutate the config. This is at lower priority so that 1) to make sure
1472 - // it doesn't happen to get batched with the in-progress render, and 2)
1473 - // so it doesn't interrupt the in-progress render.
1474 - Scheduler.unstable_runWithPriority(
1475 - Scheduler.unstable_IdlePriority,
1476 - () => {
1477 - source.valueB = '3';
1478 - },
1479 - );
1480 - });
1461 + return result;
1462 + }
1463
1482 - expect(Scheduler).toHaveYielded([
1483 - // The partial render completes
1484 - 'Child: 2',
1485 - 'Commit: 2, 2',
1464 + const root = ReactNoop.createRoot();
1465 + await act(async () => {
1466 + root.render(<App parentConfig={configA} childConfig={configB} />);
1467 + });
1468 + expect(Scheduler).toHaveYielded(['Parent: 1', 'Child: 2', 'Commit: 1, 2']);
1469 +
1470 + await act(async () => {
1471 + // Switch the parent and the child to read using the same config
1472 + root.render(<App parentConfig={configB} childConfig={configB} />);
1473 + // Start rendering the parent, but yield before rendering the child
1474 + expect(Scheduler).toFlushAndYieldThrough(['Parent: 2']);
1475 +
1476 + // Mutate the config. This is at lower priority so that 1) to make sure
1477 + // it doesn't happen to get batched with the in-progress render, and 2)
1478 + // so it doesn't interrupt the in-progress render.
1479 + Scheduler.unstable_runWithPriority(
1480 + Scheduler.unstable_IdlePriority,
1481 + () => {
1482 + source.valueB = '3';
1483 + },
1484 + );
1485 + });
1486
1487 - // Then we start rendering the low priority mutation
1488 - 'Parent: 3',
1487 + expect(Scheduler).toHaveYielded([
1488 + // The partial render completes
1489 + 'Child: 2',
1490 + 'Commit: 2, 2',
1491
1490 - // Eventually the child corrects itself, because of the check that
1491 - // occurs when re-subscribing.
1492 - 'Child: 3',
1493 - 'Commit: 3, 3',
1494 - ]);
1495 - });
1492 + // Then we start rendering the low priority mutation
1493 + 'Parent: 3',
1494
1497 - it('should not tear with newly mounted component when updates were scheduled at a lower priority', async () => {
1498 - const source = createSource('one');
1499 - const mutableSource = createMutableSource(source);
1495 + // Eventually the child corrects itself, because of the check that
1496 + // occurs when re-subscribing.
1497 + 'Child: 3',
1498 + 'Commit: 3, 3',
1499 + ]);
1500 + });
1501
1501 - let committedA = null;
1502 - let committedB = null;
1502 + // @gate experimental
1503 + it('should not tear with newly mounted component when updates were scheduled at a lower priority', async () => {
1504 + const source = createSource('one');
1505 + const mutableSource = createMutableSource(source);
1506
1504 - const onRender = () => {
1505 - if (committedB !== null) {
1506 - expect(committedA).toBe(committedB);
1507 - }
1508 - };
1507 + let committedA = null;
1508 + let committedB = null;
1509
1510 - function ComponentA() {
1511 - const snapshot = useMutableSource(
1512 - mutableSource,
1513 - defaultGetSnapshot,
1514 - defaultSubscribe,
1515 - );
1516 - Scheduler.unstable_yieldValue(`a:${snapshot}`);
1517 - React.useEffect(() => {
1518 - committedA = snapshot;
1519 - }, [snapshot]);
1520 - return <div>{`a:${snapshot}`}</div>;
1521 - }
1522 - function ComponentB() {
1523 - const snapshot = useMutableSource(
1524 - mutableSource,
1525 - defaultGetSnapshot,
1526 - defaultSubscribe,
1527 - );
1528 - Scheduler.unstable_yieldValue(`b:${snapshot}`);
1529 - React.useEffect(() => {
1530 - committedB = snapshot;
1531 - }, [snapshot]);
1532 - return <div>{`b:${snapshot}`}</div>;
1510 + const onRender = () => {
1511 + if (committedB !== null) {
1512 + expect(committedA).toBe(committedB);
1513 }
1514 + };
1515
1535 - // Mount ComponentA with data version 1
1536 - act(() => {
1537 - ReactNoop.render(
1538 - <React.Profiler id="root" onRender={onRender}>
1539 - <ComponentA />
1540 - </React.Profiler>,
1541 - () => Scheduler.unstable_yieldValue('Sync effect'),
1542 - );
1543 - });
1544 - expect(Scheduler).toHaveYielded(['a:one', 'Sync effect']);
1516 + function ComponentA() {
1517 + const snapshot = useMutableSource(
1518 + mutableSource,
1519 + defaultGetSnapshot,
1520 + defaultSubscribe,
1521 + );
1522 + Scheduler.unstable_yieldValue(`a:${snapshot}`);
1523 + React.useEffect(() => {
1524 + committedA = snapshot;
1525 + }, [snapshot]);
1526 + return <div>{`a:${snapshot}`}</div>;
1527 + }
1528 + function ComponentB() {
1529 + const snapshot = useMutableSource(
1530 + mutableSource,
1531 + defaultGetSnapshot,
1532 + defaultSubscribe,
1533 + );
1534 + Scheduler.unstable_yieldValue(`b:${snapshot}`);
1535 + React.useEffect(() => {
1536 + committedB = snapshot;
1537 + }, [snapshot]);
1538 + return <div>{`b:${snapshot}`}</div>;
1539 + }
1540 +
1541 + // Mount ComponentA with data version 1
1542 + act(() => {
1543 + ReactNoop.render(
1544 + <React.Profiler id="root" onRender={onRender}>
1545 + <ComponentA />
1546 + </React.Profiler>,
1547 + () => Scheduler.unstable_yieldValue('Sync effect'),
1548 + );
1549 + });
1550 + expect(Scheduler).toHaveYielded(['a:one', 'Sync effect']);
1551 + expect(source.listenerCount).toBe(1);
1552 +
1553 + // Mount ComponentB with version 1 (but don't commit it)
1554 + act(() => {
1555 + ReactNoop.render(
1556 + <React.Profiler id="root" onRender={onRender}>
1557 + <ComponentA />
1558 + <ComponentB />
1559 + </React.Profiler>,
1560 + () => Scheduler.unstable_yieldValue('Sync effect'),
1561 + );
1562 + expect(Scheduler).toFlushAndYieldThrough([
1563 + 'a:one',
1564 + 'b:one',
1565 + 'Sync effect',
1566 + ]);
1567 expect(source.listenerCount).toBe(1);
1568
1547 - // Mount ComponentB with version 1 (but don't commit it)
1548 - act(() => {
1549 - ReactNoop.render(
1550 - <React.Profiler id="root" onRender={onRender}>
1551 - <ComponentA />
1552 - <ComponentB />
1553 - </React.Profiler>,
1554 - () => Scheduler.unstable_yieldValue('Sync effect'),
1555 - );
1556 - expect(Scheduler).toFlushAndYieldThrough([
1557 - 'a:one',
1558 - 'b:one',
1559 - 'Sync effect',
1560 - ]);
1561 - expect(source.listenerCount).toBe(1);
1562 -
1563 - // Mutate -> schedule update for ComponentA
1564 - Scheduler.unstable_runWithPriority(
1565 - Scheduler.unstable_IdlePriority,
1566 - () => {
1567 - source.value = 'two';
1568 - },
1569 - );
1569 + // Mutate -> schedule update for ComponentA
1570 + Scheduler.unstable_runWithPriority(
1571 + Scheduler.unstable_IdlePriority,
1572 + () => {
1573 + source.value = 'two';
1574 + },
1575 + );
1576
1571 - // Commit ComponentB -> notice the change and schedule an update for ComponentB
1572 - expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
1573 - expect(source.listenerCount).toBe(2);
1574 - });
1577 + // Commit ComponentB -> notice the change and schedule an update for ComponentB
1578 + expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
1579 + expect(source.listenerCount).toBe(2);
1580 });
1581 + });
1582
1577 - if (__DEV__) {
1578 - describe('dev warnings', () => {
1579 - it('should warn if the subscribe function does not return an unsubscribe function', () => {
1580 - const source = createSource('one');
1581 - const mutableSource = createMutableSource(source);
1582 -
1583 - const brokenSubscribe = () => {};
1584 -
1585 - expect(() => {
1586 - act(() => {
1587 - ReactNoop.render(
1588 - <Component
1589 - label="only"
1590 - getSnapshot={defaultGetSnapshot}
1591 - mutableSource={mutableSource}
1592 - subscribe={brokenSubscribe}
1593 - />,
1594 - );
1595 - });
1596 - }).toErrorDev(
1597 - 'Mutable source subscribe function must return an unsubscribe function.',
1598 - );
1599 - });
1583 + if (__DEV__) {
1584 + describe('dev warnings', () => {
1585 + // @gate experimental
1586 + it('should warn if the subscribe function does not return an unsubscribe function', () => {
1587 + const source = createSource('one');
1588 + const mutableSource = createMutableSource(source);
1589
1601 - it('should error if multiple renderers of the same type use a mutable source at the same time', () => {
1602 - const source = createSource('one');
1603 - const mutableSource = createMutableSource(source);
1590 + const brokenSubscribe = () => {};
1591
1592 + expect(() => {
1593 act(() => {
1606 - // Start a render that uses the mutable source.
1594 ReactNoop.render(
1608 - <>
1609 - <Component
1610 - label="a"
1611 - getSnapshot={defaultGetSnapshot}
1612 - mutableSource={mutableSource}
1613 - subscribe={defaultSubscribe}
1614 - />
1615 - <Component
1616 - label="b"
1617 - getSnapshot={defaultGetSnapshot}
1618 - mutableSource={mutableSource}
1619 - subscribe={defaultSubscribe}
1620 - />
1621 - </>,
1595 + <Component
1596 + label="only"
1597 + getSnapshot={defaultGetSnapshot}
1598 + mutableSource={mutableSource}
1599 + subscribe={brokenSubscribe}
1600 + />,
1601 );
1623 - expect(Scheduler).toFlushAndYieldThrough(['a:one']);
1624 -
1625 - const PrevScheduler = Scheduler;
1626 -
1627 - // Get a new copy of ReactNoop.
1628 - loadModules();
1602 + });
1603 + }).toErrorDev(
1604 + 'Mutable source subscribe function must return an unsubscribe function.',
1605 + );
1606 + });
1607
1630 - spyOnDev(console, 'error');
1608 + // @gate experimental
1609 + it('should error if multiple renderers of the same type use a mutable source at the same time', () => {
1610 + const source = createSource('one');
1611 + const mutableSource = createMutableSource(source);
1612
1632 - // Use the mutablesource again but with a different renderer.
1633 - ReactNoop.render(
1613 + act(() => {
1614 + // Start a render that uses the mutable source.
1615 + ReactNoop.render(
1616 + <>
1617 <Component
1635 - label="c"
1618 + label="a"
1619 getSnapshot={defaultGetSnapshot}
1620 mutableSource={mutableSource}
1621 subscribe={defaultSubscribe}
1639 - />,
1640 - );
1641 - expect(Scheduler).toFlushAndYieldThrough(['c:one']);
1642 -
1643 - expect(console.error.calls.argsFor(0)[0]).toContain(
1644 - 'Detected multiple renderers concurrently rendering the ' +
1645 - 'same mutable source. This is currently unsupported.',
1646 - );
1622 + />
1623 + <Component
1624 + label="b"
1625 + getSnapshot={defaultGetSnapshot}
1626 + mutableSource={mutableSource}
1627 + subscribe={defaultSubscribe}
1628 + />
1629 + </>,
1630 + );
1631 + expect(Scheduler).toFlushAndYieldThrough(['a:one']);
1632
1648 - // TODO (useMutableSource) Act will automatically flush remaining work from render 1,
1649 - // but at this point something in the hooks dispatcher has been broken by jest.resetModules()
1650 - // Figure out what this is and remove this catch.
1651 - expect(() =>
1652 - PrevScheduler.unstable_flushAllWithoutAsserting(),
1653 - ).toThrow('Invalid hook call');
1654 - });
1655 - });
1633 + const PrevScheduler = Scheduler;
1634
1657 - it('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', () => {
1658 - const source = createSource('one');
1659 - const mutableSource = createMutableSource(source);
1635 + // Get a new copy of ReactNoop.
1636 + loadModules();
1637
1661 - act(() => {
1662 - // Start a render that uses the mutable source.
1663 - ReactNoop.render(
1664 - <>
1665 - <Component
1666 - label="a"
1667 - getSnapshot={defaultGetSnapshot}
1668 - mutableSource={mutableSource}
1669 - subscribe={defaultSubscribe}
1670 - />
1671 - <Component
1672 - label="b"
1673 - getSnapshot={defaultGetSnapshot}
1674 - mutableSource={mutableSource}
1675 - subscribe={defaultSubscribe}
1676 - />
1677 - </>,
1678 - );
1679 - expect(Scheduler).toFlushAndYieldThrough(['a:one']);
1638 + spyOnDev(console, 'error');
1639
1681 - const PrevScheduler = Scheduler;
1640 + // Use the mutablesource again but with a different renderer.
1641 + ReactNoop.render(
1642 + <Component
1643 + label="c"
1644 + getSnapshot={defaultGetSnapshot}
1645 + mutableSource={mutableSource}
1646 + subscribe={defaultSubscribe}
1647 + />,
1648 + );
1649 + expect(Scheduler).toFlushAndYieldThrough(['c:one']);
1650
1683 - // Get a new copy of ReactNoop.
1684 - loadModules();
1651 + expect(console.error.calls.argsFor(0)[0]).toContain(
1652 + 'Detected multiple renderers concurrently rendering the ' +
1653 + 'same mutable source. This is currently unsupported.',
1654 + );
1655
1686 - spyOnDev(console, 'error');
1656 + // TODO (useMutableSource) Act will automatically flush remaining work from render 1,
1657 + // but at this point something in the hooks dispatcher has been broken by jest.resetModules()
1658 + // Figure out what this is and remove this catch.
1659 + expect(() =>
1660 + PrevScheduler.unstable_flushAllWithoutAsserting(),
1661 + ).toThrow('Invalid hook call');
1662 + });
1663 + });
1664
1688 - // Mutate before the new render reads from the source.
1689 - source.value = 'two';
1665 + // @gate experimental
1666 + it('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', () => {
1667 + const source = createSource('one');
1668 + const mutableSource = createMutableSource(source);
1669
1691 - // Use the mutablesource again but with a different renderer.
1692 - ReactNoop.render(
1670 + act(() => {
1671 + // Start a render that uses the mutable source.
1672 + ReactNoop.render(
1673 + <>
1674 <Component
1694 - label="c"
1675 + label="a"
1676 getSnapshot={defaultGetSnapshot}
1677 mutableSource={mutableSource}
1678 subscribe={defaultSubscribe}
1698 - />,
1699 - );
1700 - expect(Scheduler).toFlushAndYieldThrough(['c:two']);
1679 + />
1680 + <Component
1681 + label="b"
1682 + getSnapshot={defaultGetSnapshot}
1683 + mutableSource={mutableSource}
1684 + subscribe={defaultSubscribe}
1685 + />
1686 + </>,
1687 + );
1688 + expect(Scheduler).toFlushAndYieldThrough(['a:one']);
1689
1702 - expect(console.error.calls.argsFor(0)[0]).toContain(
1703 - 'Detected multiple renderers concurrently rendering the ' +
1704 - 'same mutable source. This is currently unsupported.',
1705 - );
1690 + const PrevScheduler = Scheduler;
1691
1707 - // TODO (useMutableSource) Act will automatically flush remaining work from render 1,
1708 - // but at this point something in the hooks dispatcher has been broken by jest.resetModules()
1709 - // Figure out what this is and remove this catch.
1710 - expect(() =>
1711 - PrevScheduler.unstable_flushAllWithoutAsserting(),
1712 - ).toThrow('Invalid hook call');
1713 - });
1692 + // Get a new copy of ReactNoop.
1693 + loadModules();
1694 +
1695 + spyOnDev(console, 'error');
1696 +
1697 + // Mutate before the new render reads from the source.
1698 + source.value = 'two';
1699 +
1700 + // Use the mutablesource again but with a different renderer.
1701 + ReactNoop.render(
1702 + <Component
1703 + label="c"
1704 + getSnapshot={defaultGetSnapshot}
1705 + mutableSource={mutableSource}
1706 + subscribe={defaultSubscribe}
1707 + />,
1708 + );
1709 + expect(Scheduler).toFlushAndYieldThrough(['c:two']);
1710 +
1711 + expect(console.error.calls.argsFor(0)[0]).toContain(
1712 + 'Detected multiple renderers concurrently rendering the ' +
1713 + 'same mutable source. This is currently unsupported.',
1714 + );
1715 +
1716 + // TODO (useMutableSource) Act will automatically flush remaining work from render 1,
1717 + // but at this point something in the hooks dispatcher has been broken by jest.resetModules()
1718 + // Figure out what this is and remove this catch.
1719 + expect(() =>
1720 + PrevScheduler.unstable_flushAllWithoutAsserting(),
1721 + ).toThrow('Invalid hook call');
1722 });
1723 });
1716 - }
1724 + });
1725 }
1726 });
packages/react/src/__tests__/ReactDOMTracing-test.internal.js
+12 -14
@@ -62,13 +62,9 @@ describe('ReactDOMTracing', () => {
62 loadModules();
63 });
64
65 - if (!__EXPERIMENTAL__) {
66 - it("empty test so Jest doesn't complain", () => {});
67 - return;
68 - }
69 -
65 describe('interaction tracing', () => {
66 describe('hidden', () => {
67 + // @gate experimental
68 it('traces interaction through hidden subtree', () => {
69 const Child = () => {
70 const [didMount, setDidMount] = React.useState(false);
@@ -145,6 +141,7 @@ describe('ReactDOMTracing', () => {
141 );
142 });
143
144 + // @gate experimental
145 it('traces interaction through hidden subtree when there is other pending traced work', () => {
146 const Child = () => {
147 Scheduler.unstable_yieldValue('Child');
@@ -212,6 +209,7 @@ describe('ReactDOMTracing', () => {
209 ).toHaveBeenLastNotifiedOfInteraction(interaction);
210 });
211
212 + // @gate experimental
213 it('traces interaction through hidden subtree that schedules more idle/never work', () => {
214 const Child = () => {
215 const [didMount, setDidMount] = React.useState(false);
@@ -294,6 +292,7 @@ describe('ReactDOMTracing', () => {
292 );
293 });
294
295 + // @gate experimental
296 it('does not continue interactions across pre-existing idle work', () => {
297 const Child = () => {
298 Scheduler.unstable_yieldValue('Child');
@@ -394,6 +393,7 @@ describe('ReactDOMTracing', () => {
393 });
394 });
395
396 + // @gate experimental
397 it('should properly trace interactions when there is work of interleaved priorities', () => {
398 const Child = () => {
399 Scheduler.unstable_yieldValue('Child');
@@ -515,6 +515,7 @@ describe('ReactDOMTracing', () => {
515 });
516 });
517
518 + // @gate experimental
519 it('should properly trace interactions through a multi-pass SuspenseList render', () => {
520 const SuspenseList = React.SuspenseList;
521 const Suspense = React.Suspense;
@@ -599,7 +600,8 @@ describe('ReactDOMTracing', () => {
600 });
601
602 describe('hydration', () => {
602 - it('traces interaction across hydration', async done => {
603 + // @gate experimental
604 + it('traces interaction across hydration', () => {
605 const ref = React.createRef();
606
607 function Child() {
@@ -644,11 +646,10 @@ describe('ReactDOMTracing', () => {
646 expect(
647 onInteractionScheduledWorkCompleted,
648 ).toHaveBeenLastNotifiedOfInteraction(interaction);
647 -
648 - done();
649 });
650
651 - it('traces interaction across suspended hydration', async done => {
651 + // @gate experimental
652 + it('traces interaction across suspended hydration', async () => {
653 let suspend = false;
654 let resolve;
655 const promise = new Promise(
@@ -716,11 +717,10 @@ describe('ReactDOMTracing', () => {
717 expect(
718 onInteractionScheduledWorkCompleted,
719 ).toHaveBeenLastNotifiedOfInteraction(interaction);
719 -
720 - done();
720 });
721
723 - it('traces interaction across client-rendered hydration', async done => {
722 + // @gate experimental
723 + it('traces interaction across client-rendered hydration', () => {
724 let suspend = false;
725 const promise = new Promise(() => {});
726 const ref = React.createRef();
@@ -788,8 +788,6 @@ describe('ReactDOMTracing', () => {
788 expect(
789 onInteractionScheduledWorkCompleted,
790 ).toHaveBeenLastNotifiedOfInteraction(interaction);
791 -
792 - done();
791 });
792 });
793 });
packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
+1 -5
@@ -53,11 +53,7 @@ describe('ProfilerDOM', () => {
53 return props.text;
54 }
55
56 - if (!__EXPERIMENTAL__) {
57 - it("empty test so Jest doesn't complain", () => {});
58 - return;
59 - }
60 -
56 + // @gate experimental
57 it('should correctly trace interactions for async roots', async () => {
58 let resolve;
59 let thenable = {
packages/shared/__tests__/ReactError-test.internal.js
+24 -25
@@ -36,29 +36,28 @@ describe('ReactError', () => {
36 }
37 });
38
39 - if (__DEV__) {
40 - it("empty test so Jest doesn't complain", () => {});
41 - } else {
42 - it('should error with minified error code', () => {
43 - expect(() => ReactDOM.render('Hi', null)).toThrowError(
44 - 'Minified React error #200; visit ' +
45 - 'https://reactjs.org/docs/error-decoder.html?invariant=200' +
46 - ' for the full message or use the non-minified dev environment' +
47 - ' for full errors and additional helpful warnings.',
48 - );
49 - });
50 - it('should serialize arguments', () => {
51 - function Oops() {
52 - return;
53 - }
54 - Oops.displayName = '#wtf';
55 - const container = document.createElement('div');
56 - expect(() => ReactDOM.render(<Oops />, container)).toThrowError(
57 - 'Minified React error #152; visit ' +
58 - 'https://reactjs.org/docs/error-decoder.html?invariant=152&args[]=%23wtf' +
59 - ' for the full message or use the non-minified dev environment' +
60 - ' for full errors and additional helpful warnings.',
61 - );
62 - });
63 - }
39 + // @gate build === "production"
40 + it('should error with minified error code', () => {
41 + expect(() => ReactDOM.render('Hi', null)).toThrowError(
42 + 'Minified React error #200; visit ' +
43 + 'https://reactjs.org/docs/error-decoder.html?invariant=200' +
44 + ' for the full message or use the non-minified dev environment' +
45 + ' for full errors and additional helpful warnings.',
46 + );
47 + });
48 +
49 + // @gate build === "production"
50 + it('should serialize arguments', () => {
51 + function Oops() {
52 + return;
53 + }
54 + Oops.displayName = '#wtf';
55 + const container = document.createElement('div');
56 + expect(() => ReactDOM.render(<Oops />, container)).toThrowError(
57 + 'Minified React error #152; visit ' +
58 + 'https://reactjs.org/docs/error-decoder.html?invariant=152&args[]=%23wtf' +
59 + ' for the full message or use the non-minified dev environment' +
60 + ' for full errors and additional helpful warnings.',
61 + );
62 + });
63 });
packages/shared/__tests__/describeComponentFrame-test.js
+1 -6
@@ -11,7 +11,6 @@
11
12 let React;
13 let ReactDOM;
14 -const ReactFeatureFlags = require('shared/ReactFeatureFlags');
14
15 describe('Component stack trace displaying', () => {
16 beforeEach(() => {
@@ -19,11 +18,7 @@ describe('Component stack trace displaying', () => {
18 ReactDOM = require('react-dom');
19 });
20
22 - if (ReactFeatureFlags.enableComponentStackLocations) {
23 - it("empty test so Jest doesn't complain", () => {});
24 - return;
25 - }
26 -
21 + // @gate !enableComponentStackLocations || !__DEV__
22 it('should provide filenames in stack traces', () => {
23 class Component extends React.Component {
24 render() {
scripts/jest/TestFlags.js
+21
@@ -35,7 +35,11 @@
35 const environmentFlags = {
36 __DEV__,
37 build: __DEV__ ? 'development' : 'production',
38 +
39 + // TODO: Should "experimental" also imply "modern"? Maybe we should
40 + // always compare to the channel?
41 experimental: __EXPERIMENTAL__,
42 + // Similarly, should stable imply "classic"?
43 stable: !__EXPERIMENTAL__,
44 };
45
@@ -44,6 +48,18 @@ function getTestFlags() {
48 // not to but there are exceptions.
49 const featureFlags = require('shared/ReactFeatureFlags');
50
51 + // TODO: This is a heuristic to detect the release channel by checking a flag
52 + // that is known to only be enabled in www. What we should do instead is set
53 + // the release channel explicitly in the each test config file.
54 + const www = featureFlags.enableSuspenseCallback === true;
55 + const releaseChannel = www
56 + ? __EXPERIMENTAL__
57 + ? 'modern'
58 + : 'classic'
59 + : __EXPERIMENTAL__
60 + ? 'experimental'
61 + : 'stable';
62 +
63 // Return a proxy so we can throw if you attempt to access a flag that
64 // doesn't exist.
65 return new Proxy(
@@ -52,6 +68,11 @@ function getTestFlags() {
68 old: featureFlags.enableNewReconciler === true,
69 new: featureFlags.enableNewReconciler === true,
70
71 + channel: releaseChannel,
72 + modern: releaseChannel === 'modern',
73 + classic: releaseChannel === 'classic',
74 + www,
75 +
76 ...featureFlags,
77 ...environmentFlags,
78 },