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
},