diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js
index 326e3c39bf..63ec249079 100644
--- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js
+++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js
@@ -25,11 +25,7 @@ describe('ReactHooksInspection', () => {
ReactDebugTools = require('react-debug-tools');
});
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
+ // @gate experimental
it('should inspect a simple useResponder hook', () => {
const TestResponder = React.DEPRECATED_createResponder('TestResponder', {});
@@ -51,6 +47,7 @@ describe('ReactHooksInspection', () => {
]);
});
+ // @gate experimental
it('should inspect a simple ReactDOM.useEvent hook', () => {
let clickHandle;
let ref;
diff --git a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js
index b972cf3177..360b78a3ad 100644
--- a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js
+++ b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js
@@ -27,6 +27,7 @@ describe('StoreStressConcurrent', () => {
print = require('./storeSerializer').print;
});
+ // TODO: Remove this in favor of @gate pragma
if (!__EXPERIMENTAL__) {
it("empty test so Jest doesn't complain", () => {});
return;
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index 10cd143c54..773ab3232a 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -87,11 +87,7 @@ describe('ReactDOMServerPartialHydration', () => {
SuspenseList = React.SuspenseList;
});
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
+ // @gate experimental
it('hydrates a parent even if a child Suspense boundary is blocked', async () => {
let suspend = false;
let resolve;
@@ -150,6 +146,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
});
+ // @gate experimental
it('calls the hydration callbacks after hydration or deletion', async () => {
let suspend = false;
let resolve;
@@ -240,6 +237,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(deleted.length).toBe(1);
});
+ // @gate experimental
it('calls the onDeleted hydration callback if the parent gets deleted', async () => {
let suspend = false;
const promise = new Promise(() => {});
@@ -297,6 +295,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(deleted.length).toBe(1);
});
+ // @gate experimental || www
it('warns and replaces the boundary content in legacy mode', async () => {
let suspend = false;
let resolve;
@@ -368,6 +367,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('Hello');
});
+ // @gate experimental
it('can insert siblings before the dehydrated boundary', () => {
let suspend = false;
const promise = new Promise(() => {});
@@ -426,6 +426,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.firstChild.firstChild.textContent).toBe('First');
});
+ // @gate experimental
it('can delete the dehydrated boundary before it is hydrated', () => {
let suspend = false;
const promise = new Promise(() => {});
@@ -482,6 +483,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.firstChild.children[1].textContent).toBe('After');
});
+ // @gate experimental
it('blocks updates to hydrate the content first if props have changed', async () => {
let suspend = false;
let resolve;
@@ -553,6 +555,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(span.className).toBe('hi');
});
+ // @gate experimental
it('shows the fallback if props have changed before hydration completes and is still suspended', async () => {
let suspend = false;
let resolve;
@@ -622,6 +625,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('Hi');
});
+ // @gate experimental
it('shows the fallback of the outer if fallback is missing', async () => {
// This is the same exact test as above but with a nested Suspense without a fallback.
// This should be a noop.
@@ -695,6 +699,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('Hi');
});
+ // @gate experimental
it('clears nested suspense boundaries if they did not hydrate yet', async () => {
let suspend = false;
let resolve;
@@ -767,6 +772,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('Hi Hi');
});
+ // @gate experimental
it('hydrates first if props changed but we are able to resolve within a timeout', async () => {
let suspend = false;
let resolve;
@@ -847,6 +853,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(span.className).toBe('hi');
});
+ // @gate experimental
it('blocks the update to hydrate first if context has changed', async () => {
let suspend = false;
let resolve;
@@ -931,6 +938,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(span.className).toBe('hi');
});
+ // @gate experimental
it('shows the fallback if context has changed before hydration completes and is still suspended', async () => {
let suspend = false;
let resolve;
@@ -1014,6 +1022,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('Hi');
});
+ // @gate experimental
it('replaces the fallback with client content if it is not rendered by the server', async () => {
let suspend = false;
const promise = new Promise(resolvePromise => {});
@@ -1062,6 +1071,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
});
+ // @gate experimental
it('replaces the fallback within the suspended time if there is a nested suspense', async () => {
let suspend = false;
const promise = new Promise(resolvePromise => {});
@@ -1121,6 +1131,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
});
+ // @gate experimental
it('replaces the fallback within the suspended time if there is a nested suspense in a nested suspense', async () => {
let suspend = false;
const promise = new Promise(resolvePromise => {});
@@ -1182,6 +1193,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
});
+ // @gate experimental
it('waits for pending content to come in from the server and then hydrates it', async () => {
let suspend = false;
const promise = new Promise(resolvePromise => {});
@@ -1269,6 +1281,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
});
+ // @gate experimental
it('handles an error on the client if the server ends up erroring', async () => {
let suspend = false;
const promise = new Promise(resolvePromise => {});
@@ -1363,6 +1376,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(div);
});
+ // @gate experimental
it('shows inserted items in a SuspenseList before content is hydrated', async () => {
let suspend = false;
let resolve;
@@ -1448,6 +1462,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(spanB);
});
+ // @gate experimental
it('shows is able to hydrate boundaries even if others in a list are pending', async () => {
let suspend = false;
let resolve;
@@ -1522,6 +1537,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('ALoading B');
});
+ // @gate experimental
it('shows inserted items before pending in a SuspenseList as fallbacks', async () => {
let suspend = false;
let resolve;
@@ -1613,6 +1629,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('ABC');
});
+ // @gate experimental
it('can client render nested boundaries', async () => {
let suspend = false;
const promise = new Promise(() => {});
@@ -1667,6 +1684,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.lastChild.data).toBe('unrelated comment');
});
+ // @gate experimental
it('can hydrate TWO suspense boundaries', async () => {
const ref1 = React.createRef();
const ref2 = React.createRef();
@@ -1706,6 +1724,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref2.current).toBe(span2);
});
+ // @gate experimental
it('regenerates if it cannot hydrate before changes to props/context expire', async () => {
let suspend = false;
const promise = new Promise(resolvePromise => {});
@@ -1787,6 +1806,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(newSpan.className).toBe('hi');
});
+ // @gate experimental
it('does not invoke an event on a hydrated node until it commits', async () => {
let suspend = false;
let resolve;
@@ -1868,6 +1888,7 @@ describe('ReactDOMServerPartialHydration', () => {
document.body.removeChild(container);
});
+ // @gate experimental
it('does not invoke an event on a hydrated EventResponder until it commits', async () => {
let suspend = false;
let resolve;
@@ -1949,6 +1970,7 @@ describe('ReactDOMServerPartialHydration', () => {
document.body.removeChild(container);
});
+ // @gate experimental
it('invokes discrete events on nested suspense boundaries in a root (legacy system)', async () => {
let suspend = false;
let resolve;
@@ -2028,6 +2050,7 @@ describe('ReactDOMServerPartialHydration', () => {
document.body.removeChild(container);
});
+ // @gate experimental
it('invokes discrete events on nested suspense boundaries in a root (responder system)', async () => {
let suspend = false;
let resolve;
@@ -2110,6 +2133,7 @@ describe('ReactDOMServerPartialHydration', () => {
document.body.removeChild(container);
});
+ // @gate experimental
it('does not invoke the parent of dehydrated boundary event', async () => {
let suspend = false;
let resolve;
@@ -2184,6 +2208,7 @@ describe('ReactDOMServerPartialHydration', () => {
document.body.removeChild(container);
});
+ // @gate experimental
it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => {
let suspend = false;
let resolve;
@@ -2261,6 +2286,7 @@ describe('ReactDOMServerPartialHydration', () => {
document.body.removeChild(parentContainer);
});
+ // @gate experimental
it('blocks only on the last continuous event (legacy system)', async () => {
let suspend1 = false;
let resolve1;
@@ -2365,129 +2391,125 @@ describe('ReactDOMServerPartialHydration', () => {
document.body.removeChild(container);
});
- if (__EXPERIMENTAL__) {
- it('blocks only on the last continuous event (Responder system)', async () => {
- useHover = require('react-interactions/events/hover').useHover;
+ // @gate experimental
+ it('blocks only on the last continuous event (Responder system)', async () => {
+ useHover = require('react-interactions/events/hover').useHover;
- let suspend1 = false;
- let resolve1;
- const promise1 = new Promise(
- resolvePromise => (resolve1 = resolvePromise),
- );
- let suspend2 = false;
- let resolve2;
- const promise2 = new Promise(
- resolvePromise => (resolve2 = resolvePromise),
- );
+ let suspend1 = false;
+ let resolve1;
+ const promise1 = new Promise(resolvePromise => (resolve1 = resolvePromise));
+ let suspend2 = false;
+ let resolve2;
+ const promise2 = new Promise(resolvePromise => (resolve2 = resolvePromise));
- function First({text}) {
- if (suspend1) {
- throw promise1;
- } else {
- return 'Hello';
- }
+ function First({text}) {
+ if (suspend1) {
+ throw promise1;
+ } else {
+ return 'Hello';
}
+ }
- function Second({text}) {
- if (suspend2) {
- throw promise2;
- } else {
- return 'World';
- }
+ function Second({text}) {
+ if (suspend2) {
+ throw promise2;
+ } else {
+ return 'World';
}
+ }
- const ops = [];
+ const ops = [];
- function App() {
- const listener1 = useHover({
- onHoverStart() {
- ops.push('Hover Start First');
- },
- onHoverEnd() {
- ops.push('Hover End First');
- },
- });
- const listener2 = useHover({
- onHoverStart() {
- ops.push('Hover Start Second');
- },
- onHoverEnd() {
- ops.push('Hover End Second');
- },
- });
- return (
-
-
-
- {/* We suspend after to test what happens when we eager
+ function App() {
+ const listener1 = useHover({
+ onHoverStart() {
+ ops.push('Hover Start First');
+ },
+ onHoverEnd() {
+ ops.push('Hover End First');
+ },
+ });
+ const listener2 = useHover({
+ onHoverStart() {
+ ops.push('Hover Start Second');
+ },
+ onHoverEnd() {
+ ops.push('Hover End Second');
+ },
+ });
+ return (
+
+
+
+ {/* We suspend after to test what happens when we eager
attach the listener. */}
-
-
-
-
-
-
-
-
- );
- }
+
+
+
+
+
+
+
+
+ );
+ }
- const finalHTML = ReactDOMServer.renderToString( );
- const container = document.createElement('div');
- container.innerHTML = finalHTML;
+ const finalHTML = ReactDOMServer.renderToString( );
+ const container = document.createElement('div');
+ container.innerHTML = finalHTML;
- // We need this to be in the document since we'll dispatch events on it.
- document.body.appendChild(container);
+ // We need this to be in the document since we'll dispatch events on it.
+ document.body.appendChild(container);
- const appDiv = container.getElementsByTagName('div')[0];
- const firstSpan = appDiv.getElementsByTagName('span')[0];
- const secondSpan = appDiv.getElementsByTagName('span')[1];
- expect(firstSpan.textContent).toBe('');
- expect(secondSpan.textContent).toBe('World');
+ const appDiv = container.getElementsByTagName('div')[0];
+ const firstSpan = appDiv.getElementsByTagName('span')[0];
+ const secondSpan = appDiv.getElementsByTagName('span')[1];
+ expect(firstSpan.textContent).toBe('');
+ expect(secondSpan.textContent).toBe('World');
- // On the client we don't have all data yet but we want to start
- // hydrating anyway.
- suspend1 = true;
- suspend2 = true;
- const root = ReactDOM.createRoot(container, {hydrate: true});
- root.render( );
+ // On the client we don't have all data yet but we want to start
+ // hydrating anyway.
+ suspend1 = true;
+ suspend2 = true;
+ const root = ReactDOM.createRoot(container, {hydrate: true});
+ root.render( );
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
- dispatchMouseEvent(appDiv, null);
- dispatchMouseEvent(firstSpan, appDiv);
- dispatchMouseEvent(secondSpan, firstSpan);
+ dispatchMouseEvent(appDiv, null);
+ dispatchMouseEvent(firstSpan, appDiv);
+ dispatchMouseEvent(secondSpan, firstSpan);
- // Neither target is yet hydrated.
- expect(ops).toEqual([]);
+ // Neither target is yet hydrated.
+ expect(ops).toEqual([]);
- // Resolving the second promise so that rendering can complete.
- suspend2 = false;
- resolve2();
- await promise2;
+ // Resolving the second promise so that rendering can complete.
+ suspend2 = false;
+ resolve2();
+ await promise2;
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
- // We've unblocked the current hover target so we should be
- // able to replay it now.
- expect(ops).toEqual(['Hover Start Second']);
+ // We've unblocked the current hover target so we should be
+ // able to replay it now.
+ expect(ops).toEqual(['Hover Start Second']);
- // Resolving the first promise has no effect now.
- suspend1 = false;
- resolve1();
- await promise1;
+ // Resolving the first promise has no effect now.
+ suspend1 = false;
+ resolve1();
+ await promise1;
- Scheduler.unstable_flushAll();
- jest.runAllTimers();
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
- expect(ops).toEqual(['Hover Start Second']);
+ expect(ops).toEqual(['Hover Start Second']);
- document.body.removeChild(container);
- });
- }
+ document.body.removeChild(container);
+ });
+ // @gate experimental
it('finishes normal pri work before continuing to hydrate a retry', async () => {
let suspend = false;
let resolve;
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
index 4b02cd9c73..b50023c1d6 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
@@ -106,11 +106,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
Suspense = React.Suspense;
});
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
+ // @gate experimental
it('hydrates the target boundary synchronously during a click', async () => {
function Child({text}) {
Scheduler.unstable_yieldValue(text);
@@ -173,6 +169,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});
+ // @gate experimental
it('hydrates at higher pri if sync did not work first time', async () => {
let suspend = false;
let resolve;
@@ -257,6 +254,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});
+ // @gate experimental
it('hydrates at higher pri for secondary discrete events', async () => {
let suspend = false;
let resolve;
@@ -349,247 +347,249 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});
- if (__EXPERIMENTAL__) {
- it('hydrates the target boundary synchronously during a click (flare)', async () => {
- const usePress = require('react-interactions/events/press').usePress;
+ // @gate experimental
+ it('hydrates the target boundary synchronously during a click (flare)', async () => {
+ const usePress = require('react-interactions/events/press').usePress;
- function Child({text}) {
- Scheduler.unstable_yieldValue(text);
- const listener = usePress({
- onPress() {
- Scheduler.unstable_yieldValue('Clicked ' + text);
- },
- });
+ function Child({text}) {
+ Scheduler.unstable_yieldValue(text);
+ const listener = usePress({
+ onPress() {
+ Scheduler.unstable_yieldValue('Clicked ' + text);
+ },
+ });
- return {text} ;
+ return {text} ;
+ }
+
+ function App() {
+ Scheduler.unstable_yieldValue('App');
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ const finalHTML = ReactDOMServer.renderToString( );
+
+ expect(Scheduler).toHaveYielded(['App', 'A', 'B']);
+
+ const container = document.createElement('div');
+ // We need this to be in the document since we'll dispatch events on it.
+ document.body.appendChild(container);
+
+ container.innerHTML = finalHTML;
+
+ const root = ReactDOM.createRoot(container, {hydrate: true});
+ root.render( );
+
+ // Nothing has been hydrated so far.
+ expect(Scheduler).toHaveYielded([]);
+
+ const span = container.getElementsByTagName('span')[1];
+
+ const target = createEventTarget(span);
+
+ // This should synchronously hydrate the root App and the second suspense
+ // boundary.
+ const preventDefault = jest.fn();
+ target.virtualclick({preventDefault});
+
+ // The event should have been canceled because we called preventDefault.
+ expect(preventDefault).toHaveBeenCalled();
+
+ // We rendered App, B and then invoked the event without rendering A.
+ expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']);
+
+ // After continuing the scheduler, we finally hydrate A.
+ expect(Scheduler).toFlushAndYield(['A']);
+
+ document.body.removeChild(container);
+ });
+
+ // @gate experimental
+ it('hydrates at higher pri if sync did not work first time (flare)', async () => {
+ const usePress = require('react-interactions/events/press').usePress;
+ let suspend = false;
+ let resolve;
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
+
+ function Child({text}) {
+ if ((text === 'A' || text === 'D') && suspend) {
+ throw promise;
}
+ Scheduler.unstable_yieldValue(text);
- function App() {
- Scheduler.unstable_yieldValue('App');
- return (
-
-
-
-
-
-
-
-
- );
+ const listener = usePress({
+ onPress() {
+ Scheduler.unstable_yieldValue('Clicked ' + text);
+ },
+ });
+ return {text} ;
+ }
+
+ function App() {
+ Scheduler.unstable_yieldValue('App');
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ const finalHTML = ReactDOMServer.renderToString( );
+
+ expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
+
+ const container = document.createElement('div');
+ // We need this to be in the document since we'll dispatch events on it.
+ document.body.appendChild(container);
+
+ container.innerHTML = finalHTML;
+
+ const spanD = container.getElementsByTagName('span')[3];
+
+ suspend = true;
+
+ // A and D will be suspended. We'll click on D which should take
+ // priority, after we unsuspend.
+ const root = ReactDOM.createRoot(container, {hydrate: true});
+ root.render( );
+
+ // Nothing has been hydrated so far.
+ expect(Scheduler).toHaveYielded([]);
+
+ // This click target cannot be hydrated yet because it's suspended.
+ const result = dispatchClickEvent(spanD);
+
+ expect(Scheduler).toHaveYielded(['App']);
+
+ expect(result).toBe(true);
+
+ // Continuing rendering will render B next.
+ expect(Scheduler).toFlushAndYield(['B', 'C']);
+
+ suspend = false;
+ resolve();
+ await promise;
+
+ // After the click, we should prioritize D and the Click first,
+ // and only after that render A and C.
+ expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']);
+
+ document.body.removeChild(container);
+ });
+
+ // @gate experimental
+ it('hydrates at higher pri for secondary discrete events (flare)', async () => {
+ const usePress = require('react-interactions/events/press').usePress;
+ let suspend = false;
+ let resolve;
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
+
+ function Child({text}) {
+ if ((text === 'A' || text === 'D') && suspend) {
+ throw promise;
}
+ Scheduler.unstable_yieldValue(text);
- const finalHTML = ReactDOMServer.renderToString( );
+ const listener = usePress({
+ onPress() {
+ Scheduler.unstable_yieldValue('Clicked ' + text);
+ },
+ });
+ return {text} ;
+ }
- expect(Scheduler).toHaveYielded(['App', 'A', 'B']);
+ function App() {
+ Scheduler.unstable_yieldValue('App');
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
- const container = document.createElement('div');
- // We need this to be in the document since we'll dispatch events on it.
- document.body.appendChild(container);
+ const finalHTML = ReactDOMServer.renderToString( );
- container.innerHTML = finalHTML;
+ expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
- const root = ReactDOM.createRoot(container, {hydrate: true});
- root.render( );
+ const container = document.createElement('div');
+ // We need this to be in the document since we'll dispatch events on it.
+ document.body.appendChild(container);
- // Nothing has been hydrated so far.
- expect(Scheduler).toHaveYielded([]);
+ container.innerHTML = finalHTML;
- const span = container.getElementsByTagName('span')[1];
+ const spanA = container.getElementsByTagName('span')[0];
+ const spanC = container.getElementsByTagName('span')[2];
+ const spanD = container.getElementsByTagName('span')[3];
- const target = createEventTarget(span);
+ suspend = true;
- // This should synchronously hydrate the root App and the second suspense
- // boundary.
- const preventDefault = jest.fn();
- target.virtualclick({preventDefault});
+ // A and D will be suspended. We'll click on D which should take
+ // priority, after we unsuspend.
+ const root = ReactDOM.createRoot(container, {hydrate: true});
+ root.render( );
- // The event should have been canceled because we called preventDefault.
- expect(preventDefault).toHaveBeenCalled();
+ // Nothing has been hydrated so far.
+ expect(Scheduler).toHaveYielded([]);
- // We rendered App, B and then invoked the event without rendering A.
- expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']);
+ // This click target cannot be hydrated yet because the first is Suspended.
+ dispatchClickEvent(spanA);
+ dispatchClickEvent(spanC);
+ dispatchClickEvent(spanD);
- // After continuing the scheduler, we finally hydrate A.
- expect(Scheduler).toFlushAndYield(['A']);
+ expect(Scheduler).toHaveYielded(['App']);
- document.body.removeChild(container);
- });
+ suspend = false;
+ resolve();
+ await promise;
- it('hydrates at higher pri if sync did not work first time (flare)', async () => {
- const usePress = require('react-interactions/events/press').usePress;
- let suspend = false;
- let resolve;
- const promise = new Promise(resolvePromise => (resolve = resolvePromise));
+ // We should prioritize hydrating A, C and D first since we clicked in
+ // them. Only after they're done will we hydrate B.
+ expect(Scheduler).toFlushAndYield([
+ 'A',
+ 'Clicked A',
+ 'C',
+ 'Clicked C',
+ 'D',
+ 'Clicked D',
+ // B should render last since it wasn't clicked.
+ 'B',
+ ]);
- function Child({text}) {
- if ((text === 'A' || text === 'D') && suspend) {
- throw promise;
- }
- Scheduler.unstable_yieldValue(text);
-
- const listener = usePress({
- onPress() {
- Scheduler.unstable_yieldValue('Clicked ' + text);
- },
- });
- return {text} ;
- }
-
- function App() {
- Scheduler.unstable_yieldValue('App');
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- const finalHTML = ReactDOMServer.renderToString( );
-
- expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
-
- const container = document.createElement('div');
- // We need this to be in the document since we'll dispatch events on it.
- document.body.appendChild(container);
-
- container.innerHTML = finalHTML;
-
- const spanD = container.getElementsByTagName('span')[3];
-
- suspend = true;
-
- // A and D will be suspended. We'll click on D which should take
- // priority, after we unsuspend.
- const root = ReactDOM.createRoot(container, {hydrate: true});
- root.render( );
-
- // Nothing has been hydrated so far.
- expect(Scheduler).toHaveYielded([]);
-
- // This click target cannot be hydrated yet because it's suspended.
- const result = dispatchClickEvent(spanD);
-
- expect(Scheduler).toHaveYielded(['App']);
-
- expect(result).toBe(true);
-
- // Continuing rendering will render B next.
- expect(Scheduler).toFlushAndYield(['B', 'C']);
-
- suspend = false;
- resolve();
- await promise;
-
- // After the click, we should prioritize D and the Click first,
- // and only after that render A and C.
- expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']);
-
- document.body.removeChild(container);
- });
-
- it('hydrates at higher pri for secondary discrete events (flare)', async () => {
- const usePress = require('react-interactions/events/press').usePress;
- let suspend = false;
- let resolve;
- const promise = new Promise(resolvePromise => (resolve = resolvePromise));
-
- function Child({text}) {
- if ((text === 'A' || text === 'D') && suspend) {
- throw promise;
- }
- Scheduler.unstable_yieldValue(text);
-
- const listener = usePress({
- onPress() {
- Scheduler.unstable_yieldValue('Clicked ' + text);
- },
- });
- return {text} ;
- }
-
- function App() {
- Scheduler.unstable_yieldValue('App');
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- const finalHTML = ReactDOMServer.renderToString( );
-
- expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
-
- const container = document.createElement('div');
- // We need this to be in the document since we'll dispatch events on it.
- document.body.appendChild(container);
-
- container.innerHTML = finalHTML;
-
- const spanA = container.getElementsByTagName('span')[0];
- const spanC = container.getElementsByTagName('span')[2];
- const spanD = container.getElementsByTagName('span')[3];
-
- suspend = true;
-
- // A and D will be suspended. We'll click on D which should take
- // priority, after we unsuspend.
- const root = ReactDOM.createRoot(container, {hydrate: true});
- root.render( );
-
- // Nothing has been hydrated so far.
- expect(Scheduler).toHaveYielded([]);
-
- // This click target cannot be hydrated yet because the first is Suspended.
- dispatchClickEvent(spanA);
- dispatchClickEvent(spanC);
- dispatchClickEvent(spanD);
-
- expect(Scheduler).toHaveYielded(['App']);
-
- suspend = false;
- resolve();
- await promise;
-
- // We should prioritize hydrating A, C and D first since we clicked in
- // them. Only after they're done will we hydrate B.
- expect(Scheduler).toFlushAndYield([
- 'A',
- 'Clicked A',
- 'C',
- 'Clicked C',
- 'D',
- 'Clicked D',
- // B should render last since it wasn't clicked.
- 'B',
- ]);
-
- document.body.removeChild(container);
- });
- }
+ document.body.removeChild(container);
+ });
+ // @gate experimental
it('hydrates the hovered targets as higher priority for continuous events', async () => {
let suspend = false;
let resolve;
@@ -690,6 +690,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});
+ // @gate experimental
it('hydrates the last target path first for continuous events', async () => {
let suspend = false;
let resolve;
@@ -775,6 +776,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});
+ // @gate experimental
it('hydrates the last explicitly hydrated target at higher priority', async () => {
function Child({text}) {
Scheduler.unstable_yieldValue(text);
@@ -823,6 +825,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
expect(Scheduler).toFlushAndYield(['App', 'C', 'B', 'A']);
});
+ // @gate experimental
it('hydrates before an update even if hydration moves away from it', async () => {
function Child({text}) {
Scheduler.unstable_yieldValue(text);
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
index 67fdc71bcc..cc3b563d0f 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
@@ -44,11 +44,6 @@ describe('ReactDOMServerSuspense', () => {
resetModules();
});
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
function Text(props) {
return {props.text}
;
}
@@ -57,6 +52,7 @@ describe('ReactDOMServerSuspense', () => {
throw new Promise(() => {});
}
+ // @gate experimental || www
it('should render the children when no promise is thrown', async () => {
const c = await serverRender(
@@ -71,6 +67,7 @@ describe('ReactDOMServerSuspense', () => {
expect(e.textContent).toBe('Children');
});
+ // @gate experimental || www
it('should render the fallback when a promise thrown', async () => {
const c = await serverRender(
@@ -85,6 +82,7 @@ describe('ReactDOMServerSuspense', () => {
expect(e.textContent).toBe('Fallback');
});
+ // @gate experimental || www
it('should work with nested suspense components', async () => {
const c = await serverRender(
@@ -105,6 +103,7 @@ describe('ReactDOMServerSuspense', () => {
);
});
+ // @gate experimental
it('server renders a SuspenseList component and its children', async () => {
const example = (
@@ -137,33 +136,36 @@ describe('ReactDOMServerSuspense', () => {
expect(divB).toBe(divB2);
});
- itThrowsWhenRendering(
- 'a suspending component outside a Suspense node',
- async render => {
- await render(
- ,
- 1,
- );
- },
- 'Add a component higher in the tree',
- );
+ // TODO: Remove this in favor of @gate pragma
+ if (__EXPERIMENTAL__) {
+ itThrowsWhenRendering(
+ 'a suspending component outside a Suspense node',
+ async render => {
+ await render(
+ ,
+ 1,
+ );
+ },
+ 'Add a component higher in the tree',
+ );
- itThrowsWhenRendering(
- 'a suspending component without a Suspense above',
- async render => {
- await render(
- ,
- 1,
- );
- },
- 'Add a component higher in the tree',
- );
+ itThrowsWhenRendering(
+ 'a suspending component without a Suspense above',
+ async render => {
+ await render(
+ ,
+ 1,
+ );
+ },
+ 'Add a component higher in the tree',
+ );
+ }
it('does not get confused by throwing null', () => {
function Bad() {
diff --git a/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js b/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js
index 678f96de9f..99d1bd92e4 100644
--- a/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js
+++ b/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js
@@ -18,324 +18,315 @@ const renderSubtreeIntoContainer = require('react-dom')
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
-// Once this flag is always true, we should delete this test file
-if (__EXPERIMENTAL__) {
- describe('renderSubtreeIntoContainer', () => {
- it('empty test', () => {
- // Empty test to prevent "Your test suite must contain at least one test." error.
- });
+describe('renderSubtreeIntoContainer', () => {
+ it('should pass context when rendering subtree elsewhere', () => {
+ const portal = document.createElement('div');
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: PropTypes.string.isRequired,
+ };
+
+ render() {
+ return {this.context.foo}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: PropTypes.string.isRequired,
+ };
+
+ getChildContext() {
+ return {
+ foo: 'bar',
+ };
+ }
+
+ render() {
+ return null;
+ }
+
+ componentDidMount() {
+ if (ReactFeatureFlags.warnUnstableRenderSubtreeIntoContainer) {
+ expect(
+ function() {
+ renderSubtreeIntoContainer(this, , portal);
+ }.bind(this),
+ ).toWarnDev(
+ 'ReactDOM.unstable_renderSubtreeIntoContainer() is deprecated and ' +
+ 'will be removed in a future major release. Consider using React Portals instead.',
+ );
+ } else {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+ }
+ }
+
+ ReactTestUtils.renderIntoDocument( );
+ expect(portal.firstChild.innerHTML).toBe('bar');
});
-} else {
- describe('renderSubtreeIntoContainer', () => {
- it('should pass context when rendering subtree elsewhere', () => {
- const portal = document.createElement('div');
- class Component extends React.Component {
- static contextTypes = {
- foo: PropTypes.string.isRequired,
- };
+ it('should throw if parentComponent is invalid', () => {
+ const portal = document.createElement('div');
- render() {
- return {this.context.foo}
;
- }
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: PropTypes.string.isRequired,
+ };
+
+ render() {
+ return {this.context.foo}
;
}
+ }
- class Parent extends React.Component {
- static childContextTypes = {
- foo: PropTypes.string.isRequired,
- };
+ // ESLint is confused here and thinks Parent is unused, presumably because
+ // it is only used inside of the class body?
+ // eslint-disable-next-line no-unused-vars
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: PropTypes.string.isRequired,
+ };
- getChildContext() {
- return {
- foo: 'bar',
- };
- }
-
- render() {
- return null;
- }
-
- componentDidMount() {
- if (ReactFeatureFlags.warnUnstableRenderSubtreeIntoContainer) {
- expect(
- function() {
- renderSubtreeIntoContainer(this, , portal);
- }.bind(this),
- ).toWarnDev(
- 'ReactDOM.unstable_renderSubtreeIntoContainer() is deprecated and ' +
- 'will be removed in a future major release. Consider using React Portals instead.',
- );
- } else {
- renderSubtreeIntoContainer(this, , portal);
- }
- }
- }
-
- ReactTestUtils.renderIntoDocument( );
- expect(portal.firstChild.innerHTML).toBe('bar');
- });
-
- it('should throw if parentComponent is invalid', () => {
- const portal = document.createElement('div');
-
- class Component extends React.Component {
- static contextTypes = {
- foo: PropTypes.string.isRequired,
- };
-
- render() {
- return {this.context.foo}
;
- }
- }
-
- // ESLint is confused here and thinks Parent is unused, presumably because
- // it is only used inside of the class body?
- // eslint-disable-next-line no-unused-vars
- class Parent extends React.Component {
- static childContextTypes = {
- foo: PropTypes.string.isRequired,
- };
-
- getChildContext() {
- return {
- foo: 'bar',
- };
- }
-
- render() {
- return null;
- }
-
- componentDidMount() {
- expect(function() {
- renderSubtreeIntoContainer( , , portal);
- }).toThrowError('parentComponentmust be a valid React Component');
- }
- }
- });
-
- it('should update context if it changes due to setState', () => {
- const container = document.createElement('div');
- document.body.appendChild(container);
- const portal = document.createElement('div');
-
- class Component extends React.Component {
- static contextTypes = {
- foo: PropTypes.string.isRequired,
- getFoo: PropTypes.func.isRequired,
- };
-
- render() {
- return {this.context.foo + '-' + this.context.getFoo()}
;
- }
- }
-
- class Parent extends React.Component {
- static childContextTypes = {
- foo: PropTypes.string.isRequired,
- getFoo: PropTypes.func.isRequired,
- };
-
- state = {
- bar: 'initial',
- };
-
- getChildContext() {
- return {
- foo: this.state.bar,
- getFoo: () => this.state.bar,
- };
- }
-
- render() {
- return null;
- }
-
- componentDidMount() {
- renderSubtreeIntoContainer(this, , portal);
- }
-
- componentDidUpdate() {
- renderSubtreeIntoContainer(this, , portal);
- }
- }
-
- const instance = ReactDOM.render( , container);
- expect(portal.firstChild.innerHTML).toBe('initial-initial');
- instance.setState({bar: 'changed'});
- expect(portal.firstChild.innerHTML).toBe('changed-changed');
- });
-
- it('should update context if it changes due to re-render', () => {
- const container = document.createElement('div');
- document.body.appendChild(container);
- const portal = document.createElement('div');
-
- class Component extends React.Component {
- static contextTypes = {
- foo: PropTypes.string.isRequired,
- getFoo: PropTypes.func.isRequired,
- };
-
- render() {
- return {this.context.foo + '-' + this.context.getFoo()}
;
- }
- }
-
- class Parent extends React.Component {
- static childContextTypes = {
- foo: PropTypes.string.isRequired,
- getFoo: PropTypes.func.isRequired,
- };
-
- getChildContext() {
- return {
- foo: this.props.bar,
- getFoo: () => this.props.bar,
- };
- }
-
- render() {
- return null;
- }
-
- componentDidMount() {
- renderSubtreeIntoContainer(this, , portal);
- }
-
- componentDidUpdate() {
- renderSubtreeIntoContainer(this, , portal);
- }
- }
-
- ReactDOM.render( , container);
- expect(portal.firstChild.innerHTML).toBe('initial-initial');
- ReactDOM.render( , container);
- expect(portal.firstChild.innerHTML).toBe('changed-changed');
- });
-
- it('should render portal with non-context-provider parent', () => {
- const container = document.createElement('div');
- document.body.appendChild(container);
- const portal = document.createElement('div');
-
- class Parent extends React.Component {
- render() {
- return null;
- }
-
- componentDidMount() {
- renderSubtreeIntoContainer(this, hello
, portal);
- }
- }
-
- ReactDOM.render( , container);
- expect(portal.firstChild.innerHTML).toBe('hello');
- });
-
- it('should get context through non-context-provider parent', () => {
- const container = document.createElement('div');
- document.body.appendChild(container);
- const portal = document.createElement('div');
-
- class Parent extends React.Component {
- render() {
- return ;
- }
- getChildContext() {
- return {value: this.props.value};
- }
- static childContextTypes = {
- value: PropTypes.string.isRequired,
+ getChildContext() {
+ return {
+ foo: 'bar',
};
}
- class Middle extends React.Component {
- render() {
- return null;
- }
- componentDidMount() {
- renderSubtreeIntoContainer(this, , portal);
- }
+ render() {
+ return null;
}
- class Child extends React.Component {
- static contextTypes = {
- value: PropTypes.string.isRequired,
- };
- render() {
- return {this.context.value}
;
- }
+ componentDidMount() {
+ expect(function() {
+ renderSubtreeIntoContainer( , , portal);
+ }).toThrowError('parentComponentmust be a valid React Component');
}
-
- ReactDOM.render( , container);
- expect(portal.textContent).toBe('foo');
- });
-
- it('should get context through middle non-context-provider layer', () => {
- const container = document.createElement('div');
- document.body.appendChild(container);
- const portal1 = document.createElement('div');
- const portal2 = document.createElement('div');
-
- class Parent extends React.Component {
- render() {
- return null;
- }
- getChildContext() {
- return {value: this.props.value};
- }
- componentDidMount() {
- renderSubtreeIntoContainer(this, , portal1);
- }
- static childContextTypes = {
- value: PropTypes.string.isRequired,
- };
- }
-
- class Middle extends React.Component {
- render() {
- return null;
- }
- componentDidMount() {
- renderSubtreeIntoContainer(this, , portal2);
- }
- }
-
- class Child extends React.Component {
- static contextTypes = {
- value: PropTypes.string.isRequired,
- };
- render() {
- return {this.context.value}
;
- }
- }
-
- ReactDOM.render( , container);
- expect(portal2.textContent).toBe('foo');
- });
-
- it('fails gracefully when mixing React 15 and 16', () => {
- class C extends React.Component {
- render() {
- return
;
- }
- }
- const c = ReactDOM.render( , document.createElement('div'));
- // React 15 calls this:
- // https://github.com/facebook/react/blob/77b71fc3c4/src/renderers/dom/client/ReactMount.js#L478-L479
- expect(() => {
- c._reactInternalInstance._processChildContext({});
- }).toThrow(
- __DEV__
- ? '_processChildContext is not available in React 16+. This likely ' +
- 'means you have multiple copies of React and are attempting to nest ' +
- 'a React 15 tree inside a React 16 tree using ' +
- "unstable_renderSubtreeIntoContainer, which isn't supported. Try to " +
- 'make sure you have only one copy of React (and ideally, switch to ' +
- 'ReactDOM.createPortal).'
- : "Cannot read property '_processChildContext' of undefined",
- );
- });
+ }
});
-}
+
+ it('should update context if it changes due to setState', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const portal = document.createElement('div');
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: PropTypes.string.isRequired,
+ getFoo: PropTypes.func.isRequired,
+ };
+
+ render() {
+ return {this.context.foo + '-' + this.context.getFoo()}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: PropTypes.string.isRequired,
+ getFoo: PropTypes.func.isRequired,
+ };
+
+ state = {
+ bar: 'initial',
+ };
+
+ getChildContext() {
+ return {
+ foo: this.state.bar,
+ getFoo: () => this.state.bar,
+ };
+ }
+
+ render() {
+ return null;
+ }
+
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+
+ componentDidUpdate() {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+ }
+
+ const instance = ReactDOM.render( , container);
+ expect(portal.firstChild.innerHTML).toBe('initial-initial');
+ instance.setState({bar: 'changed'});
+ expect(portal.firstChild.innerHTML).toBe('changed-changed');
+ });
+
+ it('should update context if it changes due to re-render', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const portal = document.createElement('div');
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: PropTypes.string.isRequired,
+ getFoo: PropTypes.func.isRequired,
+ };
+
+ render() {
+ return {this.context.foo + '-' + this.context.getFoo()}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: PropTypes.string.isRequired,
+ getFoo: PropTypes.func.isRequired,
+ };
+
+ getChildContext() {
+ return {
+ foo: this.props.bar,
+ getFoo: () => this.props.bar,
+ };
+ }
+
+ render() {
+ return null;
+ }
+
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+
+ componentDidUpdate() {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+ }
+
+ ReactDOM.render( , container);
+ expect(portal.firstChild.innerHTML).toBe('initial-initial');
+ ReactDOM.render( , container);
+ expect(portal.firstChild.innerHTML).toBe('changed-changed');
+ });
+
+ it('should render portal with non-context-provider parent', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const portal = document.createElement('div');
+
+ class Parent extends React.Component {
+ render() {
+ return null;
+ }
+
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, hello
, portal);
+ }
+ }
+
+ ReactDOM.render( , container);
+ expect(portal.firstChild.innerHTML).toBe('hello');
+ });
+
+ it('should get context through non-context-provider parent', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const portal = document.createElement('div');
+
+ class Parent extends React.Component {
+ render() {
+ return ;
+ }
+ getChildContext() {
+ return {value: this.props.value};
+ }
+ static childContextTypes = {
+ value: PropTypes.string.isRequired,
+ };
+ }
+
+ class Middle extends React.Component {
+ render() {
+ return null;
+ }
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+ }
+
+ class Child extends React.Component {
+ static contextTypes = {
+ value: PropTypes.string.isRequired,
+ };
+ render() {
+ return {this.context.value}
;
+ }
+ }
+
+ ReactDOM.render( , container);
+ expect(portal.textContent).toBe('foo');
+ });
+
+ it('should get context through middle non-context-provider layer', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const portal1 = document.createElement('div');
+ const portal2 = document.createElement('div');
+
+ class Parent extends React.Component {
+ render() {
+ return null;
+ }
+ getChildContext() {
+ return {value: this.props.value};
+ }
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, , portal1);
+ }
+ static childContextTypes = {
+ value: PropTypes.string.isRequired,
+ };
+ }
+
+ class Middle extends React.Component {
+ render() {
+ return null;
+ }
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, , portal2);
+ }
+ }
+
+ class Child extends React.Component {
+ static contextTypes = {
+ value: PropTypes.string.isRequired,
+ };
+ render() {
+ return {this.context.value}
;
+ }
+ }
+
+ ReactDOM.render( , container);
+ expect(portal2.textContent).toBe('foo');
+ });
+
+ it('fails gracefully when mixing React 15 and 16', () => {
+ class C extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ const c = ReactDOM.render( , document.createElement('div'));
+ // React 15 calls this:
+ // https://github.com/facebook/react/blob/77b71fc3c4/src/renderers/dom/client/ReactMount.js#L478-L479
+ expect(() => {
+ c._reactInternalInstance._processChildContext({});
+ }).toThrow(
+ __DEV__
+ ? '_processChildContext is not available in React 16+. This likely ' +
+ 'means you have multiple copies of React and are attempting to nest ' +
+ 'a React 15 tree inside a React 16 tree using ' +
+ "unstable_renderSubtreeIntoContainer, which isn't supported. Try to " +
+ 'make sure you have only one copy of React (and ideally, switch to ' +
+ 'ReactDOM.createPortal).'
+ : "Cannot read property '_processChildContext' of undefined",
+ );
+ });
+});
diff --git a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
index 44c4ab26fe..f73c32580d 100644
--- a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
@@ -1097,11 +1097,7 @@ describe('DOMModernPluginEventSystem', () => {
ReactTestUtils = require('react-dom/test-utils');
});
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
+ // @gate experimental
it('should create the same event listener map', () => {
const listenerMaps = [];
@@ -1119,6 +1115,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(listenerMaps[0]).toEqual(listenerMaps[1]);
});
+ // @gate experimental
it('can render correctly with the ReactDOMServer', () => {
const clickEvent = jest.fn();
@@ -1136,6 +1133,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(output).toBe(`Hello world
`);
});
+ // @gate experimental
it('can render correctly with the ReactDOMServer hydration', () => {
const clickEvent = jest.fn();
const spanRef = React.createRef();
@@ -1164,6 +1162,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(clickEvent).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('should correctly work for a basic "click" listener', () => {
let log = [];
const clickEvent = jest.fn(event => {
@@ -1272,6 +1271,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(clickEvent2).toBeCalledTimes(1);
});
+ // @gate experimental
it('should correctly work for setting and clearing a basic "click" listener', () => {
const clickEvent = jest.fn();
const divRef = React.createRef();
@@ -1315,6 +1315,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(clickEvent).toBeCalledTimes(0);
});
+ // @gate experimental
it('should handle the target being a text node', () => {
const clickEvent = jest.fn();
const buttonRef = React.createRef();
@@ -1337,6 +1338,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(clickEvent).toBeCalledTimes(1);
});
+ // @gate experimental
it('handle propagation of click events', () => {
const buttonRef = React.createRef();
const divRef = React.createRef();
@@ -1390,6 +1392,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(log[3]).toEqual(['bubble', buttonElement]);
});
+ // @gate experimental
it('handle propagation of click events mixed with onClick events', () => {
const buttonRef = React.createRef();
const divRef = React.createRef();
@@ -1442,6 +1445,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(log[5]).toEqual(['bubble', buttonElement]);
});
+ // @gate experimental
it('should correctly work for a basic "click" listener on the outer target', () => {
const log = [];
const clickEvent = jest.fn(event => {
@@ -1507,6 +1511,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(clickEvent).toBeCalledTimes(2);
});
+ // @gate experimental
it('should correctly handle many nested target listeners', () => {
const buttonRef = React.createRef();
const targetListener1 = jest.fn();
@@ -1572,6 +1577,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(targetListener4).toHaveBeenCalledTimes(2);
});
+ // @gate experimental
it('should correctly handle stopPropagation corrrectly for target events', () => {
const buttonRef = React.createRef();
const divRef = React.createRef();
@@ -1605,6 +1611,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(clickEvent).toHaveBeenCalledTimes(0);
});
+ // @gate experimental
it('should correctly handle stopPropagation corrrectly for many target events', () => {
const buttonRef = React.createRef();
const targetListerner1 = jest.fn(e => e.stopPropagation());
@@ -1639,6 +1646,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(targetListerner4).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('should correctly handle stopPropagation for mixed capture/bubbling target listeners', () => {
const buttonRef = React.createRef();
const targetListerner1 = jest.fn(e => e.stopPropagation());
@@ -1725,6 +1733,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(log).toEqual([{counter: 1}]);
});
+ // @gate experimental
it('should correctly work for a basic "click" listener that upgrades', () => {
const clickEvent = jest.fn();
const buttonRef = React.createRef();
@@ -1776,6 +1785,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(clickEvent).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('should correctly work for a basic "click" listener that upgrades #2', () => {
const clickEvent = jest.fn();
const buttonRef = React.createRef();
@@ -1827,6 +1837,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(clickEvent).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('should correctly work for a basic "click" window listener', () => {
const log = [];
const clickEvent = jest.fn(event => {
@@ -1878,6 +1889,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(clickEvent).toBeCalledTimes(2);
});
+ // @gate experimental
it('handle propagation of click events on the window', () => {
const buttonRef = React.createRef();
const divRef = React.createRef();
@@ -1937,6 +1949,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(log[5]).toEqual(['bubble', window]);
});
+ // @gate experimental
it('should correctly handle stopPropagation for mixed listeners', () => {
const buttonRef = React.createRef();
const rootListerner1 = jest.fn(e => e.stopPropagation());
@@ -1975,6 +1988,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(rootListerner2).toHaveBeenCalledTimes(0);
});
+ // @gate experimental
it('should correctly handle stopPropagation for delegated listeners', () => {
const buttonRef = React.createRef();
const rootListerner1 = jest.fn(e => e.stopPropagation());
@@ -2014,6 +2028,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(rootListerner4).toHaveBeenCalledTimes(0);
});
+ // @gate experimental
it('handle propagation of click events on the window and document', () => {
const buttonRef = React.createRef();
const divRef = React.createRef();
@@ -2079,6 +2094,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(log[7]).toEqual(['bubble', window]);
});
+ // @gate experimental
it('handles propagation of custom user events', () => {
const buttonRef = React.createRef();
const divRef = React.createRef();
@@ -2153,6 +2169,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(log[5]).toEqual(['bubble', buttonElement]);
});
+ // @gate experimental
it('beforeblur and afterblur are called after a focused element is unmounted', () => {
const log = [];
// We have to persist here because we want to read relatedTarget later.
@@ -2202,6 +2219,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(log).toEqual(['beforeblur', 'afterblur']);
});
+ // @gate experimental
it('beforeblur and afterblur are called after a nested focused element is unmounted', () => {
const log = [];
// We have to persist here because we want to read relatedTarget later.
@@ -2346,6 +2364,7 @@ describe('DOMModernPluginEventSystem', () => {
ReactDOMServer = require('react-dom/server');
});
+ // @gate experimental
it('handle propagation of click events on a scope', () => {
const buttonRef = React.createRef();
const log = [];
@@ -2390,6 +2409,7 @@ describe('DOMModernPluginEventSystem', () => {
]);
});
+ // @gate experimental
it('handle mixed propagation of click events on a scope', () => {
const buttonRef = React.createRef();
const divRef = React.createRef();
@@ -2464,6 +2484,7 @@ describe('DOMModernPluginEventSystem', () => {
]);
});
+ // @gate experimental
it('should not handle the target being a dangling text node within a scope', () => {
const clickEvent = jest.fn();
const buttonRef = React.createRef();
@@ -2495,6 +2516,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(clickEvent).toBeCalledTimes(0);
});
+ // @gate experimental
it('handle stopPropagation (inner) correctly between scopes', () => {
const buttonRef = React.createRef();
const outerOnClick = jest.fn();
@@ -2531,6 +2553,7 @@ describe('DOMModernPluginEventSystem', () => {
expect(outerOnClick).toHaveBeenCalledTimes(0);
});
+ // @gate experimental
it('handle stopPropagation (outer) correctly between scopes', () => {
const buttonRef = React.createRef();
const outerOnClick = jest.fn(e => e.stopPropagation());
@@ -2567,6 +2590,8 @@ describe('DOMModernPluginEventSystem', () => {
expect(outerOnClick).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
+
it('handle stopPropagation (inner and outer) correctly between scopes', () => {
const buttonRef = React.createRef();
const onClick = jest.fn(e => e.stopPropagation());
diff --git a/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js
index 932bc4e990..9c57ac6531 100644
--- a/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js
@@ -66,11 +66,6 @@ function dispatchClickEvent(element) {
describe('DOMEventResponderSystem', () => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
@@ -89,6 +84,7 @@ describe('DOMEventResponderSystem', () => {
container = null;
});
+ // @gate experimental
it('can mount and render correctly with the ReactTestRenderer', () => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
@@ -106,6 +102,7 @@ describe('DOMEventResponderSystem', () => {
expect(renderer).toMatchRenderedOutput(Hello world
);
});
+ // @gate experimental
it('can render correctly with the ReactDOMServer', () => {
const TestResponder = createEventResponder({});
@@ -118,6 +115,7 @@ describe('DOMEventResponderSystem', () => {
expect(output).toBe(`Hello world
`);
});
+ // @gate experimental
it('can render correctly with the ReactDOMServer hydration', () => {
const onEvent = jest.fn();
const TestResponder = createEventResponder({
@@ -147,6 +145,7 @@ describe('DOMEventResponderSystem', () => {
expect(onEvent).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('the event responders should fire on click event', () => {
let eventResponderFiredCount = 0;
const eventLog = [];
@@ -202,6 +201,7 @@ describe('DOMEventResponderSystem', () => {
expect(eventResponderFiredCount).toBe(2);
});
+ // @gate experimental
it('the event responders should fire on click event (passive events forced)', () => {
// JSDOM does not support passive events, so this manually overrides the value to be true
const checkPassiveEvents = require('react-dom/src/events/checkPassiveEvents');
@@ -246,6 +246,7 @@ describe('DOMEventResponderSystem', () => {
]);
});
+ // @gate experimental
it('nested event responders should not fire multiple times', () => {
let eventResponderFiredCount = 0;
let eventLog = [];
@@ -327,6 +328,7 @@ describe('DOMEventResponderSystem', () => {
]);
});
+ // @gate experimental
it('nested event responders should fire in the correct order', () => {
let eventLog = [];
const buttonRef = React.createRef();
@@ -390,6 +392,7 @@ describe('DOMEventResponderSystem', () => {
expect(eventLog).toEqual(['B [bubble]', 'A [bubble]']);
});
+ // @gate experimental
it('nested event responders should fire in the correct order #2', () => {
const eventLog = [];
const buttonRef = React.createRef();
@@ -426,6 +429,7 @@ describe('DOMEventResponderSystem', () => {
expect(eventLog).toEqual(['B [bubble]']);
});
+ // @gate experimental
it('custom event dispatching for click -> magicClick works', () => {
const eventLog = [];
const buttonRef = React.createRef();
@@ -472,6 +476,7 @@ describe('DOMEventResponderSystem', () => {
expect(eventLog).toEqual(['magic event fired', 'magicclick', 'bubble']);
});
+ // @gate experimental
it('the event responder onMount() function should fire', () => {
let onMountFired = 0;
@@ -505,6 +510,7 @@ describe('DOMEventResponderSystem', () => {
expect(onMountFired).toEqual(2);
});
+ // @gate experimental
it('the event responder onUnmount() function should fire', () => {
let onUnmountFired = 0;
@@ -551,6 +557,7 @@ describe('DOMEventResponderSystem', () => {
expect(onUnmountFired).toEqual(4);
});
+ // @gate experimental
it('the event responder onUnmount() function should fire using scopes', () => {
let onUnmountFired = 0;
@@ -598,6 +605,7 @@ describe('DOMEventResponderSystem', () => {
expect(onUnmountFired).toEqual(4);
});
+ // @gate experimental
it('the event responder onUnmount() function should fire with state', () => {
let counter = 0;
@@ -621,6 +629,7 @@ describe('DOMEventResponderSystem', () => {
expect(counter).toEqual(5);
});
+ // @gate experimental
it('the event responder target listeners should correctly fire for only their events', () => {
let clickEventComponent1Fired = 0;
let clickEventComponent2Fired = 0;
@@ -682,6 +691,7 @@ describe('DOMEventResponderSystem', () => {
]);
});
+ // @gate experimental
it('the event responder system should warn on accessing invalid properties', () => {
const TestResponder = createEventResponder({
targetEventTypes: ['click'],
@@ -747,6 +757,7 @@ describe('DOMEventResponderSystem', () => {
expect(container.innerHTML).toBe('Click me! ');
});
+ // @gate experimental
it('should work with event responder hooks', () => {
const buttonRef = React.createRef();
const eventLogs = [];
@@ -839,6 +850,7 @@ describe('DOMEventResponderSystem', () => {
expect(log).toEqual([{counter: 1}]);
});
+ // @gate experimental
it('should correctly pass through event properties', () => {
const timeStamps = [];
const ref = React.createRef();
@@ -903,6 +915,7 @@ describe('DOMEventResponderSystem', () => {
]);
});
+ // @gate experimental
it('should not propagate target events through portals by default', () => {
const buttonRef = React.createRef();
const onEvent = jest.fn();
@@ -926,6 +939,7 @@ describe('DOMEventResponderSystem', () => {
expect(onEvent).not.toBeCalled();
});
+ // @gate experimental
it('should propagate target events through portals when enabled', () => {
const buttonRef = React.createRef();
const onEvent = jest.fn();
@@ -950,6 +964,7 @@ describe('DOMEventResponderSystem', () => {
expect(onEvent).toBeCalled();
});
+ // @gate experimental
it('event upgrading should work correctly', () => {
let eventResponderFiredCount = 0;
const buttonRef = React.createRef();
diff --git a/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js
index affcf8e6fb..379203bf33 100644
--- a/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js
@@ -28,8 +28,13 @@ function initializeModules(hasPointerEvents) {
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
- useContextMenu = require('react-interactions/events/context-menu')
- .useContextMenu;
+
+ // TODO: This import throws outside of experimental mode. Figure out better
+ // strategy for gated imports.
+ if (__EXPERIMENTAL__) {
+ useContextMenu = require('react-interactions/events/context-menu')
+ .useContextMenu;
+ }
}
const forcePointerEvents = true;
@@ -38,11 +43,6 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
describe.each(table)('ContextMenu responder', hasPointerEvents => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
initializeModules(hasPointerEvents);
container = document.createElement('div');
@@ -56,6 +56,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
});
describe('all platforms', () => {
+ // @gate experimental
it('mouse right-click', () => {
const onContextMenu = jest.fn();
const preventDefault = jest.fn();
@@ -79,6 +80,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('touch long-press', () => {
const onContextMenu = jest.fn();
const preventDefault = jest.fn();
@@ -102,6 +104,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('"disabled" is true', () => {
const onContextMenu = jest.fn();
const ref = React.createRef();
@@ -119,6 +122,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
expect(onContextMenu).toHaveBeenCalledTimes(0);
});
+ // @gate experimental
it('"preventDefault" is false', () => {
const preventDefault = jest.fn();
const onContextMenu = jest.fn();
@@ -149,6 +153,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
platform.clear();
});
+ // @gate experimental
it('mouse modified left-click', () => {
const onContextMenu = jest.fn();
const ref = React.createRef();
@@ -181,6 +186,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
platform.clear();
});
+ // @gate experimental
it('mouse modified left-click', () => {
const onContextMenu = jest.fn();
const ref = React.createRef();
diff --git a/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js
index 0b751d5c06..162e752a85 100644
--- a/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js
@@ -28,8 +28,13 @@ function initializeModules(hasPointerEvents) {
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
- FocusResponder = require('react-interactions/events/focus').FocusResponder;
- useFocus = require('react-interactions/events/focus').useFocus;
+
+ // TODO: This import throws outside of experimental mode. Figure out better
+ // strategy for gated imports.
+ if (__EXPERIMENTAL__) {
+ FocusResponder = require('react-interactions/events/focus').FocusResponder;
+ useFocus = require('react-interactions/events/focus').useFocus;
+ }
}
const forcePointerEvents = true;
@@ -38,11 +43,6 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
describe.each(table)('Focus responder', hasPointerEvents => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
initializeModules(hasPointerEvents);
container = document.createElement('div');
@@ -58,7 +58,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
describe('disabled', () => {
let onBlur, onFocus, ref;
- beforeEach(() => {
+ const componentInit = () => {
onBlur = jest.fn();
onFocus = jest.fn();
ref = React.createRef();
@@ -71,9 +71,11 @@ describe.each(table)('Focus responder', hasPointerEvents => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('does not call callbacks', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.focus();
target.blur();
@@ -85,7 +87,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
describe('onBlur', () => {
let onBlur, ref;
- beforeEach(() => {
+ const componentInit = () => {
onBlur = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -95,9 +97,11 @@ describe.each(table)('Focus responder', hasPointerEvents => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('is called after "blur" event', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.focus();
target.blur();
@@ -125,21 +129,25 @@ describe.each(table)('Focus responder', hasPointerEvents => {
ReactDOM.render( , container);
};
- beforeEach(componentInit);
-
+ // @gate experimental
it('is called after "focus" event', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.focus();
expect(onFocus).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('is not called if descendants of target receive focus', () => {
+ componentInit();
const target = createEventTarget(innerRef.current);
target.focus();
expect(onFocus).not.toBeCalled();
});
+ // @gate experimental
it('is called with the correct pointerType: mouse', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown();
target.pointerup();
@@ -149,7 +157,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called with the correct pointerType: touch', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const pointerType = 'touch';
target.pointerdown({pointerType});
@@ -161,7 +171,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
});
if (hasPointerEvents) {
+ // @gate experimental
it('is called with the correct pointerType: pen', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const pointerType = 'pen';
target.pointerdown({pointerType});
@@ -173,7 +185,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
});
}
+ // @gate experimental
it('is called with the correct pointerType using a keyboard', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'LeftArrow'});
target.focus();
@@ -183,6 +197,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called with the correct pointerType using Tab+altKey on Mac', () => {
platform.set('mac');
jest.resetModules();
@@ -207,7 +222,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
describe('onFocusChange', () => {
let onFocusChange, ref, innerRef;
- beforeEach(() => {
+ const componentInit = () => {
onFocusChange = jest.fn();
ref = React.createRef();
innerRef = React.createRef();
@@ -222,9 +237,11 @@ describe.each(table)('Focus responder', hasPointerEvents => {
);
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('is called after "blur" and "focus" events', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.focus();
expect(onFocusChange).toHaveBeenCalledTimes(1);
@@ -234,7 +251,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
expect(onFocusChange).toHaveBeenCalledWith(false);
});
+ // @gate experimental
it('is not called after "blur" and "focus" events on descendants', () => {
+ componentInit();
const target = createEventTarget(innerRef.current);
target.focus();
expect(onFocusChange).toHaveBeenCalledTimes(0);
@@ -246,7 +265,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
describe('onFocusVisibleChange', () => {
let onFocusVisibleChange, ref, innerRef;
- beforeEach(() => {
+ const componentInit = () => {
onFocusVisibleChange = jest.fn();
ref = React.createRef();
innerRef = React.createRef();
@@ -261,9 +280,11 @@ describe.each(table)('Focus responder', hasPointerEvents => {
);
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('is called after "focus" and "blur" if keyboard navigation is active', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const containerTarget = createEventTarget(container);
// use keyboard first
@@ -276,7 +297,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
});
+ // @gate experimental
it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const containerTarget = createEventTarget(container);
// use keyboard first
@@ -293,7 +316,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
});
+ // @gate experimental
it('is not called after "focus" and "blur" events without keyboard', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const containerTarget = createEventTarget(container);
target.pointerdown();
@@ -303,7 +328,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
});
+ // @gate experimental
it('is not called after "blur" and "focus" events on descendants', () => {
+ componentInit();
const innerTarget = createEventTarget(innerRef.current);
const containerTarget = createEventTarget(container);
containerTarget.keydown({key: 'Tab'});
@@ -315,6 +342,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
});
describe('nested Focus components', () => {
+ // @gate experimental
it('propagates events in the correct order', () => {
const events = [];
const innerRef = React.createRef();
@@ -367,6 +395,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
});
});
+ // @gate experimental
it('expect displayName to show up for event component', () => {
expect(FocusResponder.displayName).toBe('Focus');
});
diff --git a/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
index 8efb1a37ce..76b065a1ee 100644
--- a/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
@@ -25,10 +25,15 @@ const initializeModules = hasPointerEvents => {
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
- FocusWithinResponder = require('react-interactions/events/focus')
- .FocusWithinResponder;
- useFocusWithin = require('react-interactions/events/focus').useFocusWithin;
Scheduler = require('scheduler');
+
+ // TODO: This import throws outside of experimental mode. Figure out better
+ // strategy for gated imports.
+ if (__EXPERIMENTAL__) {
+ FocusWithinResponder = require('react-interactions/events/focus')
+ .FocusWithinResponder;
+ useFocusWithin = require('react-interactions/events/focus').useFocusWithin;
+ }
};
const forcePointerEvents = true;
@@ -37,11 +42,6 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
describe.each(table)('FocusWithin responder', hasPointerEvents => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
initializeModules();
container = document.createElement('div');
@@ -57,7 +57,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
describe('disabled', () => {
let onFocusWithinChange, onFocusWithinVisibleChange, ref;
- beforeEach(() => {
+ const componentInit = () => {
onFocusWithinChange = jest.fn();
onFocusWithinVisibleChange = jest.fn();
ref = React.createRef();
@@ -70,9 +70,11 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('prevents custom events being dispatched', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.focus();
target.blur();
@@ -96,15 +98,17 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
);
};
- beforeEach(() => {
+ const componentInit = () => {
onFocusWithinChange = jest.fn();
ref = React.createRef();
innerRef = React.createRef();
innerRef2 = React.createRef();
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('is called after "blur" and "focus" events on focus target', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.focus();
expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
@@ -114,7 +118,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(onFocusWithinChange).toHaveBeenCalledWith(false);
});
+ // @gate experimental
it('is called after "blur" and "focus" events on descendants', () => {
+ componentInit();
const target = createEventTarget(innerRef.current);
target.focus();
expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
@@ -124,7 +130,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(onFocusWithinChange).toHaveBeenCalledWith(false);
});
+ // @gate experimental
it('is only called once when focus moves within and outside the subtree', () => {
+ componentInit();
const node = ref.current;
const innerNode1 = innerRef.current;
const innerNode2 = innerRef.current;
@@ -165,15 +173,17 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
);
};
- beforeEach(() => {
+ const componentInit = () => {
onFocusWithinVisibleChange = jest.fn();
ref = React.createRef();
innerRef = React.createRef();
innerRef2 = React.createRef();
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('is called after "focus" and "blur" on focus target if keyboard was used', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const containerTarget = createEventTarget(container);
// use keyboard first
@@ -186,7 +196,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
});
+ // @gate experimental
it('is called after "focus" and "blur" on descendants if keyboard was used', () => {
+ componentInit();
const innerTarget = createEventTarget(innerRef.current);
const containerTarget = createEventTarget(container);
// use keyboard first
@@ -199,7 +211,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
});
+ // @gate experimental
it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
+ componentInit();
const node = ref.current;
const innerNode1 = innerRef.current;
const innerNode2 = innerRef2.current;
@@ -235,7 +249,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
});
+ // @gate experimental
it('is not called after "focus" and "blur" events without keyboard', () => {
+ componentInit();
const innerTarget = createEventTarget(innerRef.current);
innerTarget.pointerdown();
innerTarget.pointerup();
@@ -243,7 +259,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0);
});
+ // @gate experimental
it('is only called once when focus moves within and outside the subtree', () => {
+ componentInit();
const node = ref.current;
const innerNode1 = innerRef.current;
const innerNode2 = innerRef2.current;
@@ -280,6 +298,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
innerRef2 = React.createRef();
});
+ // @gate experimental
it('is called after a focused element is unmounted', () => {
const Component = ({show}) => {
const listener = useFocusWithin({
@@ -310,6 +329,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called after a nested focused element is unmounted', () => {
const Component = ({show}) => {
const listener = useFocusWithin({
@@ -405,6 +425,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
});
});
+ // @gate experimental
it('expect displayName to show up for event component', () => {
expect(FocusWithinResponder.displayName).toBe('FocusWithin');
});
diff --git a/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js
index 35f71ebaba..19f6269f2d 100644
--- a/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js
@@ -24,8 +24,13 @@ function initializeModules(hasPointerEvents) {
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
- HoverResponder = require('react-interactions/events/hover').HoverResponder;
- useHover = require('react-interactions/events/hover').useHover;
+
+ // TODO: This import throws outside of experimental mode. Figure out better
+ // strategy for gated imports.
+ if (__EXPERIMENTAL__) {
+ HoverResponder = require('react-interactions/events/hover').HoverResponder;
+ useHover = require('react-interactions/events/hover').useHover;
+ }
}
const forcePointerEvents = true;
@@ -34,11 +39,6 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
describe.each(table)('Hover responder', hasPointerEvents => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
initializeModules(hasPointerEvents);
container = document.createElement('div');
@@ -54,7 +54,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
describe('disabled', () => {
let onHoverChange, onHoverStart, onHoverMove, onHoverEnd, ref;
- beforeEach(() => {
+ const componentInit = () => {
onHoverChange = jest.fn();
onHoverStart = jest.fn();
onHoverMove = jest.fn();
@@ -71,9 +71,11 @@ describe.each(table)('Hover responder', hasPointerEvents => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('does not call callbacks', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerenter();
target.pointerexit();
@@ -87,7 +89,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
describe('onHoverStart', () => {
let onHoverStart, ref;
- beforeEach(() => {
+ const componentInit = () => {
onHoverStart = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -97,22 +99,28 @@ describe.each(table)('Hover responder', hasPointerEvents => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('is called for mouse pointers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerenter();
expect(onHoverStart).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('is not called for touch pointers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({pointerType: 'touch'});
target.pointerup({pointerType: 'touch'});
expect(onHoverStart).not.toBeCalled();
});
+ // @gate experimental
it('is called if a mouse pointer is used after a touch pointer', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({pointerType: 'touch'});
target.pointerup({pointerType: 'touch'});
@@ -124,7 +132,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
describe('onHoverChange', () => {
let onHoverChange, ref;
- beforeEach(() => {
+ const componentInit = () => {
onHoverChange = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -134,9 +142,11 @@ describe.each(table)('Hover responder', hasPointerEvents => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('is called for mouse pointers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerenter();
expect(onHoverChange).toHaveBeenCalledTimes(1);
@@ -146,7 +156,9 @@ describe.each(table)('Hover responder', hasPointerEvents => {
expect(onHoverChange).toHaveBeenCalledWith(false);
});
+ // @gate experimental
it('is not called for touch pointers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({pointerType: 'touch'});
target.pointerup({pointerType: 'touch'});
@@ -157,7 +169,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
describe('onHoverEnd', () => {
let onHoverEnd, ref;
- beforeEach(() => {
+ const componentInit = () => {
onHoverEnd = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -167,9 +179,11 @@ describe.each(table)('Hover responder', hasPointerEvents => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('is called for mouse pointers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerenter();
target.pointerexit();
@@ -177,7 +191,9 @@ describe.each(table)('Hover responder', hasPointerEvents => {
});
if (hasPointerEvents) {
+ // @gate experimental
it('is called once for cancelled mouse pointers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerenter();
target.pointercancel();
@@ -192,14 +208,18 @@ describe.each(table)('Hover responder', hasPointerEvents => {
});
}
+ // @gate experimental
it('is not called for touch pointers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({pointerType: 'touch'});
target.pointerup({pointerType: 'touch'});
expect(onHoverEnd).not.toBeCalled();
});
+ // @gate experimental
it('should correctly work with React Portals', () => {
+ componentInit();
const portalNode = document.createElement('div');
const divRef = React.createRef();
const spanRef = React.createRef();
@@ -227,6 +247,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
});
describe('onHoverMove', () => {
+ // @gate experimental
it('is called after the active pointer moves"', () => {
const onHoverMove = jest.fn();
const ref = React.createRef();
@@ -250,6 +271,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
});
describe('nested Hover components', () => {
+ // @gate experimental
it('not propagate by default', () => {
const events = [];
const innerRef = React.createRef();
@@ -310,10 +332,12 @@ describe.each(table)('Hover responder', hasPointerEvents => {
});
});
+ // @gate experimental
it('expect displayName to show up for event component', () => {
expect(HoverResponder.displayName).toBe('Hover');
});
+ // @gate experimental
it('should correctly pass through event properties', () => {
const timeStamps = [];
const ref = React.createRef();
diff --git a/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js
index 77084ac4d9..bcbe4d5224 100644
--- a/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js
@@ -38,18 +38,18 @@ const modulesInit = () => {
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
- InputResponder = require('react-interactions/events/input').InputResponder;
- useInput = require('react-interactions/events/input').useInput;
+
+ // TODO: This import throws outside of experimental mode. Figure out better
+ // strategy for gated imports.
+ if (__EXPERIMENTAL__) {
+ InputResponder = require('react-interactions/events/input').InputResponder;
+ useInput = require('react-interactions/events/input').useInput;
+ }
};
describe('Input event responder', () => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
jest.resetModules();
modulesInit();
@@ -66,7 +66,7 @@ describe('Input event responder', () => {
describe('disabled', () => {
let onChange, onValueChange, ref;
- beforeEach(() => {
+ const componentInit = () => {
onChange = jest.fn();
onValueChange = jest.fn();
ref = React.createRef();
@@ -80,9 +80,11 @@ describe('Input event responder', () => {
return ;
}
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
it('prevents custom events being dispatched', () => {
+ componentInit();
ref.current.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
@@ -104,6 +106,7 @@ describe('Input event responder', () => {
// keep track of the "current" value and only fire events when it changes.
// See https://github.com/facebook/react/pull/5746.
+ // @gate experimental
it('should consider initial text value to be current', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -151,6 +154,7 @@ describe('Input event responder', () => {
}
});
+ // @gate experimental
it('should consider initial checkbox checked=true to be current', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -194,6 +198,7 @@ describe('Input event responder', () => {
expect(onValueChangeCalled).toBe(0);
});
+ // @gate experimental
it('should consider initial checkbox checked=false to be current', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -236,6 +241,7 @@ describe('Input event responder', () => {
expect(onValueChangeCalled).toBe(0);
});
+ // @gate experimental
it('should fire change for checkbox input', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -283,6 +289,7 @@ describe('Input event responder', () => {
expect(onValueChangeCalled).toBe(2);
});
+ // @gate experimental
it('should not fire change setting the value programmatically', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -345,6 +352,7 @@ describe('Input event responder', () => {
expect(onValueChangeCalled).toBe(1);
});
+ // @gate experimental
it('should not distinguish equal string and number values', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -390,6 +398,7 @@ describe('Input event responder', () => {
});
// See a similar input test above for a detailed description of why.
+ // @gate experimental
it('should not fire change when setting checked programmatically', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -441,6 +450,7 @@ describe('Input event responder', () => {
expect(onValueChangeCalled).toBe(1);
});
+ // @gate experimental
it('should only fire change for checked radio button once', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -477,6 +487,7 @@ describe('Input event responder', () => {
expect(onValueChangeCalled).toBe(1);
});
+ // @gate experimental
it('should track radio button cousins in a group', () => {
let onChangeCalled1 = 0;
let onValueChangeCalled1 = 0;
@@ -566,6 +577,7 @@ describe('Input event responder', () => {
expect(onValueChangeCalled2).toBe(1);
});
+ // @gate experimental
it('should deduplicate input value change events', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -672,6 +684,7 @@ describe('Input event responder', () => {
});
});
+ // @gate experimental
it('should listen for both change and input events when supported', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -711,6 +724,7 @@ describe('Input event responder', () => {
expect(onValueChangeCalled).toBe(2);
});
+ // @gate experimental
it('should only fire events when the value changes for range inputs', () => {
let onChangeCalled = 0;
let onValueChangeCalled = 0;
@@ -756,6 +770,7 @@ describe('Input event responder', () => {
expect(onValueChangeCalled).toBe(2);
});
+ // @gate experimental || build === "production"
it('does not crash for nodes with custom value property', () => {
let originalCreateElement;
// https://github.com/facebook/react/issues/10196
@@ -793,6 +808,7 @@ describe('Input event responder', () => {
});
describe('concurrent mode', () => {
+ // @gate experimental
// @gate experimental
it('text input', () => {
const root = ReactDOM.createRoot(container);
@@ -849,6 +865,7 @@ describe('Input event responder', () => {
expect(input.value).toBe('changed [!]');
});
+ // @gate experimental
// @gate experimental
it('checkbox input', () => {
const root = ReactDOM.createRoot(container);
@@ -918,6 +935,7 @@ describe('Input event responder', () => {
expect(input.checked).toBe(false);
});
+ // @gate experimental
// @gate experimental
it('textarea', () => {
const root = ReactDOM.createRoot(container);
@@ -976,6 +994,7 @@ describe('Input event responder', () => {
});
});
+ // @gate experimental
it('expect displayName to show up for event component', () => {
expect(InputResponder.displayName).toBe('Input');
});
diff --git a/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js
index bd649765d5..7cfe8db7f7 100644
--- a/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js
@@ -22,17 +22,17 @@ function initializeModules(hasPointerEvents) {
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
- useKeyboard = require('react-interactions/events/keyboard').useKeyboard;
+
+ // TODO: This import throws outside of experimental mode. Figure out better
+ // strategy for gated imports.
+ if (__EXPERIMENTAL__) {
+ useKeyboard = require('react-interactions/events/keyboard').useKeyboard;
+ }
}
describe('Keyboard responder', () => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
initializeModules();
container = document.createElement('div');
@@ -82,6 +82,7 @@ describe('Keyboard responder', () => {
};
}
+ // @gate experimental
test('propagates key event when a continuePropagation() is used', () => {
const {
onClickInner,
@@ -104,6 +105,7 @@ describe('Keyboard responder', () => {
expect(onClickOuter).toBeCalled();
});
+ // @gate experimental
test('does not propagate key event by default', () => {
const {
onClickInner,
@@ -129,7 +131,7 @@ describe('Keyboard responder', () => {
describe('disabled', () => {
let onKeyDown, onKeyUp, ref;
- beforeEach(() => {
+ const componentInit = () => {
onKeyDown = jest.fn();
onKeyUp = jest.fn();
ref = React.createRef();
@@ -138,9 +140,11 @@ describe('Keyboard responder', () => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
test('does not call callbacks', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown();
target.keyup();
@@ -152,7 +156,7 @@ describe('Keyboard responder', () => {
describe('onClick', () => {
let onClick, ref;
- beforeEach(() => {
+ const componentInit = () => {
onClick = jest.fn(e => {
e.preventDefault();
});
@@ -162,10 +166,12 @@ describe('Keyboard responder', () => {
return
;
};
ReactDOM.render( , container);
- });
+ };
// e.g, "Enter" on link
+ // @gate experimental
test('click is between key events', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({key: 'Enter'});
@@ -187,7 +193,9 @@ describe('Keyboard responder', () => {
});
// e.g., "Spacebar" on button
+ // @gate experimental
test('click is after key events', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({key: 'Enter'});
@@ -209,7 +217,9 @@ describe('Keyboard responder', () => {
});
// e.g, generated by a screen-reader
+ // @gate experimental
test('click is orphan', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.virtualclick();
expect(onClick).toHaveBeenCalledTimes(1);
@@ -232,7 +242,7 @@ describe('Keyboard responder', () => {
describe('onKeyDown', () => {
let onKeyDown, ref;
- beforeEach(() => {
+ const componentInit = () => {
onKeyDown = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -240,9 +250,11 @@ describe('Keyboard responder', () => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
test('key down', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Q'});
expect(onKeyDown).toHaveBeenCalledTimes(1);
@@ -263,7 +275,9 @@ describe('Keyboard responder', () => {
);
});
+ // @gate experimental
test('modified key down', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({
key: 'Q',
@@ -293,7 +307,7 @@ describe('Keyboard responder', () => {
describe('onKeyUp', () => {
let onKeyUp, ref;
- beforeEach(() => {
+ const componentInit = () => {
onKeyUp = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -301,9 +315,11 @@ describe('Keyboard responder', () => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
test('key up', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Q'});
target.keyup({key: 'Q'});
@@ -325,7 +341,9 @@ describe('Keyboard responder', () => {
);
});
+ // @gate experimental
test('modified key up', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Q'});
target.keyup({
@@ -364,6 +382,7 @@ describe('Keyboard responder', () => {
return ref;
}
+ // @gate experimental
test('does not prevent native click by default', () => {
const onClick = jest.fn();
const preventDefault = jest.fn();
@@ -381,6 +400,7 @@ describe('Keyboard responder', () => {
);
});
+ // @gate experimental
test('prevents native behaviour with preventDefault', () => {
const onClick = jest.fn(e => e.preventDefault());
const preventDefault = jest.fn();
@@ -409,6 +429,7 @@ describe('Keyboard responder', () => {
return ref;
}
+ // @gate experimental
test('key config matches', () => {
const onKeyDown = jest.fn(e => {
if (e.key === 'Tab') {
@@ -434,6 +455,7 @@ describe('Keyboard responder', () => {
);
});
+ // @gate experimental
test('key config matches (modifier keys)', () => {
const onKeyDown = jest.fn(e => {
if (e.key === 'Tab' && e.shiftKey) {
@@ -457,6 +479,7 @@ describe('Keyboard responder', () => {
);
});
+ // @gate experimental
test('key config does not match (modifier keys)', () => {
const onKeyDown = jest.fn(e => {
if (e.key === 'Tab' && e.shiftKey) {
diff --git a/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js b/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js
index 2a062a40fb..2a31c5f37f 100644
--- a/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js
@@ -19,11 +19,6 @@ let Scheduler;
describe('mixing responders with the heritage event system', () => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
@@ -39,11 +34,7 @@ describe('mixing responders with the heritage event system', () => {
container = null;
});
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
+ // @gate experimental
it('should properly only flush sync once when the event systems are mixed', () => {
const useTap = require('react-interactions/events/tap').useTap;
const ref = React.createRef();
@@ -113,6 +104,7 @@ describe('mixing responders with the heritage event system', () => {
document.body.removeChild(newContainer);
});
+ // @gate experimental
it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => {
const useTap = require('react-interactions/events/tap').useTap;
const ref = React.createRef();
@@ -182,6 +174,7 @@ describe('mixing responders with the heritage event system', () => {
document.body.removeChild(newContainer);
});
+ // @gate experimental
it(
'should only flush before outermost discrete event handler when mixing ' +
'event systems',
@@ -241,6 +234,7 @@ describe('mixing responders with the heritage event system', () => {
);
describe('mixing the Input and Press repsonders', () => {
+ // @gate experimental
it('is async for non-input events', () => {
const useTap = require('react-interactions/events/tap').useTap;
const useInput = require('react-interactions/events/input').useInput;
diff --git a/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js
index 07aa6c5e3b..59f3488ba7 100644
--- a/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js
@@ -30,19 +30,17 @@ function initializeModules(hasPointerEvents) {
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
- usePress = require('react-interactions/events/press').usePress;
-}
-const pointerTypesTable = [['mouse'], ['touch']];
+ // TODO: This import throws outside of experimental mode. Figure out better
+ // strategy for gated imports.
+ if (__EXPERIMENTAL__) {
+ usePress = require('react-interactions/events/press').usePress;
+ }
+}
describeWithPointerEvent('Press responder', hasPointerEvents => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
initializeModules(hasPointerEvents);
container = document.createElement('div');
@@ -59,7 +57,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
describe('disabled', () => {
let onPressStart, onPressChange, onPressMove, onPressEnd, onPress, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPressStart = jest.fn();
onPressChange = jest.fn();
onPressMove = jest.fn();
@@ -78,9 +76,11 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
test('does not call callbacks for pointers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown();
target.pointerup();
@@ -91,7 +91,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
expect(onPress).not.toBeCalled();
});
+ // @gate experimental
test('does not call callbacks for keyboard', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({key: 'Enter'});
@@ -106,7 +108,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
describe('onPressStart', () => {
let onPressStart, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPressStart = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -117,21 +119,33 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
+ };
+
+ // @gate experimental
+ it('is called after pointer down: mouse', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'mouse'});
+ expect(onPressStart).toHaveBeenCalledTimes(1);
+ expect(onPressStart).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'pressstart'}),
+ );
});
- it.each(pointerTypesTable)(
- 'is called after pointer down: %s',
- pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- expect(onPressStart).toHaveBeenCalledTimes(1);
- expect(onPressStart).toHaveBeenCalledWith(
- expect.objectContaining({pointerType, type: 'pressstart'}),
- );
- },
- );
+ // @gate experimental
+ it('is called after pointer down: mouse', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'touch'});
+ expect(onPressStart).toHaveBeenCalledTimes(1);
+ expect(onPressStart).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'touch', type: 'pressstart'}),
+ );
+ });
+ // @gate experimental
it('is called after middle-button pointer down', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const pointerType = 'mouse';
target.pointerdown({
@@ -150,7 +164,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called after virtual middle-button pointer down', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const pointerType = 'mouse';
target.pointerdown({
@@ -169,7 +185,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('ignores any events not caused by primary/middle-click or touch/pen contact', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({buttons: buttonsType.secondary});
target.pointerup({buttons: buttonsType.secondary});
@@ -178,7 +196,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
expect(onPressStart).toHaveBeenCalledTimes(0);
});
+ // @gate experimental
it('is called once after "keydown" events for Enter', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keydown({key: 'Enter'});
@@ -189,7 +209,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called once after "keydown" events for Spacebar', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const preventDefault = jest.fn();
target.keydown({key: ' ', preventDefault});
@@ -204,7 +226,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is not called after "keydown" for other keys', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'a'});
expect(onPressStart).not.toBeCalled();
@@ -214,7 +238,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
describe('onPressEnd', () => {
let onPressEnd, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPressEnd = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -225,22 +249,35 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
+ };
+
+ // @gate experimental
+ it('is called after pointer up: mouse', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'mouse'});
+ target.pointerup({pointerType: 'mouse'});
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
+ expect(onPressEnd).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'pressend'}),
+ );
});
- it.each(pointerTypesTable)(
- 'is called after pointer up: %s',
- pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- target.pointerup({pointerType});
- expect(onPressEnd).toHaveBeenCalledTimes(1);
- expect(onPressEnd).toHaveBeenCalledWith(
- expect.objectContaining({pointerType, type: 'pressend'}),
- );
- },
- );
+ // @gate experimental
+ it('is called after pointer up: touch', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'touch'});
+ target.pointerup({pointerType: 'touch'});
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
+ expect(onPressEnd).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'touch', type: 'pressend'}),
+ );
+ });
+ // @gate experimental
it('is called after middle-button pointer up', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({
button: buttonType.auxiliary,
@@ -258,7 +295,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called after virtual middle-button pointer up', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({
button: buttonType.auxiliary,
@@ -276,7 +315,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called after "keyup" event for Enter', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
// click occurs before keyup
@@ -288,7 +329,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called after "keyup" event for Spacebar', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: ' '});
target.keyup({key: ' '});
@@ -298,14 +341,18 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is not called after "keyup" event for other keys', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({key: 'a'});
expect(onPressEnd).not.toBeCalled();
});
+ // @gate experimental
it('is called with keyboard modifiers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({
@@ -331,7 +378,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
describe('onPressChange', () => {
let onPressChange, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPressChange = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -342,22 +389,35 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
+ };
+
+ // @gate experimental
+ it('is called after pointer down and up: %s', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'mouse'});
+ expect(onPressChange).toHaveBeenCalledTimes(1);
+ expect(onPressChange).toHaveBeenCalledWith(true);
+ target.pointerup({pointerType: 'mouse'});
+ expect(onPressChange).toHaveBeenCalledTimes(2);
+ expect(onPressChange).toHaveBeenCalledWith(false);
});
- it.each(pointerTypesTable)(
- 'is called after pointer down and up: %s',
- pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- expect(onPressChange).toHaveBeenCalledTimes(1);
- expect(onPressChange).toHaveBeenCalledWith(true);
- target.pointerup({pointerType});
- expect(onPressChange).toHaveBeenCalledTimes(2);
- expect(onPressChange).toHaveBeenCalledWith(false);
- },
- );
+ // @gate experimental
+ it('is called after pointer down and up: touch', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'touch'});
+ expect(onPressChange).toHaveBeenCalledTimes(1);
+ expect(onPressChange).toHaveBeenCalledWith(true);
+ target.pointerup({pointerType: 'touch'});
+ expect(onPressChange).toHaveBeenCalledTimes(2);
+ expect(onPressChange).toHaveBeenCalledWith(false);
+ });
+ // @gate experimental
it('is called after valid "keydown" and "keyup" events', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
expect(onPressChange).toHaveBeenCalledTimes(1);
@@ -371,7 +431,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
describe('onPress', () => {
let onPress, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPress = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -388,22 +448,35 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
right: 100,
});
document.elementFromPoint = () => ref.current;
+ };
+
+ // @gate experimental
+ it('is called after pointer up: %s', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'mouse'});
+ target.pointerup({pointerType: 'mouse', x: 10, y: 10});
+ expect(onPress).toHaveBeenCalledTimes(1);
+ expect(onPress).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'press'}),
+ );
});
- it.each(pointerTypesTable)(
- 'is called after pointer up: %s',
- pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- target.pointerup({pointerType, x: 10, y: 10});
- expect(onPress).toHaveBeenCalledTimes(1);
- expect(onPress).toHaveBeenCalledWith(
- expect.objectContaining({pointerType, type: 'press'}),
- );
- },
- );
+ // @gate experimental
+ it('is called after pointer up: %s', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'touch'});
+ target.pointerup({pointerType: 'touch', x: 10, y: 10});
+ expect(onPress).toHaveBeenCalledTimes(1);
+ expect(onPress).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'touch', type: 'press'}),
+ );
+ });
+ // @gate experimental
it('is not called after middle-button press', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({
button: buttonType.auxiliary,
@@ -414,7 +487,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
expect(onPress).not.toHaveBeenCalled();
});
+ // @gate experimental
it('is not called after virtual middle-button press', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({
button: buttonType.auxiliary,
@@ -425,7 +500,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
expect(onPress).not.toHaveBeenCalled();
});
+ // @gate experimental
it('is called after valid "keyup" event', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({key: 'Enter'});
@@ -435,7 +512,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is not called after invalid "keyup" event', () => {
+ componentInit();
const inputRef = React.createRef();
const Component = () => {
const listener = usePress({onPress});
@@ -450,7 +529,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
expect(onPress).not.toBeCalled();
});
+ // @gate experimental
it('is called with modifier keys', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({metaKey: true, pointerType: 'mouse'});
target.pointerup({metaKey: true, pointerType: 'mouse'});
@@ -463,7 +544,9 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called once after virtual screen reader "click" event', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const preventDefault = jest.fn();
target.virtualclick({preventDefault});
@@ -481,7 +564,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
describe('onPressMove', () => {
let onPressMove, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPressMove = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -498,25 +581,41 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
right: 100,
});
document.elementFromPoint = () => ref.current;
+ };
+
+ // @gate experimental
+ it('is called after pointer move: mouse', () => {
+ componentInit();
+ const node = ref.current;
+ const target = createEventTarget(node);
+ target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
+ target.pointerdown({pointerType: 'mouse'});
+ target.pointermove({pointerType: 'mouse', x: 10, y: 10});
+ target.pointermove({pointerType: 'mouse', x: 20, y: 20});
+ expect(onPressMove).toHaveBeenCalledTimes(2);
+ expect(onPressMove).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'pressmove'}),
+ );
});
- it.each(pointerTypesTable)(
- 'is called after pointer move: %s',
- pointerType => {
- const node = ref.current;
- const target = createEventTarget(node);
- target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
- target.pointerdown({pointerType});
- target.pointermove({pointerType, x: 10, y: 10});
- target.pointermove({pointerType, x: 20, y: 20});
- expect(onPressMove).toHaveBeenCalledTimes(2);
- expect(onPressMove).toHaveBeenCalledWith(
- expect.objectContaining({pointerType, type: 'pressmove'}),
- );
- },
- );
+ // @gate experimental
+ it('is called after pointer move: touch', () => {
+ componentInit();
+ const node = ref.current;
+ const target = createEventTarget(node);
+ target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
+ target.pointerdown({pointerType: 'touch'});
+ target.pointermove({pointerType: 'touch', x: 10, y: 10});
+ target.pointermove({pointerType: 'touch', x: 20, y: 20});
+ expect(onPressMove).toHaveBeenCalledTimes(2);
+ expect(onPressMove).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'touch', type: 'pressmove'}),
+ );
+ });
+ // @gate experimental
it('is not called if pointer move occurs during keyboard press', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
target.keydown({key: 'Enter'});
@@ -531,6 +630,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
});
describe('link components', () => {
+ // @gate experimental
it('prevents native behavior by default', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -551,6 +651,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('prevents native behaviour for keyboard events by default', () => {
const onPress = jest.fn();
const preventDefaultClick = jest.fn();
@@ -575,6 +676,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('deeply prevents native behaviour by default', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -596,6 +698,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
expect(preventDefault).toBeCalled();
});
+ // @gate experimental
it('prevents native behaviour by default with nested elements', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -620,6 +723,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('uses native behaviour for interactions with modifier keys', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -642,6 +746,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
});
});
+ // @gate experimental
it('uses native behaviour for pointer events if preventDefault is false', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -662,6 +767,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('uses native behaviour for keyboard events if preventDefault is false', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -685,6 +791,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
});
});
+ // @gate experimental
it('should not trigger an invariant in addRootEventTypes()', () => {
const ref = React.createRef();
@@ -701,6 +808,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
target.pointerdown();
});
+ // @gate experimental
it('when blur occurs on a pressed target, we should disengage press', () => {
const onPress = jest.fn();
const onPressStart = jest.fn();
diff --git a/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js
index 53f3a9cbb8..abb9518890 100644
--- a/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js
@@ -30,9 +30,14 @@ function initializeModules(hasPointerEvents) {
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
- PressResponder = require('react-interactions/events/press-legacy')
- .PressResponder;
- usePress = require('react-interactions/events/press-legacy').usePress;
+
+ // TODO: This import throws outside of experimental mode. Figure out better
+ // strategy for gated imports.
+ if (__EXPERIMENTAL__) {
+ PressResponder = require('react-interactions/events/press-legacy')
+ .PressResponder;
+ usePress = require('react-interactions/events/press-legacy').usePress;
+ }
}
function removePressMoveStrings(eventString) {
@@ -50,11 +55,6 @@ const pointerTypesTable = [['mouse'], ['touch']];
describe.each(environmentTable)('Press responder', hasPointerEvents => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
initializeModules(hasPointerEvents);
container = document.createElement('div');
@@ -71,7 +71,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
describe('disabled', () => {
let onPressStart, onPress, onPressEnd, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPressStart = jest.fn();
onPress = jest.fn();
onPressEnd = jest.fn();
@@ -87,9 +87,11 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
- });
+ };
+ // @gate experimental
it('does not call callbacks', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown();
target.pointerup();
@@ -102,7 +104,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
describe('onPressStart', () => {
let onPressStart, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPressStart = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -113,21 +115,33 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
+ };
+
+ // @gate experimental
+ it('is called after pointer down: mouse', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'mouse'});
+ expect(onPressStart).toHaveBeenCalledTimes(1);
+ expect(onPressStart).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'pressstart'}),
+ );
});
- it.each(pointerTypesTable)(
- 'is called after pointer down: %s',
- pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- expect(onPressStart).toHaveBeenCalledTimes(1);
- expect(onPressStart).toHaveBeenCalledWith(
- expect.objectContaining({pointerType, type: 'pressstart'}),
- );
- },
- );
+ // @gate experimental
+ it('is called after pointer down: touch', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'touch'});
+ expect(onPressStart).toHaveBeenCalledTimes(1);
+ expect(onPressStart).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'touch', type: 'pressstart'}),
+ );
+ });
+ // @gate experimental
it('is called after middle-button pointer down', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({
button: buttonType.auxiliary,
@@ -144,7 +158,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is not called after pointer move following middle-button press', () => {
+ componentInit();
const node = ref.current;
const target = createEventTarget(node);
target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
@@ -159,7 +175,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPressStart).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('ignores any events not caused by primary/middle-click or touch/pen contact', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({buttons: buttonsType.secondary});
target.pointerup({buttons: buttonsType.secondary});
@@ -168,7 +186,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPressStart).toHaveBeenCalledTimes(0);
});
+ // @gate experimental
it('is called once after "keydown" events for Enter', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keydown({key: 'Enter'});
@@ -179,7 +199,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called once after "keydown" events for Spacebar', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const preventDefault = jest.fn();
target.keydown({key: ' ', preventDefault});
@@ -194,7 +216,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is not called after "keydown" for other keys', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'a'});
expect(onPressStart).not.toBeCalled();
@@ -204,7 +228,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
describe('onPressEnd', () => {
let onPressEnd, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPressEnd = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -215,22 +239,35 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
+ };
+
+ // @gate experimental
+ it('is called after pointer up: mouse', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'mouse'});
+ target.pointerup({pointerType: 'mouse'});
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
+ expect(onPressEnd).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'pressend'}),
+ );
});
- it.each(pointerTypesTable)(
- 'is called after pointer up: %s',
- pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- target.pointerup({pointerType});
- expect(onPressEnd).toHaveBeenCalledTimes(1);
- expect(onPressEnd).toHaveBeenCalledWith(
- expect.objectContaining({pointerType, type: 'pressend'}),
- );
- },
- );
+ // @gate experimental
+ it('is called after pointer up: touch', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'touch'});
+ target.pointerup({pointerType: 'touch'});
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
+ expect(onPressEnd).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'touch', type: 'pressend'}),
+ );
+ });
+ // @gate experimental
it('is called after middle-button pointer up', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({
buttons: buttonsType.auxiliary,
@@ -247,7 +284,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called after "keyup" event for Enter', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
// click occurs before keyup
@@ -259,7 +298,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called after "keyup" event for Spacebar', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: ' '});
target.keyup({key: ' '});
@@ -269,14 +310,18 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is not called after "keyup" event for other keys', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({key: 'a'});
expect(onPressEnd).not.toBeCalled();
});
+ // @gate experimental
it('is called with keyboard modifiers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({
@@ -302,7 +347,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
describe('onPressChange', () => {
let onPressChange, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPressChange = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -313,22 +358,35 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
+ };
+
+ // @gate experimental
+ it('is called after pointer down and up: mouse', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'mouse'});
+ expect(onPressChange).toHaveBeenCalledTimes(1);
+ expect(onPressChange).toHaveBeenCalledWith(true);
+ target.pointerup({pointerType: 'mouse'});
+ expect(onPressChange).toHaveBeenCalledTimes(2);
+ expect(onPressChange).toHaveBeenCalledWith(false);
});
- it.each(pointerTypesTable)(
- 'is called after pointer down and up: %s',
- pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- expect(onPressChange).toHaveBeenCalledTimes(1);
- expect(onPressChange).toHaveBeenCalledWith(true);
- target.pointerup({pointerType});
- expect(onPressChange).toHaveBeenCalledTimes(2);
- expect(onPressChange).toHaveBeenCalledWith(false);
- },
- );
+ // @gate experimental
+ it('is called after pointer down and up: touch', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'touch'});
+ expect(onPressChange).toHaveBeenCalledTimes(1);
+ expect(onPressChange).toHaveBeenCalledWith(true);
+ target.pointerup({pointerType: 'touch'});
+ expect(onPressChange).toHaveBeenCalledTimes(2);
+ expect(onPressChange).toHaveBeenCalledWith(false);
+ });
+ // @gate experimental
it('is called after valid "keydown" and "keyup" events', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
expect(onPressChange).toHaveBeenCalledTimes(1);
@@ -342,7 +400,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
describe('onPress', () => {
let onPress, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPress = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -359,22 +417,35 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
right: 100,
});
document.elementFromPoint = () => ref.current;
+ };
+
+ // @gate experimental
+ it('is called after pointer up: mouse', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'mouse'});
+ target.pointerup({pointerType: 'mouse', x: 10, y: 10});
+ expect(onPress).toHaveBeenCalledTimes(1);
+ expect(onPress).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'press'}),
+ );
});
- it.each(pointerTypesTable)(
- 'is called after pointer up: %s',
- pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- target.pointerup({pointerType, x: 10, y: 10});
- expect(onPress).toHaveBeenCalledTimes(1);
- expect(onPress).toHaveBeenCalledWith(
- expect.objectContaining({pointerType, type: 'press'}),
- );
- },
- );
+ // @gate experimental
+ it('is called after pointer up: touch', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'touch'});
+ target.pointerup({pointerType: 'touch', x: 10, y: 10});
+ expect(onPress).toHaveBeenCalledTimes(1);
+ expect(onPress).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'touch', type: 'press'}),
+ );
+ });
+ // @gate experimental
it('is not called after middle-button press', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({
buttons: buttonsType.auxiliary,
@@ -384,7 +455,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPress).not.toHaveBeenCalled();
});
+ // @gate experimental
it('is not called after virtual middle-button press', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({
button: buttonType.auxiliary,
@@ -395,7 +468,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPress).not.toHaveBeenCalled();
});
+ // @gate experimental
it('is called after valid "keyup" event', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({key: 'Enter'});
@@ -405,7 +480,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is not called after invalid "keyup" event', () => {
+ componentInit();
const inputRef = React.createRef();
const Component = () => {
const listener = usePress({onPress});
@@ -420,7 +497,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPress).not.toBeCalled();
});
+ // @gate experimental
it('is called with modifier keys', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown({metaKey: true, pointerType: 'mouse'});
target.pointerup({metaKey: true, pointerType: 'mouse'});
@@ -433,7 +512,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('is called if target rect is not right but the target is (for mouse events)', () => {
+ componentInit();
const buttonRef = React.createRef();
const divRef = React.createRef();
@@ -455,7 +536,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPress).toBeCalled();
});
+ // @gate experimental
it('is called once after virtual screen reader "click" event', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const preventDefault = jest.fn();
target.virtualclick({preventDefault});
@@ -473,7 +556,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
describe('onPressMove', () => {
let onPressMove, ref;
- beforeEach(() => {
+ const componentInit = () => {
onPressMove = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -490,25 +573,41 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
right: 100,
});
document.elementFromPoint = () => ref.current;
+ };
+
+ // @gate experimental
+ it('is called after pointer move: mouse', () => {
+ componentInit();
+ const node = ref.current;
+ const target = createEventTarget(node);
+ target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
+ target.pointerdown({pointerType: 'mouse'});
+ target.pointermove({pointerType: 'mouse', x: 10, y: 10});
+ target.pointermove({pointerType: 'mouse', x: 20, y: 20});
+ expect(onPressMove).toHaveBeenCalledTimes(2);
+ expect(onPressMove).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'pressmove'}),
+ );
});
- it.each(pointerTypesTable)(
- 'is called after pointer move: %s',
- pointerType => {
- const node = ref.current;
- const target = createEventTarget(node);
- target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
- target.pointerdown({pointerType});
- target.pointermove({pointerType, x: 10, y: 10});
- target.pointermove({pointerType, x: 20, y: 20});
- expect(onPressMove).toHaveBeenCalledTimes(2);
- expect(onPressMove).toHaveBeenCalledWith(
- expect.objectContaining({pointerType, type: 'pressmove'}),
- );
- },
- );
+ // @gate experimental
+ it('is called after pointer move: touch', () => {
+ componentInit();
+ const node = ref.current;
+ const target = createEventTarget(node);
+ target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
+ target.pointerdown({pointerType: 'touch'});
+ target.pointermove({pointerType: 'touch', x: 10, y: 10});
+ target.pointermove({pointerType: 'touch', x: 20, y: 20});
+ expect(onPressMove).toHaveBeenCalledTimes(2);
+ expect(onPressMove).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'touch', type: 'pressmove'}),
+ );
+ });
+ // @gate experimental
it('is not called if pointer move occurs during keyboard press', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
target.keydown({key: 'Enter'});
@@ -525,7 +624,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
describe.each(pointerTypesTable)('press with movement: %s', pointerType => {
let events, ref, outerRef;
- beforeEach(() => {
+ const componentInit = () => {
events = [];
ref = React.createRef();
outerRef = React.createRef();
@@ -548,7 +647,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
- });
+ };
const rectMock = {width: 100, height: 100, x: 50, y: 50};
const pressRectOffset = 20;
@@ -569,7 +668,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
* │ HitRect X │ <= Move to X and release
* └──────────────────┘
*/
+ // @gate experimental
it('"onPress*" events are called immediately', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.setBoundingClientRect(rectMock);
target.pointerdown({pointerType});
@@ -585,7 +686,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
]);
});
+ // @gate experimental
it('"onPress*" events are correctly called with target change', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const outerTarget = createEventTarget(outerRef.current);
target.setBoundingClientRect(rectMock);
@@ -614,7 +717,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
]);
});
+ // @gate experimental
it('press retention offset can be configured', () => {
+ componentInit();
const localEvents = [];
const localRef = React.createRef();
const createEventHandler = msg => () => {
@@ -654,7 +759,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
]);
});
+ // @gate experimental
it('responder region accounts for decrease in element dimensions', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.setBoundingClientRect(rectMock);
target.pointerdown({pointerType});
@@ -674,7 +781,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
]);
});
+ // @gate experimental
it('responder region accounts for increase in element dimensions', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.setBoundingClientRect(rectMock);
target.pointerdown({pointerType});
@@ -704,7 +813,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
* └──────────────────┘
* X <= Move to X and release
*/
+ // @gate experimental
it('"onPress" is not called on release', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const targetContainer = createEventTarget(container);
target.setBoundingClientRect(rectMock);
@@ -727,7 +838,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
});
});
+ // @gate experimental
it('"onPress" is called on re-entry to hit rect', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const targetContainer = createEventTarget(container);
target.setBoundingClientRect(rectMock);
@@ -759,6 +872,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
describe('nested responders', () => {
if (hasPointerEvents) {
+ // @gate experimental
it('dispatch events in the correct order', () => {
const events = [];
const ref = React.createRef();
@@ -819,6 +933,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
}
describe('correctly not propagate', () => {
+ // @gate experimental
it('for onPress', () => {
const ref = React.createRef();
const onPress = jest.fn();
@@ -845,6 +960,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPress).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('for onPressStart/onPressEnd', () => {
const ref = React.createRef();
const onPressStart = jest.fn();
@@ -874,6 +990,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('for onPressChange', () => {
const ref = React.createRef();
const onPressChange = jest.fn();
@@ -903,6 +1020,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
});
describe('link components', () => {
+ // @gate experimental
it('prevents native behavior by default', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -923,6 +1041,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('prevents native behaviour for keyboard events by default', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -943,6 +1062,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('deeply prevents native behaviour by default', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -964,6 +1084,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(preventDefault).toBeCalled();
});
+ // @gate experimental
it('prevents native behaviour by default with nested elements', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -988,6 +1109,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('uses native behaviour for interactions with modifier keys', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -1010,6 +1132,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
});
});
+ // @gate experimental
it('uses native behaviour for pointer events if preventDefault is false', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -1030,6 +1153,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
+ // @gate experimental
it('uses native behaviour for keyboard events if preventDefault is false', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
@@ -1053,7 +1177,8 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
});
describe('responder cancellation', () => {
- it.each(pointerTypesTable)('ends on pointer cancel', pointerType => {
+ // @gate experimental
+ it('ends on pointer cancel: mouse', () => {
const onPressEnd = jest.fn();
const ref = React.createRef();
@@ -1064,12 +1189,30 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
ReactDOM.render( , container);
const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- target.pointercancel({pointerType});
+ target.pointerdown({pointerType: 'mouse'});
+ target.pointercancel({pointerType: 'mouse'});
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
+ });
+
+ // @gate experimental
+ it('ends on pointer cancel: touch', () => {
+ const onPressEnd = jest.fn();
+ const ref = React.createRef();
+
+ const Component = () => {
+ const listener = usePress({onPressEnd});
+ return ;
+ };
+ ReactDOM.render( , container);
+
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType: 'touch'});
+ target.pointercancel({pointerType: 'touch'});
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
});
+ // @gate experimental
it('does end on "scroll" to document (not mouse)', () => {
const onPressEnd = jest.fn();
const ref = React.createRef();
@@ -1087,6 +1230,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('does end on "scroll" to a parent container (not mouse)', () => {
const onPressEnd = jest.fn();
const ref = React.createRef();
@@ -1109,6 +1253,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('does not end on "scroll" to an element outside', () => {
const onPressEnd = jest.fn();
const ref = React.createRef();
@@ -1132,10 +1277,12 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPressEnd).not.toBeCalled();
});
+ // @gate experimental
it('expect displayName to show up for event component', () => {
expect(PressResponder.displayName).toBe('Press');
});
+ // @gate experimental
it('should not trigger an invariant in addRootEventTypes()', () => {
const ref = React.createRef();
@@ -1152,6 +1299,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
target.pointerdown();
});
+ // @gate experimental
it('event.preventDefault works as expected', () => {
const onPress = jest.fn(e => e.preventDefault());
const onPressStart = jest.fn(e => e.preventDefault());
@@ -1173,6 +1321,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
expect(onPressEnd).toBeCalled();
});
+ // @gate experimental
it('when blur occurs on a pressed target, we should disengage press', () => {
const onPress = jest.fn();
const onPressStart = jest.fn();
diff --git a/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js
index 17c550a248..f986874450 100644
--- a/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js
@@ -31,7 +31,12 @@ function initializeModules(hasPointerEvents) {
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
- useTap = require('react-interactions/events/tap').useTap;
+
+ // TODO: This import throws outside of experimental mode. Figure out better
+ // strategy for gated imports.
+ if (__EXPERIMENTAL__) {
+ useTap = require('react-interactions/events/tap').useTap;
+ }
}
const coordinatesInside = {x: 51, y: 51};
@@ -73,11 +78,6 @@ function tapAndReleaseOutside({
describeWithPointerEvent('Tap responder', hasPointerEvents => {
let container;
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
initializeModules(hasPointerEvents);
container = document.createElement('div');
@@ -91,6 +91,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
resetActivePointers();
});
+ // @gate experimental
test('supports repeated use', () => {
const ref = React.createRef();
const Component = () => {
@@ -114,7 +115,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
describe('disabled', () => {
let onTapStart, onTapChange, onTapUpdate, onTapCancel, onTapEnd, ref;
- beforeEach(() => {
+ const componentInit = () => {
onTapStart = jest.fn();
onTapChange = jest.fn();
onTapUpdate = jest.fn();
@@ -133,9 +134,11 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
return
;
};
ReactDOM.render( , container);
- });
+ };
+ // @gate experimental
test('does not call callbacks', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown();
target.pointerup();
@@ -159,7 +162,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
document.elementFromPoint = () => ref.current;
}
- beforeEach(() => {
+ const componentInit = () => {
onTapCancel = jest.fn();
onTapUpdate = jest.fn();
ref = React.createRef();
@@ -168,9 +171,11 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
onTapCancel,
onTapUpdate,
});
- });
+ };
+ // @gate experimental
test('ignores values less than 10', () => {
+ componentInit();
render({
maximumDistance: 5,
onTapCancel,
@@ -184,27 +189,33 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
expect(onTapCancel).toHaveBeenCalledTimes(0);
});
- testWithPointerType('below threshold', pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType, x: 0, y: 0});
- target.pointermove({pointerType, x: 10, y: 10});
- expect(onTapUpdate).toHaveBeenCalledTimes(1);
- expect(onTapCancel).toHaveBeenCalledTimes(0);
- });
+ // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
+ // the @gate pragma.
+ if (__EXPERIMENTAL__) {
+ testWithPointerType('below threshold', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType, x: 0, y: 0});
+ target.pointermove({pointerType, x: 10, y: 10});
+ expect(onTapUpdate).toHaveBeenCalledTimes(1);
+ expect(onTapCancel).toHaveBeenCalledTimes(0);
+ });
- testWithPointerType('above threshold', pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType, x: 0, y: 0});
- target.pointermove({pointerType, x: 15, y: 14});
- expect(onTapUpdate).toHaveBeenCalledTimes(0);
- expect(onTapCancel).toHaveBeenCalledTimes(1);
- });
+ testWithPointerType('above threshold', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType, x: 0, y: 0});
+ target.pointermove({pointerType, x: 15, y: 14});
+ expect(onTapUpdate).toHaveBeenCalledTimes(0);
+ expect(onTapCancel).toHaveBeenCalledTimes(1);
+ });
+ }
});
describe('onAuxiliaryTap', () => {
let onAuxiliaryTap, ref;
- beforeEach(() => {
+ const componentInit = () => {
onAuxiliaryTap = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -213,9 +224,11 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
- });
+ };
+ // @gate experimental
test('auxiliary-button pointer up', () => {
+ componentInit();
const pointerType = 'mouse';
const button = buttonType.auxiliary;
const buttons = buttonsType.auxiliary;
@@ -225,7 +238,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
expect(onAuxiliaryTap).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
test('modifier-button pointer up', () => {
+ componentInit();
const pointerType = 'mouse';
const button = buttonType.primary;
const buttons = buttonsType.primary;
@@ -239,7 +254,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
describe('onTapStart', () => {
let onTapStart, ref;
- beforeEach(() => {
+ const componentInit = () => {
onTapStart = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -248,54 +263,61 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
- });
+ };
- testWithPointerType('pointer down', pointerType => {
- const target = createEventTarget(ref.current);
- const nativeEvent = {
- button: buttonType.primary,
- buttons: buttonsType.primary,
- pageX: 10,
- pageY: 10,
- pointerType,
- x: 10,
- y: 10,
- };
- target.pointerdown(nativeEvent);
- // 'pointerup' is only for the MouseEvent/TouchEvent fallback
- // implementation. We also dispatch 'pointerup' so that this test covers
- // the case where browsers dispatch an emulated mousedown (and mouseup)
- // event *after* a touch ends.
- target.pointerup(nativeEvent);
- expect(onTapStart).toHaveBeenCalledTimes(1);
- expect(onTapStart).toHaveBeenCalledWith(
- expect.objectContaining({
- altKey: false,
- ctrlKey: false,
- height: pointerType === 'mouse' ? 1 : 23,
- metaKey: false,
+ // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
+ // the @gate pragma.
+ if (__EXPERIMENTAL__) {
+ testWithPointerType('pointer down', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ const nativeEvent = {
+ button: buttonType.primary,
+ buttons: buttonsType.primary,
pageX: 10,
pageY: 10,
pointerType,
- pressure: pointerType === 'touch' ? 1 : 0.5,
- screenX: 10,
- screenY: 60,
- shiftKey: false,
- tangentialPressure: 0,
- target: target.node,
- tiltX: 0,
- tiltY: 0,
- timeStamp: expect.any(Number),
- twist: 0,
- type: 'tap:start',
- width: pointerType === 'mouse' ? 1 : 23,
x: 10,
y: 10,
- }),
- );
- });
+ };
+ target.pointerdown(nativeEvent);
+ // 'pointerup' is only for the MouseEvent/TouchEvent fallback
+ // implementation. We also dispatch 'pointerup' so that this test covers
+ // the case where browsers dispatch an emulated mousedown (and mouseup)
+ // event *after* a touch ends.
+ target.pointerup(nativeEvent);
+ expect(onTapStart).toHaveBeenCalledTimes(1);
+ expect(onTapStart).toHaveBeenCalledWith(
+ expect.objectContaining({
+ altKey: false,
+ ctrlKey: false,
+ height: pointerType === 'mouse' ? 1 : 23,
+ metaKey: false,
+ pageX: 10,
+ pageY: 10,
+ pointerType,
+ pressure: pointerType === 'touch' ? 1 : 0.5,
+ screenX: 10,
+ screenY: 60,
+ shiftKey: false,
+ tangentialPressure: 0,
+ target: target.node,
+ tiltX: 0,
+ tiltY: 0,
+ timeStamp: expect.any(Number),
+ twist: 0,
+ type: 'tap:start',
+ width: pointerType === 'mouse' ? 1 : 23,
+ x: 10,
+ y: 10,
+ }),
+ );
+ });
+ }
+ // @gate experimental
test('second pointer on target', () => {
+ componentInit();
const pointerType = 'touch';
const target = createEventTarget(ref.current);
const button = buttonType.primary;
@@ -306,79 +328,84 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
expect(onTapStart).toHaveBeenCalledTimes(1);
});
- testWithPointerType('ignored buttons and modifiers', pointerType => {
- const target = createEventTarget(ref.current);
- if (pointerType !== 'touch') {
- // right-click
+ // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
+ // the @gate pragma.
+ if (__EXPERIMENTAL__) {
+ testWithPointerType('ignored buttons and modifiers', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ if (pointerType !== 'touch') {
+ // right-click
+ target.pointerdown({
+ button: buttonType.secondary,
+ buttons: buttonsType.secondary,
+ pointerType,
+ });
+ target.pointerup({pointerType});
+ // middle-click
+ target.pointerdown({
+ button: buttonType.auxiliary,
+ buttons: buttonsType.auxiliary,
+ pointerType,
+ });
+ target.pointerup({pointerType});
+ // virtual middle-click with misleading 'buttons' value
+ target.pointerdown({
+ button: buttonType.auxiliary,
+ buttons: 0,
+ pointerType,
+ });
+ target.pointerup({pointerType});
+ // pen eraser
+ target.pointerdown({
+ button: buttonType.eraser,
+ buttons: buttonsType.eraser,
+ pointerType,
+ });
+ target.pointerup({pointerType});
+ }
+ // alt-click
target.pointerdown({
- button: buttonType.secondary,
- buttons: buttonsType.secondary,
+ button: buttonType.primary,
+ buttons: buttonsType.primary,
+ altKey: true,
pointerType,
});
target.pointerup({pointerType});
- // middle-click
+ // ctrl-click
target.pointerdown({
- button: buttonType.auxiliary,
- buttons: buttonsType.auxiliary,
+ button: buttonType.primary,
+ buttons: buttonsType.primary,
+ ctrlKey: true,
pointerType,
});
target.pointerup({pointerType});
- // virtual middle-click with misleading 'buttons' value
+ // meta-click
target.pointerdown({
- button: buttonType.auxiliary,
- buttons: 0,
+ button: buttonType.primary,
+ buttons: buttonsType.primary,
+ metaKey: true,
pointerType,
});
target.pointerup({pointerType});
- // pen eraser
+ // shift-click
target.pointerdown({
- button: buttonType.eraser,
- buttons: buttonsType.eraser,
+ button: buttonType.primary,
+ buttons: buttonsType.primary,
+ shiftKey: true,
pointerType,
});
target.pointerup({pointerType});
- }
- // alt-click
- target.pointerdown({
- button: buttonType.primary,
- buttons: buttonsType.primary,
- altKey: true,
- pointerType,
- });
- target.pointerup({pointerType});
- // ctrl-click
- target.pointerdown({
- button: buttonType.primary,
- buttons: buttonsType.primary,
- ctrlKey: true,
- pointerType,
- });
- target.pointerup({pointerType});
- // meta-click
- target.pointerdown({
- button: buttonType.primary,
- buttons: buttonsType.primary,
- metaKey: true,
- pointerType,
- });
- target.pointerup({pointerType});
- // shift-click
- target.pointerdown({
- button: buttonType.primary,
- buttons: buttonsType.primary,
- shiftKey: true,
- pointerType,
- });
- target.pointerup({pointerType});
- expect(onTapStart).toHaveBeenCalledTimes(0);
- });
+ expect(onTapStart).toHaveBeenCalledTimes(0);
+ });
+ }
});
describe('onTapEnd', () => {
let onTapEnd, ref;
- beforeEach(() => {
+ const componentInit = () => {
onTapEnd = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -387,87 +414,96 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
- });
+ };
- testWithPointerType('pointer up', pointerType => {
- const target = createEventTarget(ref.current);
- const button = buttonType.primary;
- const buttons = buttonsType.primary;
- target.pointerdown({button, buttons, pointerType});
- target.pointerup({
- button,
- buttons,
- pageX: 10,
- pageY: 10,
- pointerType,
- x: 10,
- y: 10,
- });
- expect(onTapEnd).toHaveBeenCalledTimes(1);
- expect(onTapEnd).toHaveBeenCalledWith(
- expect.objectContaining({
- altKey: false,
- ctrlKey: false,
- height: pointerType === 'mouse' ? 1 : 23,
- metaKey: false,
+ // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
+ // the @gate pragma.
+ if (__EXPERIMENTAL__) {
+ testWithPointerType('pointer up', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ const button = buttonType.primary;
+ const buttons = buttonsType.primary;
+ target.pointerdown({button, buttons, pointerType});
+ target.pointerup({
+ button,
+ buttons,
pageX: 10,
pageY: 10,
pointerType,
- pressure: 0,
- screenX: 10,
- screenY: 60,
- shiftKey: false,
- tangentialPressure: 0,
- target: target.node,
- tiltX: 0,
- tiltY: 0,
- timeStamp: expect.any(Number),
- twist: 0,
- type: 'tap:end',
- width: pointerType === 'mouse' ? 1 : 23,
x: 10,
y: 10,
- }),
- );
- });
-
- testWithPointerType('zero-dimension hit rect', pointerType => {
- const targetRef = React.createRef();
- const innerRef = React.createRef();
-
- const Component = () => {
- const listener = useTap({onTapEnd});
- return (
-
-
-
+ });
+ expect(onTapEnd).toHaveBeenCalledTimes(1);
+ expect(onTapEnd).toHaveBeenCalledWith(
+ expect.objectContaining({
+ altKey: false,
+ ctrlKey: false,
+ height: pointerType === 'mouse' ? 1 : 23,
+ metaKey: false,
+ pageX: 10,
+ pageY: 10,
+ pointerType,
+ pressure: 0,
+ screenX: 10,
+ screenY: 60,
+ shiftKey: false,
+ tangentialPressure: 0,
+ target: target.node,
+ tiltX: 0,
+ tiltY: 0,
+ timeStamp: expect.any(Number),
+ twist: 0,
+ type: 'tap:end',
+ width: pointerType === 'mouse' ? 1 : 23,
+ x: 10,
+ y: 10,
+ }),
);
- };
- ReactDOM.render( , container);
- document.elementFromPoint = () => innerRef.current;
-
- const target = createEventTarget(targetRef.current);
- target.setBoundingClientRect({x: 0, y: 0, width: 0, height: 0});
- const innerTarget = createEventTarget(innerRef.current);
- innerTarget.pointerdown({pointerType});
- innerTarget.pointerup({pointerType});
- expect(onTapEnd).toBeCalled();
- });
-
- testWithPointerType('pointer up outside target', pointerType => {
- const downTarget = createEventTarget(ref.current);
- const upTarget = createEventTarget(container);
- tapAndReleaseOutside({
- hasPointerEvents,
- downTarget,
- upTarget,
- pointerType,
});
- expect(onTapEnd).not.toBeCalled();
- });
+
+ testWithPointerType('zero-dimension hit rect', pointerType => {
+ componentInit();
+ const targetRef = React.createRef();
+ const innerRef = React.createRef();
+
+ const Component = () => {
+ const listener = useTap({onTapEnd});
+ return (
+
+
+
+ );
+ };
+ ReactDOM.render( , container);
+ document.elementFromPoint = () => innerRef.current;
+
+ const target = createEventTarget(targetRef.current);
+ target.setBoundingClientRect({x: 0, y: 0, width: 0, height: 0});
+ const innerTarget = createEventTarget(innerRef.current);
+ innerTarget.pointerdown({pointerType});
+ innerTarget.pointerup({pointerType});
+ expect(onTapEnd).toBeCalled();
+ });
+
+ testWithPointerType('pointer up outside target', pointerType => {
+ componentInit();
+ const downTarget = createEventTarget(ref.current);
+ const upTarget = createEventTarget(container);
+ tapAndReleaseOutside({
+ hasPointerEvents,
+ downTarget,
+ upTarget,
+ pointerType,
+ });
+ expect(onTapEnd).not.toBeCalled();
+ });
+ }
if (hasPointerEvents) {
+ // @gate experimental
test('second pointer up off target', () => {
+ componentInit();
const pointerType = 'touch';
const target = createEventTarget(ref.current);
const offTarget = createEventTarget(container);
@@ -490,7 +526,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
});
}
+ // @gate experimental
test('ignored buttons and modifiers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
// right-click
target.pointerdown({
@@ -553,7 +591,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
const rect = {x: 0, y: 0, width: 100, height: 100};
const coordinates = {x: 10, y: 10};
- beforeEach(() => {
+ const componentInit = () => {
onTapUpdate = jest.fn();
ref = React.createRef();
const Component = () => {
@@ -562,81 +600,90 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
- });
+ };
- testWithPointerType('requires activation', pointerType => {
- const target = createEventTarget(ref.current);
- target.setBoundingClientRect(rect);
- if (pointerType !== 'touch') {
- target.pointerhover({pointerType, ...coordinates});
- target.pointermove({pointerType, ...coordinates});
- }
- expect(onTapUpdate).not.toBeCalled();
- });
+ // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
+ // the @gate pragma.
+ if (__EXPERIMENTAL__) {
+ testWithPointerType('requires activation', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.setBoundingClientRect(rect);
+ if (pointerType !== 'touch') {
+ target.pointerhover({pointerType, ...coordinates});
+ target.pointermove({pointerType, ...coordinates});
+ }
+ expect(onTapUpdate).not.toBeCalled();
+ });
- testWithPointerType('pointer move', pointerType => {
- const target = createEventTarget(ref.current);
- target.setBoundingClientRect(rect);
- target.pointerdown({pointerType});
+ testWithPointerType('pointer move', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.setBoundingClientRect(rect);
+ target.pointerdown({pointerType});
- target.pointermove({pointerType, x: 10, y: 10});
- expect(onTapUpdate).toHaveBeenCalledWith(
- expect.objectContaining({
- altKey: false,
- ctrlKey: false,
- height: pointerType === 'mouse' ? 1 : 23,
- metaKey: false,
- pageX: 10,
- pageY: 10,
- pointerType,
- pressure: pointerType === 'touch' ? 1 : 0.5,
- screenX: 10,
- screenY: 60,
- shiftKey: false,
- tangentialPressure: 0,
- target: target.node,
- tiltX: 0,
- tiltY: 0,
- timeStamp: expect.any(Number),
- twist: 0,
- type: 'tap:update',
- width: pointerType === 'mouse' ? 1 : 23,
- x: 10,
- y: 10,
- }),
- );
+ target.pointermove({pointerType, x: 10, y: 10});
+ expect(onTapUpdate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ altKey: false,
+ ctrlKey: false,
+ height: pointerType === 'mouse' ? 1 : 23,
+ metaKey: false,
+ pageX: 10,
+ pageY: 10,
+ pointerType,
+ pressure: pointerType === 'touch' ? 1 : 0.5,
+ screenX: 10,
+ screenY: 60,
+ shiftKey: false,
+ tangentialPressure: 0,
+ target: target.node,
+ tiltX: 0,
+ tiltY: 0,
+ timeStamp: expect.any(Number),
+ twist: 0,
+ type: 'tap:update',
+ width: pointerType === 'mouse' ? 1 : 23,
+ x: 10,
+ y: 10,
+ }),
+ );
- target.pointermove({pointerType, x: 20, y: 20});
- expect(onTapUpdate).toHaveBeenCalledWith(
- expect.objectContaining({pointerType, x: 20, y: 20}),
- );
+ target.pointermove({pointerType, x: 20, y: 20});
+ expect(onTapUpdate).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType, x: 20, y: 20}),
+ );
- expect(onTapUpdate).toHaveBeenCalledTimes(2);
- });
+ expect(onTapUpdate).toHaveBeenCalledTimes(2);
+ });
- testWithPointerType('pointer moves outside target', pointerType => {
- const target = createEventTarget(ref.current);
- const containerTarget = createEventTarget(container);
- target.setBoundingClientRect(rect);
- target.pointerdown({pointerType});
- target.pointermove({pointerType, x: 10, y: 10});
- expect(onTapUpdate).toHaveBeenCalledTimes(1);
+ testWithPointerType('pointer moves outside target', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ const containerTarget = createEventTarget(container);
+ target.setBoundingClientRect(rect);
+ target.pointerdown({pointerType});
+ target.pointermove({pointerType, x: 10, y: 10});
+ expect(onTapUpdate).toHaveBeenCalledTimes(1);
- // NOTE: this assumes the PointerEvent implementation calls
- // 'releasePointerCapture' for touch pointers
- if (!hasPointerEvents && pointerType === 'touch') {
- document.elementFromPoint = () => containerTarget.node;
- target.pointermove({pointerType, x: 101, y: 101});
- } else {
- containerTarget.pointermove({pointerType, x: 101, y: 101});
- }
+ // NOTE: this assumes the PointerEvent implementation calls
+ // 'releasePointerCapture' for touch pointers
+ if (!hasPointerEvents && pointerType === 'touch') {
+ document.elementFromPoint = () => containerTarget.node;
+ target.pointermove({pointerType, x: 101, y: 101});
+ } else {
+ containerTarget.pointermove({pointerType, x: 101, y: 101});
+ }
- // No extra 'onTapUpdate' calls when the pointer is outside the target
- expect(onTapUpdate).toHaveBeenCalledTimes(1);
- });
+ // No extra 'onTapUpdate' calls when the pointer is outside the target
+ expect(onTapUpdate).toHaveBeenCalledTimes(1);
+ });
+ }
if (hasPointerEvents) {
+ // @gate experimental
test('second pointer off target', () => {
+ componentInit();
const pointerType = 'touch';
const target = createEventTarget(ref.current);
const offTarget = createEventTarget(container);
@@ -659,7 +706,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
eventsLog.push(msg);
};
- beforeEach(() => {
+ const componentInit = () => {
eventsLog = [];
onTapChange = jest.fn();
ref = React.createRef();
@@ -677,47 +724,54 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
};
ReactDOM.render( , container);
document.elementFromPoint = () => ref.current;
- });
+ };
- testWithPointerType('pointer down/up', pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- expect(onTapChange).toHaveBeenCalledTimes(1);
- expect(onTapChange).toHaveBeenCalledWith(true);
- target.pointerup({pointerType, x: 0, y: 0});
- expect(onTapChange).toHaveBeenCalledTimes(2);
- expect(onTapChange).toHaveBeenCalledWith(false);
- expect(eventsLog).toEqual(['start', 'change', 'change', 'end']);
- });
-
- testWithPointerType('pointer cancel', pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- expect(onTapChange).toHaveBeenCalledTimes(1);
- expect(onTapChange).toHaveBeenCalledWith(true);
- target.pointercancel({pointerType});
- expect(onTapChange).toHaveBeenCalledTimes(2);
- expect(onTapChange).toHaveBeenCalledWith(false);
- expect(eventsLog).toEqual(['start', 'change', 'change', 'cancel']);
- });
-
- testWithPointerType('pointer move outside target', pointerType => {
- const downTarget = createEventTarget(ref.current);
- const upTarget = createEventTarget(container);
- tapAndMoveOutside({
- hasPointerEvents,
- downTarget,
- upTarget,
- pointerType,
+ // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
+ // the @gate pragma.
+ if (__EXPERIMENTAL__) {
+ testWithPointerType('pointer down/up', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType});
+ expect(onTapChange).toHaveBeenCalledTimes(1);
+ expect(onTapChange).toHaveBeenCalledWith(true);
+ target.pointerup({pointerType, x: 0, y: 0});
+ expect(onTapChange).toHaveBeenCalledTimes(2);
+ expect(onTapChange).toHaveBeenCalledWith(false);
+ expect(eventsLog).toEqual(['start', 'change', 'change', 'end']);
});
- expect(onTapChange).toHaveBeenCalledTimes(2);
- });
+
+ testWithPointerType('pointer cancel', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType});
+ expect(onTapChange).toHaveBeenCalledTimes(1);
+ expect(onTapChange).toHaveBeenCalledWith(true);
+ target.pointercancel({pointerType});
+ expect(onTapChange).toHaveBeenCalledTimes(2);
+ expect(onTapChange).toHaveBeenCalledWith(false);
+ expect(eventsLog).toEqual(['start', 'change', 'change', 'cancel']);
+ });
+
+ testWithPointerType('pointer move outside target', pointerType => {
+ componentInit();
+ const downTarget = createEventTarget(ref.current);
+ const upTarget = createEventTarget(container);
+ tapAndMoveOutside({
+ hasPointerEvents,
+ downTarget,
+ upTarget,
+ pointerType,
+ });
+ expect(onTapChange).toHaveBeenCalledTimes(2);
+ });
+ }
});
describe('onTapCancel', () => {
let onTapCancel, onTapUpdate, parentRef, ref, siblingRef;
- beforeEach(() => {
+ const componentInit = () => {
onTapCancel = jest.fn();
onTapUpdate = jest.fn();
parentRef = React.createRef();
@@ -733,43 +787,50 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
);
};
ReactDOM.render( , container);
- });
+ };
- testWithPointerType('pointer cancel', pointerType => {
- const target = createEventTarget(ref.current);
- target.pointerdown({pointerType});
- target.pointercancel({pointerType});
- expect(onTapCancel).toHaveBeenCalledTimes(1);
- expect(onTapCancel).toHaveBeenCalledWith(
- expect.objectContaining({
- altKey: false,
- ctrlKey: false,
- height: 1,
- metaKey: false,
- pageX: 0,
- pageY: 0,
- pointerType,
- pressure: 0,
- screenX: 0,
- screenY: 0,
- shiftKey: false,
- tangentialPressure: 0,
- target: target.node,
- tiltX: 0,
- tiltY: 0,
- timeStamp: expect.any(Number),
- twist: 0,
- type: 'tap:cancel',
- width: 1,
- x: 0,
- y: 0,
- }),
- );
- target.pointermove({pointerType, x: 5, y: 5});
- expect(onTapUpdate).not.toBeCalled();
- });
+ // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
+ // the @gate pragma.
+ if (__EXPERIMENTAL__) {
+ testWithPointerType('pointer cancel', pointerType => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ target.pointerdown({pointerType});
+ target.pointercancel({pointerType});
+ expect(onTapCancel).toHaveBeenCalledTimes(1);
+ expect(onTapCancel).toHaveBeenCalledWith(
+ expect.objectContaining({
+ altKey: false,
+ ctrlKey: false,
+ height: 1,
+ metaKey: false,
+ pageX: 0,
+ pageY: 0,
+ pointerType,
+ pressure: 0,
+ screenX: 0,
+ screenY: 0,
+ shiftKey: false,
+ tangentialPressure: 0,
+ target: target.node,
+ tiltX: 0,
+ tiltY: 0,
+ timeStamp: expect.any(Number),
+ twist: 0,
+ type: 'tap:cancel',
+ width: 1,
+ x: 0,
+ y: 0,
+ }),
+ );
+ target.pointermove({pointerType, x: 5, y: 5});
+ expect(onTapUpdate).not.toBeCalled();
+ });
+ }
+ // @gate experimental
test('second pointer on target', () => {
+ componentInit();
const pointerType = 'touch';
const target = createEventTarget(ref.current);
const button = buttonType.primary;
@@ -780,7 +841,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
});
if (hasPointerEvents) {
+ // @gate experimental
test('second pointer off target', () => {
+ componentInit();
const pointerType = 'touch';
const target = createEventTarget(ref.current);
const offTarget = createEventTarget(container);
@@ -792,19 +855,26 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
});
}
- testWithPointerType('pointer move outside target', pointerType => {
- const downTarget = createEventTarget(ref.current);
- const upTarget = createEventTarget(container);
- tapAndMoveOutside({
- hasPointerEvents,
- downTarget,
- upTarget,
- pointerType,
+ // TODO: Get rid of this condition somehow. Perhaps with a dynamic verion of
+ // the @gate pragma.
+ if (__EXPERIMENTAL__) {
+ testWithPointerType('pointer move outside target', pointerType => {
+ componentInit();
+ const downTarget = createEventTarget(ref.current);
+ const upTarget = createEventTarget(container);
+ tapAndMoveOutside({
+ hasPointerEvents,
+ downTarget,
+ upTarget,
+ pointerType,
+ });
+ expect(onTapCancel).toBeCalled();
});
- expect(onTapCancel).toBeCalled();
- });
+ }
+ // @gate experimental
test('ignored modifiers', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const button = buttonType.primary;
const buttons = buttonsType.primary;
@@ -824,13 +894,17 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
expect(onTapCancel).toHaveBeenCalledTimes(4);
});
+ // @gate experimental
test('long press context menu', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.contextmenu({}, {pointerType: 'touch'});
expect(onTapCancel).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
test('parent scroll (non-mouse)', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const parentTarget = createEventTarget(parentRef.current);
target.pointerdown({pointerType: 'touch'});
@@ -838,7 +912,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
expect(onTapCancel).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
test('sibling scroll', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const siblingTarget = createEventTarget(siblingRef.current);
target.pointerdown();
@@ -846,7 +922,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
expect(onTapCancel).not.toBeCalled();
});
+ // @gate experimental
test('document scroll (non-mouse)', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const documentTarget = createEventTarget(document);
target.pointerdown({pointerType: 'touch'});
@@ -855,7 +933,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
});
// Scroll on an element not managed by React
+ // @gate experimental
test('root container scroll (non-mouse)', () => {
+ componentInit();
const target = createEventTarget(ref.current);
const containerTarget = createEventTarget(container);
target.pointerdown({pointerType: 'touch'});
@@ -867,7 +947,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
describe('preventDefault', () => {
let onTapEnd, ref, innerRef, preventDefault, remount;
- beforeEach(() => {
+ const componentInit = () => {
remount = function(shouldPreventDefault) {
onTapEnd = jest.fn();
preventDefault = jest.fn();
@@ -887,9 +967,11 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
ReactDOM.render( , container);
};
remount();
- });
+ };
+ // @gate experimental
test('prevents native behavior by default', () => {
+ componentInit();
const target = createEventTarget(ref.current);
target.pointerdown();
target.pointerup({preventDefault});
@@ -899,7 +981,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
);
});
+ // @gate experimental
test('prevents native behaviour by default (inner target)', () => {
+ componentInit();
const innerTarget = createEventTarget(innerRef.current);
innerTarget.pointerdown();
innerTarget.pointerup({preventDefault});
@@ -909,7 +993,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
);
});
+ // @gate experimental
test('allows native behaviour if false', () => {
+ componentInit();
remount(false);
const target = createEventTarget(ref.current);
diff --git a/packages/react-noop-renderer/src/ReactNoopPersistent.js b/packages/react-noop-renderer/src/ReactNoopPersistent.js
index 3fc7f64445..dd9aa34d7d 100644
--- a/packages/react-noop-renderer/src/ReactNoopPersistent.js
+++ b/packages/react-noop-renderer/src/ReactNoopPersistent.js
@@ -24,6 +24,7 @@ export const {
getOrCreateRootContainer,
createRoot,
createBlockingRoot,
+ createLegacyRoot,
getChildrenAsJSX,
getPendingChildrenAsJSX,
createPortal,
diff --git a/packages/react-reconciler/src/__tests__/ReactFiberFundamental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberFundamental-test.internal.js
index 081b0be2d3..f465226d20 100644
--- a/packages/react-reconciler/src/__tests__/ReactFiberFundamental-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactFiberFundamental-test.internal.js
@@ -50,16 +50,12 @@ function initReactDOMServer() {
}
describe('ReactFiberFundamental', () => {
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
describe('NoopRenderer', () => {
beforeEach(() => {
initNoopRenderer();
});
+ // @gate experimental
it('should render a simple fundamental component with a single child', () => {
const FundamentalComponent = createReactFundamentalComponent({
reconcileChildren: true,
@@ -94,6 +90,7 @@ describe('ReactFiberFundamental', () => {
initTestRenderer();
});
+ // @gate experimental
it('should render a simple fundamental component with a single child', () => {
const FundamentalComponent = createReactFundamentalComponent({
reconcileChildren: true,
@@ -130,6 +127,7 @@ describe('ReactFiberFundamental', () => {
initReactDOM();
});
+ // @gate experimental
it('should render a simple fundamental component with a single child', () => {
const FundamentalComponent = createReactFundamentalComponent({
reconcileChildren: true,
@@ -155,6 +153,7 @@ describe('ReactFiberFundamental', () => {
expect(container.innerHTML).toBe('');
});
+ // @gate experimental
it('should render a simple fundamental component without reconcileChildren', () => {
const FundamentalComponent = createReactFundamentalComponent({
reconcileChildren: false,
@@ -186,6 +185,7 @@ describe('ReactFiberFundamental', () => {
initReactDOMServer();
});
+ // @gate experimental
it('should render a simple fundamental component with a single child', () => {
const getInstance = jest.fn();
const FundamentalComponent = createReactFundamentalComponent({
@@ -210,6 +210,7 @@ describe('ReactFiberFundamental', () => {
expect(output).toBe('Hello world again
');
});
+ // @gate experimental
it('should render a simple fundamental component without reconcileChildren', () => {
const FundamentalComponent = createReactFundamentalComponent({
reconcileChildren: false,
diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
index dfe89a510b..84ddb0c0dc 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
@@ -1121,384 +1121,380 @@ describe('ReactHooksWithNoopRenderer', () => {
},
);
- if (
- require('shared/ReactFeatureFlags')
- .deferPassiveEffectCleanupDuringUnmount &&
- require('shared/ReactFeatureFlags')
- .runAllPassiveEffectDestroysBeforeCreates
- ) {
- it('defers passive effect destroy functions during unmount', () => {
- function Child({bar, foo}) {
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('passive bar create');
- return () => {
- Scheduler.unstable_yieldValue('passive bar destroy');
- };
- }, [bar]);
- React.useLayoutEffect(() => {
- Scheduler.unstable_yieldValue('layout bar create');
- return () => {
- Scheduler.unstable_yieldValue('layout bar destroy');
- };
- }, [bar]);
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('passive foo create');
- return () => {
- Scheduler.unstable_yieldValue('passive foo destroy');
- };
- }, [foo]);
- React.useLayoutEffect(() => {
- Scheduler.unstable_yieldValue('layout foo create');
- return () => {
- Scheduler.unstable_yieldValue('layout foo destroy');
- };
- }, [foo]);
- Scheduler.unstable_yieldValue('render');
- return null;
- }
+ // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates
+ it('defers passive effect destroy functions during unmount', () => {
+ function Child({bar, foo}) {
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('passive bar create');
+ return () => {
+ Scheduler.unstable_yieldValue('passive bar destroy');
+ };
+ }, [bar]);
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('layout bar create');
+ return () => {
+ Scheduler.unstable_yieldValue('layout bar destroy');
+ };
+ }, [bar]);
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('passive foo create');
+ return () => {
+ Scheduler.unstable_yieldValue('passive foo destroy');
+ };
+ }, [foo]);
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('layout foo create');
+ return () => {
+ Scheduler.unstable_yieldValue('layout foo destroy');
+ };
+ }, [foo]);
+ Scheduler.unstable_yieldValue('render');
+ return null;
+ }
- act(() => {
- ReactNoop.render( , () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough([
- 'render',
- 'layout bar create',
- 'layout foo create',
- 'Sync effect',
- ]);
- // Effects are deferred until after the commit
- expect(Scheduler).toFlushAndYield([
- 'passive bar create',
- 'passive foo create',
- ]);
- });
-
- // This update is exists to test an internal implementation detail:
- // Effects without updating dependencies lose their layout/passive tag during an update.
- act(() => {
- ReactNoop.render( , () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough([
- 'render',
- 'layout foo destroy',
- 'layout foo create',
- 'Sync effect',
- ]);
- // Effects are deferred until after the commit
- expect(Scheduler).toFlushAndYield([
- 'passive foo destroy',
- 'passive foo create',
- ]);
- });
-
- // Unmount the component and verify that passive destroy functions are deferred until post-commit.
- act(() => {
- ReactNoop.render(null, () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough([
- 'layout bar destroy',
- 'layout foo destroy',
- 'Sync effect',
- ]);
- // Effects are deferred until after the commit
- expect(Scheduler).toFlushAndYield([
- 'passive bar destroy',
- 'passive foo destroy',
- ]);
- });
+ act(() => {
+ ReactNoop.render( , () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'render',
+ 'layout bar create',
+ 'layout foo create',
+ 'Sync effect',
+ ]);
+ // Effects are deferred until after the commit
+ expect(Scheduler).toFlushAndYield([
+ 'passive bar create',
+ 'passive foo create',
+ ]);
});
- it('does not warn about state updates for unmounted components with pending passive unmounts', () => {
- let completePendingRequest = null;
- function Component() {
- Scheduler.unstable_yieldValue('Component');
- const [didLoad, setDidLoad] = React.useState(false);
- React.useLayoutEffect(() => {
- Scheduler.unstable_yieldValue('layout create');
- return () => {
- Scheduler.unstable_yieldValue('layout destroy');
- };
- }, []);
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('passive create');
- // Mimic an XHR request with a complete handler that updates state.
- completePendingRequest = () => setDidLoad(true);
- return () => {
- Scheduler.unstable_yieldValue('passive destroy');
- };
- }, []);
- return didLoad;
- }
-
- act(() => {
- ReactNoop.renderToRootWithID( , 'root', () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough([
- 'Component',
- 'layout create',
- 'Sync effect',
- ]);
- ReactNoop.flushPassiveEffects();
- expect(Scheduler).toHaveYielded(['passive create']);
-
- // Unmount but don't process pending passive destroy function
- ReactNoop.unmountRootWithID('root');
- expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
-
- // Simulate an XHR completing, which will cause a state update-
- // but should not log a warning.
- completePendingRequest();
-
- ReactNoop.flushPassiveEffects();
- expect(Scheduler).toHaveYielded(['passive destroy']);
- });
+ // This update is exists to test an internal implementation detail:
+ // Effects without updating dependencies lose their layout/passive tag during an update.
+ act(() => {
+ ReactNoop.render( , () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'render',
+ 'layout foo destroy',
+ 'layout foo create',
+ 'Sync effect',
+ ]);
+ // Effects are deferred until after the commit
+ expect(Scheduler).toFlushAndYield([
+ 'passive foo destroy',
+ 'passive foo create',
+ ]);
});
- it('still warns about state updates for unmounted components with no pending passive unmounts', () => {
- let completePendingRequest = null;
- function Component() {
- Scheduler.unstable_yieldValue('Component');
- const [didLoad, setDidLoad] = React.useState(false);
- React.useLayoutEffect(() => {
- Scheduler.unstable_yieldValue('layout create');
- // Mimic an XHR request with a complete handler that updates state.
- completePendingRequest = () => setDidLoad(true);
- return () => {
- Scheduler.unstable_yieldValue('layout destroy');
- };
- }, []);
- return didLoad;
- }
-
- act(() => {
- ReactNoop.renderToRootWithID( , 'root', () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough([
- 'Component',
- 'layout create',
- 'Sync effect',
- ]);
-
- // Unmount but don't process pending passive destroy function
- ReactNoop.unmountRootWithID('root');
- expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
-
- // Simulate an XHR completing.
- expect(completePendingRequest).toErrorDev(
- "Warning: Can't perform a React state update on an unmounted component.",
- );
- });
+ // Unmount the component and verify that passive destroy functions are deferred until post-commit.
+ act(() => {
+ ReactNoop.render(null, () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'layout bar destroy',
+ 'layout foo destroy',
+ 'Sync effect',
+ ]);
+ // Effects are deferred until after the commit
+ expect(Scheduler).toFlushAndYield([
+ 'passive bar destroy',
+ 'passive foo destroy',
+ ]);
});
+ });
- it('still warns if there are pending passive unmount effects but not for the current fiber', () => {
- let completePendingRequest = null;
- function ComponentWithXHR() {
- Scheduler.unstable_yieldValue('Component');
- const [didLoad, setDidLoad] = React.useState(false);
- React.useLayoutEffect(() => {
- Scheduler.unstable_yieldValue('a:layout create');
- return () => {
- Scheduler.unstable_yieldValue('a:layout destroy');
- };
- }, []);
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('a:passive create');
- // Mimic an XHR request with a complete handler that updates state.
- completePendingRequest = () => setDidLoad(true);
- }, []);
- return didLoad;
- }
+ // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates
+ it('does not warn about state updates for unmounted components with pending passive unmounts', () => {
+ let completePendingRequest = null;
+ function Component() {
+ Scheduler.unstable_yieldValue('Component');
+ const [didLoad, setDidLoad] = React.useState(false);
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('layout create');
+ return () => {
+ Scheduler.unstable_yieldValue('layout destroy');
+ };
+ }, []);
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('passive create');
+ // Mimic an XHR request with a complete handler that updates state.
+ completePendingRequest = () => setDidLoad(true);
+ return () => {
+ Scheduler.unstable_yieldValue('passive destroy');
+ };
+ }, []);
+ return didLoad;
+ }
- function ComponentWithPendingPassiveUnmount() {
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('b:passive create');
- return () => {
- Scheduler.unstable_yieldValue('b:passive destroy');
- };
- }, []);
- return null;
- }
+ act(() => {
+ ReactNoop.renderToRootWithID( , 'root', () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'Component',
+ 'layout create',
+ 'Sync effect',
+ ]);
+ ReactNoop.flushPassiveEffects();
+ expect(Scheduler).toHaveYielded(['passive create']);
- act(() => {
- ReactNoop.renderToRootWithID(
- <>
-
-
- >,
- 'root',
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough([
- 'Component',
- 'a:layout create',
- 'Sync effect',
- ]);
- ReactNoop.flushPassiveEffects();
- expect(Scheduler).toHaveYielded([
- 'a:passive create',
- 'b:passive create',
- ]);
+ // Unmount but don't process pending passive destroy function
+ ReactNoop.unmountRootWithID('root');
+ expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
- // Unmount but don't process pending passive destroy function
- ReactNoop.unmountRootWithID('root');
- expect(Scheduler).toFlushAndYieldThrough(['a:layout destroy']);
+ // Simulate an XHR completing, which will cause a state update-
+ // but should not log a warning.
+ completePendingRequest();
- // Simulate an XHR completing in the component without a pending passive effect..
- expect(completePendingRequest).toErrorDev(
- "Warning: Can't perform a React state update on an unmounted component.",
- );
- });
+ ReactNoop.flushPassiveEffects();
+ expect(Scheduler).toHaveYielded(['passive destroy']);
});
+ });
- it('still warns if there are updates after pending passive unmount effects have been flushed', () => {
- let updaterFunction;
+ it('warns about state updates for unmounted components with no pending passive unmounts', () => {
+ let completePendingRequest = null;
+ function Component() {
+ Scheduler.unstable_yieldValue('Component');
+ const [didLoad, setDidLoad] = React.useState(false);
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('layout create');
+ // Mimic an XHR request with a complete handler that updates state.
+ completePendingRequest = () => setDidLoad(true);
+ return () => {
+ Scheduler.unstable_yieldValue('layout destroy');
+ };
+ }, []);
+ return didLoad;
+ }
- function Component() {
- Scheduler.unstable_yieldValue('Component');
- const [state, setState] = React.useState(false);
- updaterFunction = setState;
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('passive create');
- return () => {
- Scheduler.unstable_yieldValue('passive destroy');
- };
- }, []);
- return state;
- }
+ act(() => {
+ ReactNoop.renderToRootWithID( , 'root', () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'Component',
+ 'layout create',
+ 'Sync effect',
+ ]);
- act(() => {
- ReactNoop.renderToRootWithID( , 'root', () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- });
+ // Unmount but don't process pending passive destroy function
+ ReactNoop.unmountRootWithID('root');
+ expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
+
+ // Simulate an XHR completing.
+ expect(completePendingRequest).toErrorDev(
+ "Warning: Can't perform a React state update on an unmounted component.",
+ );
+ });
+ });
+
+ // @gate deferPassiveEffectCleanupDuringUnmount && runAllPassiveEffectDestroysBeforeCreates
+ it('still warns if there are pending passive unmount effects but not for the current fiber', () => {
+ let completePendingRequest = null;
+ function ComponentWithXHR() {
+ Scheduler.unstable_yieldValue('Component');
+ const [didLoad, setDidLoad] = React.useState(false);
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('a:layout create');
+ return () => {
+ Scheduler.unstable_yieldValue('a:layout destroy');
+ };
+ }, []);
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('a:passive create');
+ // Mimic an XHR request with a complete handler that updates state.
+ completePendingRequest = () => setDidLoad(true);
+ }, []);
+ return didLoad;
+ }
+
+ function ComponentWithPendingPassiveUnmount() {
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('b:passive create');
+ return () => {
+ Scheduler.unstable_yieldValue('b:passive destroy');
+ };
+ }, []);
+ return null;
+ }
+
+ act(() => {
+ ReactNoop.renderToRootWithID(
+ <>
+
+
+ >,
+ 'root',
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'Component',
+ 'a:layout create',
+ 'Sync effect',
+ ]);
+ ReactNoop.flushPassiveEffects();
expect(Scheduler).toHaveYielded([
+ 'a:passive create',
+ 'b:passive create',
+ ]);
+
+ // Unmount but don't process pending passive destroy function
+ ReactNoop.unmountRootWithID('root');
+ expect(Scheduler).toFlushAndYieldThrough(['a:layout destroy']);
+
+ // Simulate an XHR completing in the component without a pending passive effect..
+ expect(completePendingRequest).toErrorDev(
+ "Warning: Can't perform a React state update on an unmounted component.",
+ );
+ });
+ });
+
+ it('warns if there are updates after pending passive unmount effects have been flushed', () => {
+ let updaterFunction;
+
+ function Component() {
+ Scheduler.unstable_yieldValue('Component');
+ const [state, setState] = React.useState(false);
+ updaterFunction = setState;
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('passive create');
+ return () => {
+ Scheduler.unstable_yieldValue('passive destroy');
+ };
+ }, []);
+ return state;
+ }
+
+ act(() => {
+ ReactNoop.renderToRootWithID( , 'root', () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ });
+ expect(Scheduler).toHaveYielded([
+ 'Component',
+ 'Sync effect',
+ 'passive create',
+ ]);
+
+ ReactNoop.unmountRootWithID('root');
+ expect(Scheduler).toFlushAndYield(['passive destroy']);
+
+ act(() => {
+ expect(() => {
+ updaterFunction(true);
+ }).toErrorDev(
+ "Warning: Can't perform a React state update on an unmounted component. " +
+ 'This is a no-op, but it indicates a memory leak in your application. ' +
+ 'To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.\n' +
+ ' in Component (at **)',
+ );
+ });
+ });
+
+ it('does not show a warning when a component updates its own state from within passive unmount function', () => {
+ function Component() {
+ Scheduler.unstable_yieldValue('Component');
+ const [didLoad, setDidLoad] = React.useState(false);
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('passive create');
+ return () => {
+ setDidLoad(true);
+ Scheduler.unstable_yieldValue('passive destroy');
+ };
+ }, []);
+ return didLoad;
+ }
+
+ act(() => {
+ ReactNoop.renderToRootWithID( , 'root', () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough([
'Component',
'Sync effect',
'passive create',
]);
+ // Unmount but don't process pending passive destroy function
ReactNoop.unmountRootWithID('root');
expect(Scheduler).toFlushAndYield(['passive destroy']);
-
- act(() => {
- expect(() => {
- updaterFunction(true);
- }).toErrorDev(
- "Warning: Can't perform a React state update on an unmounted component. " +
- 'This is a no-op, but it indicates a memory leak in your application. ' +
- 'To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.\n' +
- ' in Component (at **)',
- );
- });
});
+ });
- it('does not show a warning when a component updates its own state from within passive unmount function', () => {
- function Component() {
- Scheduler.unstable_yieldValue('Component');
- const [didLoad, setDidLoad] = React.useState(false);
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('passive create');
- return () => {
- setDidLoad(true);
- Scheduler.unstable_yieldValue('passive destroy');
- };
- }, []);
- return didLoad;
- }
+ it('does not show a warning when a component updates a childs state from within passive unmount function', () => {
+ function Parent() {
+ Scheduler.unstable_yieldValue('Parent');
+ const updaterRef = React.useRef(null);
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('Parent passive create');
+ return () => {
+ updaterRef.current(true);
+ Scheduler.unstable_yieldValue('Parent passive destroy');
+ };
+ }, []);
+ return ;
+ }
- act(() => {
- ReactNoop.renderToRootWithID( , 'root', () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough([
- 'Component',
- 'Sync effect',
- 'passive create',
- ]);
+ function Child({updaterRef}) {
+ Scheduler.unstable_yieldValue('Child');
+ const [state, setState] = React.useState(false);
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('Child passive create');
+ updaterRef.current = setState;
+ }, []);
+ return state;
+ }
- // Unmount but don't process pending passive destroy function
- ReactNoop.unmountRootWithID('root');
- expect(Scheduler).toFlushAndYield(['passive destroy']);
- });
+ act(() => {
+ ReactNoop.renderToRootWithID( , 'root');
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'Parent',
+ 'Child',
+ 'Child passive create',
+ 'Parent passive create',
+ ]);
+
+ // Unmount but don't process pending passive destroy function
+ ReactNoop.unmountRootWithID('root');
+ expect(Scheduler).toFlushAndYield(['Parent passive destroy']);
});
+ });
- it('does not show a warning when a component updates a childs state from within passive unmount function', () => {
- function Parent() {
- Scheduler.unstable_yieldValue('Parent');
- const updaterRef = React.useRef(null);
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('Parent passive create');
- return () => {
- updaterRef.current(true);
- Scheduler.unstable_yieldValue('Parent passive destroy');
- };
- }, []);
- return ;
- }
+ it('does not show a warning when a component updates a parents state from within passive unmount function', () => {
+ function Parent() {
+ const [state, setState] = React.useState(false);
+ Scheduler.unstable_yieldValue('Parent');
+ return ;
+ }
- function Child({updaterRef}) {
- Scheduler.unstable_yieldValue('Child');
- const [state, setState] = React.useState(false);
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('Child passive create');
- updaterRef.current = setState;
- }, []);
- return state;
- }
+ function Child({setState, state}) {
+ Scheduler.unstable_yieldValue('Child');
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('Child passive create');
+ return () => {
+ Scheduler.unstable_yieldValue('Child passive destroy');
+ setState(true);
+ };
+ }, []);
+ return state;
+ }
- act(() => {
- ReactNoop.renderToRootWithID( , 'root');
- expect(Scheduler).toFlushAndYieldThrough([
- 'Parent',
- 'Child',
- 'Child passive create',
- 'Parent passive create',
- ]);
+ act(() => {
+ ReactNoop.renderToRootWithID( , 'root');
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'Parent',
+ 'Child',
+ 'Child passive create',
+ ]);
- // Unmount but don't process pending passive destroy function
- ReactNoop.unmountRootWithID('root');
- expect(Scheduler).toFlushAndYield(['Parent passive destroy']);
- });
+ // Unmount but don't process pending passive destroy function
+ ReactNoop.unmountRootWithID('root');
+ expect(Scheduler).toFlushAndYield(['Child passive destroy']);
});
-
- it('does not show a warning when a component updates a parents state from within passive unmount function', () => {
- function Parent() {
- const [state, setState] = React.useState(false);
- Scheduler.unstable_yieldValue('Parent');
- return ;
- }
-
- function Child({setState, state}) {
- Scheduler.unstable_yieldValue('Child');
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('Child passive create');
- return () => {
- Scheduler.unstable_yieldValue('Child passive destroy');
- setState(true);
- };
- }, []);
- return state;
- }
-
- act(() => {
- ReactNoop.renderToRootWithID( , 'root');
- expect(Scheduler).toFlushAndYieldThrough([
- 'Parent',
- 'Child',
- 'Child passive create',
- ]);
-
- // Unmount but don't process pending passive destroy function
- ReactNoop.unmountRootWithID('root');
- expect(Scheduler).toFlushAndYield(['Child passive destroy']);
- });
- });
- }
+ });
it('updates have async priority', () => {
function Counter(props) {
diff --git a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js
index 15edff4082..feed4a16f6 100644
--- a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js
@@ -26,11 +26,6 @@ describe('ReactScope', () => {
Scheduler = require('scheduler');
});
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
describe('ReactDOM', () => {
let ReactDOM;
let container;
@@ -47,6 +42,7 @@ describe('ReactScope', () => {
container = null;
});
+ // @gate experimental
it('DO_NOT_USE_queryAllNodes() works as intended', () => {
const testScopeQuery = (type, props) => true;
const TestScope = React.unstable_createScope();
@@ -81,6 +77,7 @@ describe('ReactScope', () => {
expect(scopeRef.current).toBe(null);
});
+ // @gate experimental
it('DO_NOT_USE_queryAllNodes() provides the correct host instance', () => {
const testScopeQuery = (type, props) => type === 'div';
const TestScope = React.unstable_createScope();
@@ -127,6 +124,7 @@ describe('ReactScope', () => {
expect(scopeRef.current).toBe(null);
});
+ // @gate experimental
it('DO_NOT_USE_queryFirstNode() works as intended', () => {
const testScopeQuery = (type, props) => true;
const TestScope = React.unstable_createScope();
@@ -161,6 +159,7 @@ describe('ReactScope', () => {
expect(scopeRef.current).toBe(null);
});
+ // @gate experimental
it('containsNode() works as intended', () => {
const TestScope = React.unstable_createScope();
const scopeRef = React.createRef();
@@ -210,6 +209,7 @@ describe('ReactScope', () => {
expect(scopeRef.current.containsNode(emRef.current)).toBe(false);
});
+ // @gate experimental
it('scopes support server-side rendering and hydration', () => {
const TestScope = React.unstable_createScope();
const scopeRef = React.createRef();
@@ -240,6 +240,7 @@ describe('ReactScope', () => {
expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]);
});
+ // @gate experimental
it('event responders can be attached to scopes', () => {
let onKeyDown = jest.fn();
const TestScope = React.unstable_createScope();
@@ -282,6 +283,7 @@ describe('ReactScope', () => {
expect(onKeyDown).toHaveBeenCalledTimes(1);
});
+ // @gate experimental
it('getChildContextValues() works as intended', () => {
const TestContext = React.createContext();
const TestScope = React.unstable_createScope();
@@ -310,6 +312,7 @@ describe('ReactScope', () => {
expect(scopeRef.current).toBe(null);
});
+ // @gate experimental
it('correctly works with suspended boundaries that are hydrated', async () => {
let suspend = false;
let resolve;
@@ -384,6 +387,7 @@ describe('ReactScope', () => {
ReactTestRenderer = require('react-test-renderer');
});
+ // @gate experimental
it('DO_NOT_USE_queryAllNodes() works as intended', () => {
const testScopeQuery = (type, props) => true;
const TestScope = React.unstable_createScope();
@@ -420,6 +424,7 @@ describe('ReactScope', () => {
expect(nodes).toEqual([aRef.current, divRef.current, spanRef.current]);
});
+ // @gate experimental
it('DO_NOT_USE_queryFirstNode() works as intended', () => {
const testScopeQuery = (type, props) => true;
const TestScope = React.unstable_createScope();
@@ -456,6 +461,7 @@ describe('ReactScope', () => {
expect(node).toEqual(aRef.current);
});
+ // @gate experimental
it('containsNode() works as intended', () => {
const TestScope = React.unstable_createScope();
const scopeRef = React.createRef();
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js
index 3ed8f664bd..413f9a72e6 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js
@@ -6,11 +6,6 @@ let Suspense;
let SuspenseList;
describe('ReactSuspenseList', () => {
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
@@ -47,6 +42,7 @@ describe('ReactSuspenseList', () => {
return Component;
}
+ // @gate experimental
it('warns if an unsupported revealOrder option is used', () => {
function Foo() {
return (
@@ -66,6 +62,7 @@ describe('ReactSuspenseList', () => {
]);
});
+ // @gate experimental
it('warns if a upper case revealOrder option is used', () => {
function Foo() {
return (
@@ -85,6 +82,7 @@ describe('ReactSuspenseList', () => {
]);
});
+ // @gate experimental
it('warns if a misspelled revealOrder option is used', () => {
function Foo() {
return (
@@ -105,6 +103,7 @@ describe('ReactSuspenseList', () => {
]);
});
+ // @gate experimental
it('warns if a single element is passed to a "forwards" list', () => {
function Foo({children}) {
return {children} ;
@@ -137,6 +136,7 @@ describe('ReactSuspenseList', () => {
]);
});
+ // @gate experimental
it('warns if a single fragment is passed to a "backwards" list', () => {
function Foo() {
return (
@@ -157,6 +157,7 @@ describe('ReactSuspenseList', () => {
]);
});
+ // @gate experimental
it('warns if a nested array is passed to a "forwards" list', () => {
function Foo({items}) {
return (
@@ -184,6 +185,7 @@ describe('ReactSuspenseList', () => {
]);
});
+ // @gate experimental
it('shows content independently by default', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -250,6 +252,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('shows content independently in legacy mode regardless of option', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -322,6 +325,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('displays all "together"', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -391,6 +395,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('displays all "together" even when nested as siblings', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -476,6 +481,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('displays all "together" in nested SuspenseLists', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -537,6 +543,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('displays all "together" in nested SuspenseLists where the inner is default', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -596,6 +603,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('displays all "together" during an update', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -680,6 +688,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('avoided boundaries can be coordinate with SuspenseList', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -778,6 +787,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('displays each items in "forwards" order', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -843,6 +853,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('displays each items in "backwards" order', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -908,6 +919,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('displays added row at the top "together" and the bottom in "forwards" order', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -1062,6 +1074,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('displays added row at the top "together" and the bottom in "backwards" order', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -1246,6 +1259,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('switches to rendering fallbacks if the tail takes long CPU time', async () => {
function Foo() {
return (
@@ -1308,6 +1322,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('only shows one loading state at a time for "collapsed" tail insertions', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -1377,6 +1392,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('warns if an unsupported tail option is used', () => {
function Foo() {
return (
@@ -1397,6 +1413,7 @@ describe('ReactSuspenseList', () => {
]);
});
+ // @gate experimental
it('warns if a tail option is used with "together"', () => {
function Foo() {
return (
@@ -1417,6 +1434,7 @@ describe('ReactSuspenseList', () => {
]);
});
+ // @gate experimental
it('renders one "collapsed" fallback even if CPU time elapsed', async () => {
function Foo() {
return (
@@ -1483,6 +1501,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('adding to the middle does not collapse insertions (forwards)', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -1625,6 +1644,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('adding to the middle does not collapse insertions (backwards)', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -1772,6 +1792,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('adding to the middle of committed tail does not collapse insertions', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -1929,6 +1950,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('only shows no initial loading state "hidden" tail insertions', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
@@ -1992,6 +2014,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('eventually resolves a nested forwards suspense list', async () => {
const B = createAsyncText('B');
@@ -2054,6 +2077,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('eventually resolves a nested forwards suspense list with a hidden tail', async () => {
const B = createAsyncText('B');
@@ -2100,6 +2124,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('eventually resolves two nested forwards suspense lists with a hidden tail', async () => {
const B = createAsyncText('B');
@@ -2167,6 +2192,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('can do unrelated adjacent updates', async () => {
let updateAdjacent;
function Adjacent() {
@@ -2213,6 +2239,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('is able to re-suspend the last rows during an update with hidden', async () => {
const AsyncB = createAsyncText('B');
@@ -2301,6 +2328,7 @@ describe('ReactSuspenseList', () => {
expect(previousInst).toBe(setAsyncB);
});
+ // @gate experimental
it('is able to re-suspend the last rows during an update with hidden', async () => {
const AsyncB = createAsyncText('B');
@@ -2389,6 +2417,7 @@ describe('ReactSuspenseList', () => {
expect(previousInst).toBe(setAsyncB);
});
+ // @gate experimental
it('is able to interrupt a partially rendered tree and continue later', async () => {
const AsyncA = createAsyncText('A');
@@ -2486,6 +2515,7 @@ describe('ReactSuspenseList', () => {
);
});
+ // @gate experimental
it('can resume class components when revealed together', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
index 511777a9ea..b9520351c4 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
@@ -11,11 +11,6 @@ let resolveText;
let rejectText;
describe('ReactSuspenseWithNoopRenderer', () => {
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
beforeEach(() => {
jest.resetModules();
@@ -922,6 +917,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
});
+ // @gate experimental
it('starts working on an update even if its priority falls between two suspended levels', async () => {
function App(props) {
return (
@@ -2331,6 +2327,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
timeoutMs: 2000,
};
+ // @gate experimental
it('top level render', async () => {
function App({page}) {
return (
@@ -2385,6 +2382,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('B')]);
});
+ // @gate experimental
it('hooks', async () => {
let transitionToPage;
function App() {
@@ -2452,6 +2450,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('B')]);
});
+ // @gate experimental
it('classes', async () => {
let transitionToPage;
class App extends React.Component {
@@ -2523,6 +2522,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
});
+ // @gate experimental
it('disables suspense config when nothing is passed to withSuspenseConfig', async () => {
function App({page}) {
return (
@@ -2597,6 +2597,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
});
+ // @gate experimental
it('withSuspenseConfig timeout applies when we use an updated avoided boundary', async () => {
function App({page}) {
return (
@@ -2645,6 +2646,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
});
+ // @gate experimental
it('withSuspenseConfig timeout applies when we use a newly created avoided boundary', async () => {
function App({page}) {
return (
@@ -2692,6 +2694,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
});
+ // @gate experimental
it('supports delaying a busy spinner from disappearing', async () => {
const SUSPENSE_CONFIG = {
timeoutMs: 10000,
@@ -2854,6 +2857,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(root).toMatchRenderedOutput( );
});
+ // @gate experimental
it('should not render hidden content while suspended on higher pri', async () => {
function Offscreen() {
Scheduler.unstable_yieldValue('Offscreen');
@@ -2908,6 +2912,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
+ // @gate experimental
it('should be able to unblock higher pri content before suspended hidden', async () => {
function Offscreen() {
Scheduler.unstable_yieldValue('Offscreen');
@@ -3624,6 +3629,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
+ // @gate experimental
it('regression: ping at high priority causes update to be dropped', async () => {
const {useState, useTransition} = React;
diff --git a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
index fce1cd634e..698102b6b8 100644
--- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
@@ -140,1579 +140,1587 @@ describe('useMutableSource', () => {
return {`${label}:${snapshot}`}
;
}
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- } else {
- beforeEach(loadModules);
+ beforeEach(loadModules);
- it('should subscribe to a source and schedule updates when it changes', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
+ // @gate experimental
+ it('should subscribe to a source and schedule updates when it changes', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
- act(() => {
- ReactNoop.renderToRootWithID(
- <>
-
-
- >,
- 'root',
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough([
- 'a:one',
- 'b:one',
- 'Sync effect',
- ]);
-
- // Subscriptions should be passive
- expect(source.listenerCount).toBe(0);
- ReactNoop.flushPassiveEffects();
- expect(source.listenerCount).toBe(2);
-
- // Changing values should schedule an update with React
- source.value = 'two';
- expect(Scheduler).toFlushAndYieldThrough(['a:two', 'b:two']);
-
- // Umounting a component should remove its subscriptino.
- ReactNoop.renderToRootWithID(
- <>
-
- >,
- 'root',
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['a:two', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
- expect(source.listenerCount).toBe(1);
-
- // Umounting a root should remove the remaining event listeners
- ReactNoop.unmountRootWithID('root');
- expect(Scheduler).toFlushAndYield([]);
- ReactNoop.flushPassiveEffects();
- expect(source.listenerCount).toBe(0);
-
- // Changes to source should not trigger an updates or warnings.
- source.value = 'three';
- expect(Scheduler).toFlushAndYield([]);
- });
- });
-
- it('should restart work if a new source is mutated during render', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- act(() => {
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
-
- // Do enough work to read from one component
- expect(Scheduler).toFlushAndYieldThrough(['a:one']);
-
- // Mutate source before continuing work
- source.value = 'two';
-
- // Render work should restart and the updated value should be used
- expect(Scheduler).toFlushAndYield(['a:two', 'b:two', 'Sync effect']);
- });
- });
-
- it('should schedule an update if a new source is mutated between render and commit (subscription)', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- act(() => {
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
-
- // Finish rendering
- expect(Scheduler).toFlushAndYieldThrough([
- 'a:one',
- 'b:one',
- 'Sync effect',
- ]);
-
- // Mutate source before subscriptions are attached
- expect(source.listenerCount).toBe(0);
- source.value = 'two';
-
- // Mutation should be detected, and a new render should be scheduled
- expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
- });
- });
-
- it('should unsubscribe and resubscribe if a new source is used', () => {
- const sourceA = createSource('a-one');
- const mutableSourceA = createMutableSource(sourceA);
-
- const sourceB = createSource('b-one');
- const mutableSourceB = createMutableSource(sourceB);
-
- act(() => {
- ReactNoop.render(
+ act(() => {
+ ReactNoop.renderToRootWithID(
+ <>
,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
- expect(sourceA.listenerCount).toBe(1);
-
- // Changing values should schedule an update with React
- sourceA.value = 'a-two';
- expect(Scheduler).toFlushAndYield(['only:a-two']);
-
- // If we re-render with a new source, the old one should be unsubscribed.
- ReactNoop.render(
- ,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['only:b-one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
- expect(sourceA.listenerCount).toBe(0);
- expect(sourceB.listenerCount).toBe(1);
-
- // Changing to original source should not schedule updates with React
- sourceA.value = 'a-three';
- expect(Scheduler).toFlushAndYield([]);
-
- // Changing new source value should schedule an update with React
- sourceB.value = 'b-two';
- expect(Scheduler).toFlushAndYield(['only:b-two']);
- });
- });
-
- it('should unsubscribe and resubscribe if a new subscribe function is provided', () => {
- const source = createSource('a-one');
- const mutableSource = createMutableSource(source);
-
- const unsubscribeA = jest.fn();
- const subscribeA = jest.fn(s => {
- const unsubscribe = defaultSubscribe(s);
- return () => {
- unsubscribe();
- unsubscribeA();
- };
- });
- const unsubscribeB = jest.fn();
- const subscribeB = jest.fn(s => {
- const unsubscribe = defaultSubscribe(s);
- return () => {
- unsubscribe();
- unsubscribeB();
- };
- });
-
- act(() => {
- ReactNoop.renderToRootWithID(
- ,
- 'root',
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
- expect(source.listenerCount).toBe(1);
- expect(subscribeA).toHaveBeenCalledTimes(1);
-
- // If we re-render with a new subscription function,
- // the old unsubscribe function should be called.
- ReactNoop.renderToRootWithID(
- ,
- 'root',
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
- expect(source.listenerCount).toBe(1);
- expect(unsubscribeA).toHaveBeenCalledTimes(1);
- expect(subscribeB).toHaveBeenCalledTimes(1);
-
- // Unmounting should call the newer unsunscribe.
- ReactNoop.unmountRootWithID('root');
- expect(Scheduler).toFlushAndYield([]);
- ReactNoop.flushPassiveEffects();
- expect(source.listenerCount).toBe(0);
- expect(unsubscribeB).toHaveBeenCalledTimes(1);
- });
- });
-
- it('should re-use previously read snapshot value when reading is unsafe', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- act(() => {
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
-
- // Changing values should schedule an update with React.
- // Start working on this update but don't finish it.
- source.value = 'two';
- expect(Scheduler).toFlushAndYieldThrough(['a:two']);
-
- // Re-renders that occur before the udpate is processed
- // should reuse snapshot so long as the config has not changed
- ReactNoop.flushSync(() => {
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- });
- expect(Scheduler).toHaveYielded(['a:one', 'b:one', 'Sync effect']);
-
- expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
- });
- });
-
- it('should read from source on newly mounted subtree if no pending updates are scheduled for source', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- act(() => {
- ReactNoop.render(
- <>
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['a:one', 'Sync effect']);
-
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
- });
- });
-
- it('should throw and restart render if source and snapshot are unavailable during an update', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- act(() => {
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
-
- // Changing values should schedule an update with React.
- // Start working on this update but don't finish it.
- Scheduler.unstable_runWithPriority(
- Scheduler.unstable_LowPriority,
- () => {
- source.value = 'two';
- expect(Scheduler).toFlushAndYieldThrough(['a:two']);
- },
- );
-
- const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
-
- // Force a higher priority render with a new config.
- // This should signal that the snapshot is not safe and trigger a full re-render.
- Scheduler.unstable_runWithPriority(
- Scheduler.unstable_UserBlockingPriority,
- () => {
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- },
- );
- expect(Scheduler).toFlushAndYieldThrough([
- 'a:new:two',
- 'b:new:two',
- 'Sync effect',
- ]);
- });
- });
-
- it('should throw and restart render if source and snapshot are unavailable during a sync update', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- act(() => {
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
-
- // Changing values should schedule an update with React.
- // Start working on this update but don't finish it.
- Scheduler.unstable_runWithPriority(
- Scheduler.unstable_LowPriority,
- () => {
- source.value = 'two';
- expect(Scheduler).toFlushAndYieldThrough(['a:two']);
- },
- );
-
- const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
-
- // Force a higher priority render with a new config.
- // This should signal that the snapshot is not safe and trigger a full re-render.
- ReactNoop.flushSync(() => {
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- });
- expect(Scheduler).toHaveYielded([
- 'a:new:two',
- 'b:new:two',
- 'Sync effect',
- ]);
- });
- });
-
- it('should only update components whose subscriptions fire', () => {
- const source = createComplexSource('a:one', 'b:one');
- const mutableSource = createMutableSource(source);
-
- // Subscribe to part of the store.
- const getSnapshotA = s => s.valueA;
- const subscribeA = (s, callback) => s.subscribeA(callback);
- const getSnapshotB = s => s.valueB;
- const subscribeB = (s, callback) => s.subscribeB(callback);
-
- act(() => {
- ReactNoop.render(
- <>
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield([
- 'a:a:one',
- 'b:b:one',
- 'Sync effect',
- ]);
-
- // Changes to part of the store (e.g. A) should not render other parts.
- source.valueA = 'a:two';
- expect(Scheduler).toFlushAndYield(['a:a:two']);
- source.valueB = 'b:two';
- expect(Scheduler).toFlushAndYield(['b:b:two']);
- });
- });
-
- it('should detect tearing in part of the store not yet subscribed to', () => {
- const source = createComplexSource('a:one', 'b:one');
- const mutableSource = createMutableSource(source);
-
- // Subscribe to part of the store.
- const getSnapshotA = s => s.valueA;
- const subscribeA = (s, callback) => s.subscribeA(callback);
- const getSnapshotB = s => s.valueB;
- const subscribeB = (s, callback) => s.subscribeB(callback);
-
- act(() => {
- ReactNoop.render(
- <>
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['a:a:one', 'Sync effect']);
-
- // Because the store has not chagned yet, there are no pending updates,
- // so it is considered safe to read from when we start this render.
- ReactNoop.render(
- <>
-
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough(['a:a:one', 'b:b:one']);
-
- // Mutating the source should trigger a tear detection on the next read,
- // which should throw and re-render the entire tree.
- source.valueB = 'b:two';
-
- expect(Scheduler).toFlushAndYield([
- 'a:a:one',
- 'b:b:two',
- 'c:b:two',
- 'Sync effect',
- ]);
- });
- });
-
- it('does not schedule an update for subscriptions that fire with an unchanged snapshot', () => {
- const MockComponent = jest.fn(Component);
-
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- act(() => {
- ReactNoop.render(
- ,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough(['only:one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
- expect(source.listenerCount).toBe(1);
-
- // Notify subscribe function but don't change the value
- source.value = 'one';
- expect(Scheduler).toFlushWithoutYielding();
- });
- });
-
- it('should throw and restart if getSnapshot changes between scheduled update and re-render', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
-
- let updateGetSnapshot;
-
- function WrapperWithState() {
- const tuple = React.useState(() => defaultGetSnapshot);
- updateGetSnapshot = tuple[1];
- return (
-
- );
- }
-
- act(() => {
- ReactNoop.render( , () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
-
- // Change the source (and schedule an update).
- Scheduler.unstable_runWithPriority(
- Scheduler.unstable_LowPriority,
- () => {
- source.value = 'two';
- },
- );
-
- // Schedule a higher priority update that changes getSnapshot.
- Scheduler.unstable_runWithPriority(
- Scheduler.unstable_UserBlockingPriority,
- () => {
- updateGetSnapshot(() => newGetSnapshot);
- },
- );
-
- expect(Scheduler).toFlushAndYield(['only:new:two']);
- });
- });
-
- it('should recover from a mutation during yield when other work is scheduled', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- act(() => {
- // Start a render that uses the mutable source.
- ReactNoop.render(
- <>
-
-
- >,
- );
- expect(Scheduler).toFlushAndYieldThrough(['a:one']);
-
- // Mutate source
- source.value = 'two';
-
- // Now render something different.
- ReactNoop.render(
);
- expect(Scheduler).toFlushAndYield([]);
- });
- });
-
- it('should not throw if the new getSnapshot returns the same snapshot value', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- const onRenderA = jest.fn();
- const onRenderB = jest.fn();
-
- let updateGetSnapshot;
-
- function WrapperWithState() {
- const tuple = React.useState(() => defaultGetSnapshot);
- updateGetSnapshot = tuple[1];
- return (
- );
- }
-
- act(() => {
- ReactNoop.render(
- <>
-
-
-
-
-
-
- >,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
- expect(onRenderA).toHaveBeenCalledTimes(1);
- expect(onRenderB).toHaveBeenCalledTimes(1);
-
- // If B's getSnapshot function updates, but the snapshot it returns is the same,
- // only B should re-render (to update its state).
- updateGetSnapshot(() => s => defaultGetSnapshot(s));
- expect(Scheduler).toFlushAndYield(['b:one']);
- ReactNoop.flushPassiveEffects();
- expect(onRenderA).toHaveBeenCalledTimes(1);
- expect(onRenderB).toHaveBeenCalledTimes(2);
- });
- });
-
- it('should not throw if getSnapshot changes but the source can be safely read from anyway', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
-
- let updateGetSnapshot;
-
- function WrapperWithState() {
- const tuple = React.useState(() => defaultGetSnapshot);
- updateGetSnapshot = tuple[1];
- return (
-
- );
- }
-
- act(() => {
- ReactNoop.render( , () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
-
- // Change the source (and schedule an update)
- // but also change the snapshot function too.
- ReactNoop.batchedUpdates(() => {
- source.value = 'two';
- updateGetSnapshot(() => newGetSnapshot);
- });
-
- expect(Scheduler).toFlushAndYield(['only:new:two']);
- });
- });
-
- it('should still schedule an update if an eager selector throws after a mutation', () => {
- const source = createSource({
- friends: [
- {id: 1, name: 'Foo'},
- {id: 2, name: 'Bar'},
- ],
- });
- const mutableSource = createMutableSource(source);
-
- function FriendsList() {
- const getSnapshot = React.useCallback(
- ({value}) => Array.from(value.friends),
- [],
- );
- const friends = useMutableSource(
- mutableSource,
- getSnapshot,
- defaultSubscribe,
- );
- return (
-
- {friends.map(friend => (
-
- ))}
-
- );
- }
-
- function Friend({id}) {
- const getSnapshot = React.useCallback(
- ({value}) => {
- // This selector is intentionally written in a way that will throw
- // if no matching friend exists in the store.
- return value.friends.find(friend => friend.id === id).name;
- },
- [id],
- );
- const name = useMutableSource(
- mutableSource,
- getSnapshot,
- defaultSubscribe,
- );
- Scheduler.unstable_yieldValue(`${id}:${name}`);
- return {name} ;
- }
-
- act(() => {
- ReactNoop.render( , () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['1:Foo', '2:Bar', 'Sync effect']);
-
- // This mutation will cause the "Bar" component to throw,
- // since its value will no longer be a part of the store.
- // Mutable source should still schedule an update though,
- // which should unmount "Bar" and mount "Baz".
- source.value = {
- friends: [
- {id: 1, name: 'Foo'},
- {id: 3, name: 'Baz'},
- ],
- };
- expect(Scheduler).toFlushAndYield(['1:Foo', '3:Baz']);
- });
- });
-
- it('should not warn about updates that fire between unmount and passive unsubcribe', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- function Wrapper() {
- React.useLayoutEffect(() => () => {
- Scheduler.unstable_yieldValue('layout unmount');
- });
- return (
-
+ >,
+ 'root',
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'a:one',
+ 'b:one',
+ 'Sync effect',
+ ]);
+
+ // Subscriptions should be passive
+ expect(source.listenerCount).toBe(0);
+ ReactNoop.flushPassiveEffects();
+ expect(source.listenerCount).toBe(2);
+
+ // Changing values should schedule an update with React
+ source.value = 'two';
+ expect(Scheduler).toFlushAndYieldThrough(['a:two', 'b:two']);
+
+ // Umounting a component should remove its subscriptino.
+ ReactNoop.renderToRootWithID(
+ <>
+
+ >,
+ 'root',
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['a:two', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+ expect(source.listenerCount).toBe(1);
+
+ // Umounting a root should remove the remaining event listeners
+ ReactNoop.unmountRootWithID('root');
+ expect(Scheduler).toFlushAndYield([]);
+ ReactNoop.flushPassiveEffects();
+ expect(source.listenerCount).toBe(0);
+
+ // Changes to source should not trigger an updates or warnings.
+ source.value = 'three';
+ expect(Scheduler).toFlushAndYield([]);
+ });
+ });
+
+ // @gate experimental
+ it('should restart work if a new source is mutated during render', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ act(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+
+ // Do enough work to read from one component
+ expect(Scheduler).toFlushAndYieldThrough(['a:one']);
+
+ // Mutate source before continuing work
+ source.value = 'two';
+
+ // Render work should restart and the updated value should be used
+ expect(Scheduler).toFlushAndYield(['a:two', 'b:two', 'Sync effect']);
+ });
+ });
+
+ // @gate experimental
+ it('should schedule an update if a new source is mutated between render and commit (subscription)', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ act(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+
+ // Finish rendering
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'a:one',
+ 'b:one',
+ 'Sync effect',
+ ]);
+
+ // Mutate source before subscriptions are attached
+ expect(source.listenerCount).toBe(0);
+ source.value = 'two';
+
+ // Mutation should be detected, and a new render should be scheduled
+ expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
+ });
+ });
+
+ // @gate experimental
+ it('should unsubscribe and resubscribe if a new source is used', () => {
+ const sourceA = createSource('a-one');
+ const mutableSourceA = createMutableSource(sourceA);
+
+ const sourceB = createSource('b-one');
+ const mutableSourceB = createMutableSource(sourceB);
+
+ act(() => {
+ ReactNoop.render(
+ ,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+ expect(sourceA.listenerCount).toBe(1);
+
+ // Changing values should schedule an update with React
+ sourceA.value = 'a-two';
+ expect(Scheduler).toFlushAndYield(['only:a-two']);
+
+ // If we re-render with a new source, the old one should be unsubscribed.
+ ReactNoop.render(
+ ,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['only:b-one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+ expect(sourceA.listenerCount).toBe(0);
+ expect(sourceB.listenerCount).toBe(1);
+
+ // Changing to original source should not schedule updates with React
+ sourceA.value = 'a-three';
+ expect(Scheduler).toFlushAndYield([]);
+
+ // Changing new source value should schedule an update with React
+ sourceB.value = 'b-two';
+ expect(Scheduler).toFlushAndYield(['only:b-two']);
+ });
+ });
+
+ // @gate experimental
+ it('should unsubscribe and resubscribe if a new subscribe function is provided', () => {
+ const source = createSource('a-one');
+ const mutableSource = createMutableSource(source);
+
+ const unsubscribeA = jest.fn();
+ const subscribeA = jest.fn(s => {
+ const unsubscribe = defaultSubscribe(s);
+ return () => {
+ unsubscribe();
+ unsubscribeA();
+ };
+ });
+ const unsubscribeB = jest.fn();
+ const subscribeB = jest.fn(s => {
+ const unsubscribe = defaultSubscribe(s);
+ return () => {
+ unsubscribe();
+ unsubscribeB();
+ };
+ });
+
+ act(() => {
+ ReactNoop.renderToRootWithID(
+ ,
+ 'root',
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+ expect(source.listenerCount).toBe(1);
+ expect(subscribeA).toHaveBeenCalledTimes(1);
+
+ // If we re-render with a new subscription function,
+ // the old unsubscribe function should be called.
+ ReactNoop.renderToRootWithID(
+ ,
+ 'root',
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+ expect(source.listenerCount).toBe(1);
+ expect(unsubscribeA).toHaveBeenCalledTimes(1);
+ expect(subscribeB).toHaveBeenCalledTimes(1);
+
+ // Unmounting should call the newer unsunscribe.
+ ReactNoop.unmountRootWithID('root');
+ expect(Scheduler).toFlushAndYield([]);
+ ReactNoop.flushPassiveEffects();
+ expect(source.listenerCount).toBe(0);
+ expect(unsubscribeB).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ // @gate experimental
+ it('should re-use previously read snapshot value when reading is unsafe', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ act(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
+
+ // Changing values should schedule an update with React.
+ // Start working on this update but don't finish it.
+ source.value = 'two';
+ expect(Scheduler).toFlushAndYieldThrough(['a:two']);
+
+ // Re-renders that occur before the udpate is processed
+ // should reuse snapshot so long as the config has not changed
+ ReactNoop.flushSync(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
);
- }
+ });
+ expect(Scheduler).toHaveYielded(['a:one', 'b:one', 'Sync effect']);
- act(() => {
- ReactNoop.renderToRootWithID( , 'root', () =>
- Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
- ReactNoop.flushPassiveEffects();
+ expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
+ });
+ });
- // Umounting a root should remove the remaining event listeners in a passive effect
- ReactNoop.unmountRootWithID('root');
- expect(Scheduler).toFlushAndYieldThrough(['layout unmount']);
+ // @gate experimental
+ it('should read from source on newly mounted subtree if no pending updates are scheduled for source', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
- // Changes to source should not cause a warning,
- // even though the unsubscribe hasn't run yet (since it's a pending passive effect).
+ act(() => {
+ ReactNoop.render(
+ <>
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['a:one', 'Sync effect']);
+
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
+ });
+ });
+
+ // @gate experimental
+ it('should throw and restart render if source and snapshot are unavailable during an update', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ act(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+
+ // Changing values should schedule an update with React.
+ // Start working on this update but don't finish it.
+ Scheduler.unstable_runWithPriority(Scheduler.unstable_LowPriority, () => {
source.value = 'two';
- expect(Scheduler).toFlushAndYield([]);
+ expect(Scheduler).toFlushAndYieldThrough(['a:two']);
});
- });
- it('should support inline selectors and updates that are processed after selector change', async () => {
- const source = createSource({
- a: 'initial',
- b: 'initial',
- });
- const mutableSource = createMutableSource(source);
+ const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
- const getSnapshotA = () => source.value.a;
- const getSnapshotB = () => source.value.b;
-
- function mutateB(newB) {
- source.value = {
- ...source.value,
- b: newB,
- };
- }
-
- function App({getSnapshot}) {
- const state = useMutableSource(
- mutableSource,
- getSnapshot,
- defaultSubscribe,
- );
- return state;
- }
-
- const root = ReactNoop.createRoot();
- await act(async () => {
- root.render( );
- });
- expect(root).toMatchRenderedOutput('initial');
-
- await act(async () => {
- mutateB('Updated B');
- root.render( );
- });
- expect(root).toMatchRenderedOutput('Updated B');
-
- await act(async () => {
- mutateB('Another update');
- });
- expect(root).toMatchRenderedOutput('Another update');
- });
-
- it('should clear the update queue when getSnapshot changes with pending lower priority updates', async () => {
- const source = createSource({
- a: 'initial',
- b: 'initial',
- });
- const mutableSource = createMutableSource(source);
-
- const getSnapshotA = () => source.value.a;
- const getSnapshotB = () => source.value.b;
-
- function mutateA(newA) {
- source.value = {
- ...source.value,
- a: newA,
- };
- }
-
- function mutateB(newB) {
- source.value = {
- ...source.value,
- b: newB,
- };
- }
-
- function App({toggle}) {
- const state = useMutableSource(
- mutableSource,
- toggle ? getSnapshotB : getSnapshotA,
- defaultSubscribe,
- );
- const result = (toggle ? 'B: ' : 'A: ') + state;
- return result;
- }
-
- const root = ReactNoop.createRoot();
- await act(async () => {
- root.render( );
- });
- expect(root).toMatchRenderedOutput('A: initial');
-
- await act(async () => {
- ReactNoop.discreteUpdates(() => {
- // Update both A and B to the same value
- mutateA('Update');
- mutateB('Update');
- // Toggle to B in the same batch
- root.render( );
- });
- // Mutate A at lower priority. This should never be rendered, because
- // by the time we get to the lower priority, we've already switched
- // to B.
- mutateA('OOPS! This mutation should be ignored');
- });
- expect(root).toMatchRenderedOutput('B: Update');
- });
-
- it('should clear the update queue when source changes with pending lower priority updates', async () => {
- const sourceA = createSource('initial');
- const sourceB = createSource('initial');
- const mutableSourceA = createMutableSource(sourceA);
- const mutableSourceB = createMutableSource(sourceB);
-
- function App({toggle}) {
- const state = useMutableSource(
- toggle ? mutableSourceB : mutableSourceA,
- defaultGetSnapshot,
- defaultSubscribe,
- );
- const result = (toggle ? 'B: ' : 'A: ') + state;
- return result;
- }
-
- const root = ReactNoop.createRoot();
- await act(async () => {
- root.render( );
- });
- expect(root).toMatchRenderedOutput('A: initial');
-
- await act(async () => {
- ReactNoop.discreteUpdates(() => {
- // Update both A and B to the same value
- sourceA.value = 'Update';
- sourceB.value = 'Update';
- // Toggle to B in the same batch
- root.render( );
- });
- // Mutate A at lower priority. This should never be rendered, because
- // by the time we get to the lower priority, we've already switched
- // to B.
- sourceA.value = 'OOPS! This mutation should be ignored';
- });
- expect(root).toMatchRenderedOutput('B: Update');
- });
-
- it('should always treat reading as potentially unsafe when getSnapshot changes between renders', async () => {
- const source = createSource({
- a: 'foo',
- b: 'bar',
- });
- const mutableSource = createMutableSource(source);
-
- const getSnapshotA = () => source.value.a;
- const getSnapshotB = () => source.value.b;
-
- function mutateA(newA) {
- source.value = {
- ...source.value,
- a: newA,
- };
- }
-
- function App({getSnapshotFirst, getSnapshotSecond}) {
- const first = useMutableSource(
- mutableSource,
- getSnapshotFirst,
- defaultSubscribe,
- );
- const second = useMutableSource(
- mutableSource,
- getSnapshotSecond,
- defaultSubscribe,
- );
-
- let result = `x: ${first}, y: ${second}`;
-
- if (getSnapshotFirst === getSnapshotSecond) {
- // When both getSnapshot functions are equal,
- // the two values must be consistent.
- if (first !== second) {
- result = 'Oops, tearing!';
- }
- }
-
- React.useEffect(() => {
- Scheduler.unstable_yieldValue(result);
- }, [result]);
-
- return result;
- }
-
- const root = ReactNoop.createRoot();
- await act(async () => {
- root.render(
- ,
- );
- });
- // x and y start out reading from different parts of the store.
- expect(Scheduler).toHaveYielded(['x: foo, y: bar']);
-
- await act(async () => {
- ReactNoop.discreteUpdates(() => {
- // At high priority, toggle y so that it reads from A instead of B.
- // Simultaneously, mutate A.
- mutateA('baz');
- root.render(
- ,
+ // Force a higher priority render with a new config.
+ // This should signal that the snapshot is not safe and trigger a full re-render.
+ Scheduler.unstable_runWithPriority(
+ Scheduler.unstable_UserBlockingPriority,
+ () => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
);
-
- // If this update were processed before the next mutation,
- // it would be expected to yield "baz" and "baz".
- });
-
- // At lower priority, mutate A again.
- // This happens to match the initial value of B.
- mutateA('bar');
-
- // When this update is processed,
- // it is expected to yield "bar" and "bar".
- });
-
- // Check that we didn't commit any inconsistent states.
- // The actual sequence of work will be:
- // 1. React renders the high-pri update, sees a new getSnapshot, detects the source has been further mutated, and throws
- // 2. React re-renders with all pending updates, including the second mutation, and renders "bar" and "bar".
- expect(Scheduler).toHaveYielded(['x: bar, y: bar']);
+ },
+ );
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'a:new:two',
+ 'b:new:two',
+ 'Sync effect',
+ ]);
});
+ });
- it('getSnapshot changes and then source is mutated in between paint and passive effect phase', async () => {
- const source = createSource({
- a: 'foo',
- b: 'bar',
+ // @gate experimental
+ it('should throw and restart render if source and snapshot are unavailable during a sync update', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ act(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+
+ // Changing values should schedule an update with React.
+ // Start working on this update but don't finish it.
+ Scheduler.unstable_runWithPriority(Scheduler.unstable_LowPriority, () => {
+ source.value = 'two';
+ expect(Scheduler).toFlushAndYieldThrough(['a:two']);
});
- const mutableSource = createMutableSource(source);
- function mutateB(newB) {
- source.value = {
- ...source.value,
- b: newB,
- };
- }
+ const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
- const getSnapshotA = () => source.value.a;
- const getSnapshotB = () => source.value.b;
-
- function App({getSnapshot}) {
- const value = useMutableSource(
- mutableSource,
- getSnapshot,
- defaultSubscribe,
+ // Force a higher priority render with a new config.
+ // This should signal that the snapshot is not safe and trigger a full re-render.
+ ReactNoop.flushSync(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
);
-
- Scheduler.unstable_yieldValue('Render: ' + value);
- React.useEffect(() => {
- Scheduler.unstable_yieldValue('Commit: ' + value);
- }, [value]);
-
- return value;
- }
-
- const root = ReactNoop.createRoot();
- await act(async () => {
- root.render( );
- });
- expect(Scheduler).toHaveYielded(['Render: foo', 'Commit: foo']);
-
- await act(async () => {
- // Switch getSnapshot to read from B instead
- root.render( );
- // Render and finish the tree, but yield right after paint, before
- // the passive effects have fired.
- expect(Scheduler).toFlushUntilNextPaint(['Render: bar']);
- // Then mutate B.
- mutateB('baz');
});
expect(Scheduler).toHaveYielded([
- // Fires the effect from the previous render
- 'Commit: bar',
- // During that effect, it should detect that the snapshot has changed
- // and re-render.
- 'Render: baz',
- 'Commit: baz',
+ 'a:new:two',
+ 'b:new:two',
+ 'Sync effect',
]);
- expect(root).toMatchRenderedOutput('baz');
});
+ });
- it('getSnapshot changes and then source is mutated in between paint and passive effect phase, case 2', async () => {
- const source = createSource({
- a: 'a0',
- b: 'b0',
+ // @gate experimental
+ it('should only update components whose subscriptions fire', () => {
+ const source = createComplexSource('a:one', 'b:one');
+ const mutableSource = createMutableSource(source);
+
+ // Subscribe to part of the store.
+ const getSnapshotA = s => s.valueA;
+ const subscribeA = (s, callback) => s.subscribeA(callback);
+ const getSnapshotB = s => s.valueB;
+ const subscribeB = (s, callback) => s.subscribeB(callback);
+
+ act(() => {
+ ReactNoop.render(
+ <>
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['a:a:one', 'b:b:one', 'Sync effect']);
+
+ // Changes to part of the store (e.g. A) should not render other parts.
+ source.valueA = 'a:two';
+ expect(Scheduler).toFlushAndYield(['a:a:two']);
+ source.valueB = 'b:two';
+ expect(Scheduler).toFlushAndYield(['b:b:two']);
+ });
+ });
+
+ // @gate experimental
+ it('should detect tearing in part of the store not yet subscribed to', () => {
+ const source = createComplexSource('a:one', 'b:one');
+ const mutableSource = createMutableSource(source);
+
+ // Subscribe to part of the store.
+ const getSnapshotA = s => s.valueA;
+ const subscribeA = (s, callback) => s.subscribeA(callback);
+ const getSnapshotB = s => s.valueB;
+ const subscribeB = (s, callback) => s.subscribeB(callback);
+
+ act(() => {
+ ReactNoop.render(
+ <>
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['a:a:one', 'Sync effect']);
+
+ // Because the store has not chagned yet, there are no pending updates,
+ // so it is considered safe to read from when we start this render.
+ ReactNoop.render(
+ <>
+
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough(['a:a:one', 'b:b:one']);
+
+ // Mutating the source should trigger a tear detection on the next read,
+ // which should throw and re-render the entire tree.
+ source.valueB = 'b:two';
+
+ expect(Scheduler).toFlushAndYield([
+ 'a:a:one',
+ 'b:b:two',
+ 'c:b:two',
+ 'Sync effect',
+ ]);
+ });
+ });
+
+ // @gate experimental
+ it('does not schedule an update for subscriptions that fire with an unchanged snapshot', () => {
+ const MockComponent = jest.fn(Component);
+
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ act(() => {
+ ReactNoop.render(
+ ,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough(['only:one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+ expect(source.listenerCount).toBe(1);
+
+ // Notify subscribe function but don't change the value
+ source.value = 'one';
+ expect(Scheduler).toFlushWithoutYielding();
+ });
+ });
+
+ // @gate experimental
+ it('should throw and restart if getSnapshot changes between scheduled update and re-render', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
+
+ let updateGetSnapshot;
+
+ function WrapperWithState() {
+ const tuple = React.useState(() => defaultGetSnapshot);
+ updateGetSnapshot = tuple[1];
+ return (
+
+ );
+ }
+
+ act(() => {
+ ReactNoop.render( , () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+
+ // Change the source (and schedule an update).
+ Scheduler.unstable_runWithPriority(Scheduler.unstable_LowPriority, () => {
+ source.value = 'two';
});
- const mutableSource = createMutableSource(source);
- const getSnapshotA = () => source.value.a;
- const getSnapshotB = () => source.value.b;
+ // Schedule a higher priority update that changes getSnapshot.
+ Scheduler.unstable_runWithPriority(
+ Scheduler.unstable_UserBlockingPriority,
+ () => {
+ updateGetSnapshot(() => newGetSnapshot);
+ },
+ );
- function mutateA(newA) {
- source.value = {
- ...source.value,
- a: newA,
- };
+ expect(Scheduler).toFlushAndYield(['only:new:two']);
+ });
+ });
+
+ // @gate experimental
+ it('should recover from a mutation during yield when other work is scheduled', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ act(() => {
+ // Start a render that uses the mutable source.
+ ReactNoop.render(
+ <>
+
+
+ >,
+ );
+ expect(Scheduler).toFlushAndYieldThrough(['a:one']);
+
+ // Mutate source
+ source.value = 'two';
+
+ // Now render something different.
+ ReactNoop.render(
);
+ expect(Scheduler).toFlushAndYield([]);
+ });
+ });
+
+ // @gate experimental
+ it('should not throw if the new getSnapshot returns the same snapshot value', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ const onRenderA = jest.fn();
+ const onRenderB = jest.fn();
+
+ let updateGetSnapshot;
+
+ function WrapperWithState() {
+ const tuple = React.useState(() => defaultGetSnapshot);
+ updateGetSnapshot = tuple[1];
+ return (
+
+ );
+ }
+
+ act(() => {
+ ReactNoop.render(
+ <>
+
+
+
+
+
+
+ >,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+ expect(onRenderA).toHaveBeenCalledTimes(1);
+ expect(onRenderB).toHaveBeenCalledTimes(1);
+
+ // If B's getSnapshot function updates, but the snapshot it returns is the same,
+ // only B should re-render (to update its state).
+ updateGetSnapshot(() => s => defaultGetSnapshot(s));
+ expect(Scheduler).toFlushAndYield(['b:one']);
+ ReactNoop.flushPassiveEffects();
+ expect(onRenderA).toHaveBeenCalledTimes(1);
+ expect(onRenderB).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ // @gate experimental
+ it('should not throw if getSnapshot changes but the source can be safely read from anyway', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
+
+ let updateGetSnapshot;
+
+ function WrapperWithState() {
+ const tuple = React.useState(() => defaultGetSnapshot);
+ updateGetSnapshot = tuple[1];
+ return (
+
+ );
+ }
+
+ act(() => {
+ ReactNoop.render( , () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+
+ // Change the source (and schedule an update)
+ // but also change the snapshot function too.
+ ReactNoop.batchedUpdates(() => {
+ source.value = 'two';
+ updateGetSnapshot(() => newGetSnapshot);
+ });
+
+ expect(Scheduler).toFlushAndYield(['only:new:two']);
+ });
+ });
+
+ // @gate experimental
+ it('should still schedule an update if an eager selector throws after a mutation', () => {
+ const source = createSource({
+ friends: [
+ {id: 1, name: 'Foo'},
+ {id: 2, name: 'Bar'},
+ ],
+ });
+ const mutableSource = createMutableSource(source);
+
+ function FriendsList() {
+ const getSnapshot = React.useCallback(
+ ({value}) => Array.from(value.friends),
+ [],
+ );
+ const friends = useMutableSource(
+ mutableSource,
+ getSnapshot,
+ defaultSubscribe,
+ );
+ return (
+
+ {friends.map(friend => (
+
+ ))}
+
+ );
+ }
+
+ function Friend({id}) {
+ const getSnapshot = React.useCallback(
+ ({value}) => {
+ // This selector is intentionally written in a way that will throw
+ // if no matching friend exists in the store.
+ return value.friends.find(friend => friend.id === id).name;
+ },
+ [id],
+ );
+ const name = useMutableSource(
+ mutableSource,
+ getSnapshot,
+ defaultSubscribe,
+ );
+ Scheduler.unstable_yieldValue(`${id}:${name}`);
+ return {name} ;
+ }
+
+ act(() => {
+ ReactNoop.render( , () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['1:Foo', '2:Bar', 'Sync effect']);
+
+ // This mutation will cause the "Bar" component to throw,
+ // since its value will no longer be a part of the store.
+ // Mutable source should still schedule an update though,
+ // which should unmount "Bar" and mount "Baz".
+ source.value = {
+ friends: [
+ {id: 1, name: 'Foo'},
+ {id: 3, name: 'Baz'},
+ ],
+ };
+ expect(Scheduler).toFlushAndYield(['1:Foo', '3:Baz']);
+ });
+ });
+
+ // @gate experimental
+ it('should not warn about updates that fire between unmount and passive unsubcribe', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ function Wrapper() {
+ React.useLayoutEffect(() => () => {
+ Scheduler.unstable_yieldValue('layout unmount');
+ });
+ return (
+
+ );
+ }
+
+ act(() => {
+ ReactNoop.renderToRootWithID( , 'root', () =>
+ Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']);
+ ReactNoop.flushPassiveEffects();
+
+ // Umounting a root should remove the remaining event listeners in a passive effect
+ ReactNoop.unmountRootWithID('root');
+ expect(Scheduler).toFlushAndYieldThrough(['layout unmount']);
+
+ // Changes to source should not cause a warning,
+ // even though the unsubscribe hasn't run yet (since it's a pending passive effect).
+ source.value = 'two';
+ expect(Scheduler).toFlushAndYield([]);
+ });
+ });
+
+ // @gate experimental
+ it('should support inline selectors and updates that are processed after selector change', async () => {
+ const source = createSource({
+ a: 'initial',
+ b: 'initial',
+ });
+ const mutableSource = createMutableSource(source);
+
+ const getSnapshotA = () => source.value.a;
+ const getSnapshotB = () => source.value.b;
+
+ function mutateB(newB) {
+ source.value = {
+ ...source.value,
+ b: newB,
+ };
+ }
+
+ function App({getSnapshot}) {
+ const state = useMutableSource(
+ mutableSource,
+ getSnapshot,
+ defaultSubscribe,
+ );
+ return state;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render( );
+ });
+ expect(root).toMatchRenderedOutput('initial');
+
+ await act(async () => {
+ mutateB('Updated B');
+ root.render( );
+ });
+ expect(root).toMatchRenderedOutput('Updated B');
+
+ await act(async () => {
+ mutateB('Another update');
+ });
+ expect(root).toMatchRenderedOutput('Another update');
+ });
+
+ // @gate experimental
+ it('should clear the update queue when getSnapshot changes with pending lower priority updates', async () => {
+ const source = createSource({
+ a: 'initial',
+ b: 'initial',
+ });
+ const mutableSource = createMutableSource(source);
+
+ const getSnapshotA = () => source.value.a;
+ const getSnapshotB = () => source.value.b;
+
+ function mutateA(newA) {
+ source.value = {
+ ...source.value,
+ a: newA,
+ };
+ }
+
+ function mutateB(newB) {
+ source.value = {
+ ...source.value,
+ b: newB,
+ };
+ }
+
+ function App({toggle}) {
+ const state = useMutableSource(
+ mutableSource,
+ toggle ? getSnapshotB : getSnapshotA,
+ defaultSubscribe,
+ );
+ const result = (toggle ? 'B: ' : 'A: ') + state;
+ return result;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render( );
+ });
+ expect(root).toMatchRenderedOutput('A: initial');
+
+ await act(async () => {
+ ReactNoop.discreteUpdates(() => {
+ // Update both A and B to the same value
+ mutateA('Update');
+ mutateB('Update');
+ // Toggle to B in the same batch
+ root.render( );
+ });
+ // Mutate A at lower priority. This should never be rendered, because
+ // by the time we get to the lower priority, we've already switched
+ // to B.
+ mutateA('OOPS! This mutation should be ignored');
+ });
+ expect(root).toMatchRenderedOutput('B: Update');
+ });
+
+ // @gate experimental
+ it('should clear the update queue when source changes with pending lower priority updates', async () => {
+ const sourceA = createSource('initial');
+ const sourceB = createSource('initial');
+ const mutableSourceA = createMutableSource(sourceA);
+ const mutableSourceB = createMutableSource(sourceB);
+
+ function App({toggle}) {
+ const state = useMutableSource(
+ toggle ? mutableSourceB : mutableSourceA,
+ defaultGetSnapshot,
+ defaultSubscribe,
+ );
+ const result = (toggle ? 'B: ' : 'A: ') + state;
+ return result;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render( );
+ });
+ expect(root).toMatchRenderedOutput('A: initial');
+
+ await act(async () => {
+ ReactNoop.discreteUpdates(() => {
+ // Update both A and B to the same value
+ sourceA.value = 'Update';
+ sourceB.value = 'Update';
+ // Toggle to B in the same batch
+ root.render( );
+ });
+ // Mutate A at lower priority. This should never be rendered, because
+ // by the time we get to the lower priority, we've already switched
+ // to B.
+ sourceA.value = 'OOPS! This mutation should be ignored';
+ });
+ expect(root).toMatchRenderedOutput('B: Update');
+ });
+
+ // @gate experimental
+ it('should always treat reading as potentially unsafe when getSnapshot changes between renders', async () => {
+ const source = createSource({
+ a: 'foo',
+ b: 'bar',
+ });
+ const mutableSource = createMutableSource(source);
+
+ const getSnapshotA = () => source.value.a;
+ const getSnapshotB = () => source.value.b;
+
+ function mutateA(newA) {
+ source.value = {
+ ...source.value,
+ a: newA,
+ };
+ }
+
+ function App({getSnapshotFirst, getSnapshotSecond}) {
+ const first = useMutableSource(
+ mutableSource,
+ getSnapshotFirst,
+ defaultSubscribe,
+ );
+ const second = useMutableSource(
+ mutableSource,
+ getSnapshotSecond,
+ defaultSubscribe,
+ );
+
+ let result = `x: ${first}, y: ${second}`;
+
+ if (getSnapshotFirst === getSnapshotSecond) {
+ // When both getSnapshot functions are equal,
+ // the two values must be consistent.
+ if (first !== second) {
+ result = 'Oops, tearing!';
+ }
}
- function App({getSnapshotFirst, getSnapshotSecond}) {
- const first = useMutableSource(
- mutableSource,
- getSnapshotFirst,
- defaultSubscribe,
- );
- const second = useMutableSource(
- mutableSource,
- getSnapshotSecond,
- defaultSubscribe,
- );
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue(result);
+ }, [result]);
- return `first: ${first}, second: ${second}`;
- }
+ return result;
+ }
- const root = ReactNoop.createRoot();
- await act(async () => {
- root.render(
- ,
- );
- });
- expect(root.getChildrenAsJSX()).toEqual('first: a0, second: b0');
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render(
+ ,
+ );
+ });
+ // x and y start out reading from different parts of the store.
+ expect(Scheduler).toHaveYielded(['x: foo, y: bar']);
- await act(async () => {
- // Switch the second getSnapshot to also read from A
+ await act(async () => {
+ ReactNoop.discreteUpdates(() => {
+ // At high priority, toggle y so that it reads from A instead of B.
+ // Simultaneously, mutate A.
+ mutateA('baz');
root.render(
,
);
- // Render and finish the tree, but yield right after paint, before
- // the passive effects have fired.
- expect(Scheduler).toFlushUntilNextPaint([]);
- // Now mutate A. Both hooks should update.
- // This is at high priority so that it doesn't get batched with default
- // priority updates that might fire during the passive effect
- ReactNoop.discreteUpdates(() => {
- mutateA('a1');
- });
- expect(Scheduler).toFlushUntilNextPaint([]);
-
- expect(root.getChildrenAsJSX()).toEqual('first: a1, second: a1');
+ // If this update were processed before the next mutation,
+ // it would be expected to yield "baz" and "baz".
});
+ // At lower priority, mutate A again.
+ // This happens to match the initial value of B.
+ mutateA('bar');
+
+ // When this update is processed,
+ // it is expected to yield "bar" and "bar".
+ });
+
+ // Check that we didn't commit any inconsistent states.
+ // The actual sequence of work will be:
+ // 1. React renders the high-pri update, sees a new getSnapshot, detects the source has been further mutated, and throws
+ // 2. React re-renders with all pending updates, including the second mutation, and renders "bar" and "bar".
+ expect(Scheduler).toHaveYielded(['x: bar, y: bar']);
+ });
+
+ // @gate experimental
+ it('getSnapshot changes and then source is mutated in between paint and passive effect phase', async () => {
+ const source = createSource({
+ a: 'foo',
+ b: 'bar',
+ });
+ const mutableSource = createMutableSource(source);
+
+ function mutateB(newB) {
+ source.value = {
+ ...source.value,
+ b: newB,
+ };
+ }
+
+ const getSnapshotA = () => source.value.a;
+ const getSnapshotB = () => source.value.b;
+
+ function App({getSnapshot}) {
+ const value = useMutableSource(
+ mutableSource,
+ getSnapshot,
+ defaultSubscribe,
+ );
+
+ Scheduler.unstable_yieldValue('Render: ' + value);
+ React.useEffect(() => {
+ Scheduler.unstable_yieldValue('Commit: ' + value);
+ }, [value]);
+
+ return value;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render( );
+ });
+ expect(Scheduler).toHaveYielded(['Render: foo', 'Commit: foo']);
+
+ await act(async () => {
+ // Switch getSnapshot to read from B instead
+ root.render( );
+ // Render and finish the tree, but yield right after paint, before
+ // the passive effects have fired.
+ expect(Scheduler).toFlushUntilNextPaint(['Render: bar']);
+ // Then mutate B.
+ mutateB('baz');
+ });
+ expect(Scheduler).toHaveYielded([
+ // Fires the effect from the previous render
+ 'Commit: bar',
+ // During that effect, it should detect that the snapshot has changed
+ // and re-render.
+ 'Render: baz',
+ 'Commit: baz',
+ ]);
+ expect(root).toMatchRenderedOutput('baz');
+ });
+
+ // @gate experimental
+ it('getSnapshot changes and then source is mutated in between paint and passive effect phase, case 2', async () => {
+ const source = createSource({
+ a: 'a0',
+ b: 'b0',
+ });
+ const mutableSource = createMutableSource(source);
+
+ const getSnapshotA = () => source.value.a;
+ const getSnapshotB = () => source.value.b;
+
+ function mutateA(newA) {
+ source.value = {
+ ...source.value,
+ a: newA,
+ };
+ }
+
+ function App({getSnapshotFirst, getSnapshotSecond}) {
+ const first = useMutableSource(
+ mutableSource,
+ getSnapshotFirst,
+ defaultSubscribe,
+ );
+ const second = useMutableSource(
+ mutableSource,
+ getSnapshotSecond,
+ defaultSubscribe,
+ );
+
+ return `first: ${first}, second: ${second}`;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render(
+ ,
+ );
+ });
+ expect(root.getChildrenAsJSX()).toEqual('first: a0, second: b0');
+
+ await act(async () => {
+ // Switch the second getSnapshot to also read from A
+ root.render(
+ ,
+ );
+ // Render and finish the tree, but yield right after paint, before
+ // the passive effects have fired.
+ expect(Scheduler).toFlushUntilNextPaint([]);
+
+ // Now mutate A. Both hooks should update.
+ // This is at high priority so that it doesn't get batched with default
+ // priority updates that might fire during the passive effect
+ ReactNoop.discreteUpdates(() => {
+ mutateA('a1');
+ });
+ expect(Scheduler).toFlushUntilNextPaint([]);
+
expect(root.getChildrenAsJSX()).toEqual('first: a1, second: a1');
});
- it('getSnapshot changes and then source is mutated during interleaved event', async () => {
- const {useEffect} = React;
+ expect(root.getChildrenAsJSX()).toEqual('first: a1, second: a1');
+ });
- const source = createComplexSource('1', '2');
- const mutableSource = createMutableSource(source);
+ // @gate experimental
+ it('getSnapshot changes and then source is mutated during interleaved event', async () => {
+ const {useEffect} = React;
- // Subscribe to part of the store.
- const getSnapshotA = s => s.valueA;
- const subscribeA = (s, callback) => s.subscribeA(callback);
- const configA = [getSnapshotA, subscribeA];
+ const source = createComplexSource('1', '2');
+ const mutableSource = createMutableSource(source);
- const getSnapshotB = s => s.valueB;
- const subscribeB = (s, callback) => s.subscribeB(callback);
- const configB = [getSnapshotB, subscribeB];
+ // Subscribe to part of the store.
+ const getSnapshotA = s => s.valueA;
+ const subscribeA = (s, callback) => s.subscribeA(callback);
+ const configA = [getSnapshotA, subscribeA];
- function App({parentConfig, childConfig}) {
- const [getSnapshot, subscribe] = parentConfig;
- const parentValue = useMutableSource(
- mutableSource,
- getSnapshot,
- subscribe,
- );
+ const getSnapshotB = s => s.valueB;
+ const subscribeB = (s, callback) => s.subscribeB(callback);
+ const configB = [getSnapshotB, subscribeB];
- Scheduler.unstable_yieldValue('Parent: ' + parentValue);
+ function App({parentConfig, childConfig}) {
+ const [getSnapshot, subscribe] = parentConfig;
+ const parentValue = useMutableSource(
+ mutableSource,
+ getSnapshot,
+ subscribe,
+ );
- return (
-
- );
- }
+ Scheduler.unstable_yieldValue('Parent: ' + parentValue);
- function Child({parentConfig, childConfig, parentValue}) {
- const [getSnapshot, subscribe] = childConfig;
- const childValue = useMutableSource(
- mutableSource,
- getSnapshot,
- subscribe,
- );
+ return (
+
+ );
+ }
- Scheduler.unstable_yieldValue('Child: ' + childValue);
+ function Child({parentConfig, childConfig, parentValue}) {
+ const [getSnapshot, subscribe] = childConfig;
+ const childValue = useMutableSource(
+ mutableSource,
+ getSnapshot,
+ subscribe,
+ );
- let result = `${parentValue}, ${childValue}`;
+ Scheduler.unstable_yieldValue('Child: ' + childValue);
- if (parentConfig === childConfig) {
- // When both components read using the same config, the two values
- // must be consistent.
- if (parentValue !== childValue) {
- result = 'Oops, tearing!';
- }
+ let result = `${parentValue}, ${childValue}`;
+
+ if (parentConfig === childConfig) {
+ // When both components read using the same config, the two values
+ // must be consistent.
+ if (parentValue !== childValue) {
+ result = 'Oops, tearing!';
}
-
- useEffect(() => {
- Scheduler.unstable_yieldValue('Commit: ' + result);
- }, [result]);
-
- return result;
}
- const root = ReactNoop.createRoot();
- await act(async () => {
- root.render( );
- });
- expect(Scheduler).toHaveYielded([
- 'Parent: 1',
- 'Child: 2',
- 'Commit: 1, 2',
- ]);
+ useEffect(() => {
+ Scheduler.unstable_yieldValue('Commit: ' + result);
+ }, [result]);
- await act(async () => {
- // Switch the parent and the child to read using the same config
- root.render( );
- // Start rendering the parent, but yield before rendering the child
- expect(Scheduler).toFlushAndYieldThrough(['Parent: 2']);
+ return result;
+ }
- // Mutate the config. This is at lower priority so that 1) to make sure
- // it doesn't happen to get batched with the in-progress render, and 2)
- // so it doesn't interrupt the in-progress render.
- Scheduler.unstable_runWithPriority(
- Scheduler.unstable_IdlePriority,
- () => {
- source.valueB = '3';
- },
- );
- });
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render( );
+ });
+ expect(Scheduler).toHaveYielded(['Parent: 1', 'Child: 2', 'Commit: 1, 2']);
- expect(Scheduler).toHaveYielded([
- // The partial render completes
- 'Child: 2',
- 'Commit: 2, 2',
+ await act(async () => {
+ // Switch the parent and the child to read using the same config
+ root.render( );
+ // Start rendering the parent, but yield before rendering the child
+ expect(Scheduler).toFlushAndYieldThrough(['Parent: 2']);
- // Then we start rendering the low priority mutation
- 'Parent: 3',
-
- // Eventually the child corrects itself, because of the check that
- // occurs when re-subscribing.
- 'Child: 3',
- 'Commit: 3, 3',
- ]);
+ // Mutate the config. This is at lower priority so that 1) to make sure
+ // it doesn't happen to get batched with the in-progress render, and 2)
+ // so it doesn't interrupt the in-progress render.
+ Scheduler.unstable_runWithPriority(
+ Scheduler.unstable_IdlePriority,
+ () => {
+ source.valueB = '3';
+ },
+ );
});
- it('should not tear with newly mounted component when updates were scheduled at a lower priority', async () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
+ expect(Scheduler).toHaveYielded([
+ // The partial render completes
+ 'Child: 2',
+ 'Commit: 2, 2',
- let committedA = null;
- let committedB = null;
+ // Then we start rendering the low priority mutation
+ 'Parent: 3',
- const onRender = () => {
- if (committedB !== null) {
- expect(committedA).toBe(committedB);
- }
- };
+ // Eventually the child corrects itself, because of the check that
+ // occurs when re-subscribing.
+ 'Child: 3',
+ 'Commit: 3, 3',
+ ]);
+ });
- function ComponentA() {
- const snapshot = useMutableSource(
- mutableSource,
- defaultGetSnapshot,
- defaultSubscribe,
- );
- Scheduler.unstable_yieldValue(`a:${snapshot}`);
- React.useEffect(() => {
- committedA = snapshot;
- }, [snapshot]);
- return {`a:${snapshot}`}
;
- }
- function ComponentB() {
- const snapshot = useMutableSource(
- mutableSource,
- defaultGetSnapshot,
- defaultSubscribe,
- );
- Scheduler.unstable_yieldValue(`b:${snapshot}`);
- React.useEffect(() => {
- committedB = snapshot;
- }, [snapshot]);
- return {`b:${snapshot}`}
;
+ // @gate experimental
+ it('should not tear with newly mounted component when updates were scheduled at a lower priority', async () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ let committedA = null;
+ let committedB = null;
+
+ const onRender = () => {
+ if (committedB !== null) {
+ expect(committedA).toBe(committedB);
}
+ };
- // Mount ComponentA with data version 1
- act(() => {
- ReactNoop.render(
-
-
- ,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- });
- expect(Scheduler).toHaveYielded(['a:one', 'Sync effect']);
+ function ComponentA() {
+ const snapshot = useMutableSource(
+ mutableSource,
+ defaultGetSnapshot,
+ defaultSubscribe,
+ );
+ Scheduler.unstable_yieldValue(`a:${snapshot}`);
+ React.useEffect(() => {
+ committedA = snapshot;
+ }, [snapshot]);
+ return {`a:${snapshot}`}
;
+ }
+ function ComponentB() {
+ const snapshot = useMutableSource(
+ mutableSource,
+ defaultGetSnapshot,
+ defaultSubscribe,
+ );
+ Scheduler.unstable_yieldValue(`b:${snapshot}`);
+ React.useEffect(() => {
+ committedB = snapshot;
+ }, [snapshot]);
+ return {`b:${snapshot}`}
;
+ }
+
+ // Mount ComponentA with data version 1
+ act(() => {
+ ReactNoop.render(
+
+
+ ,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ });
+ expect(Scheduler).toHaveYielded(['a:one', 'Sync effect']);
+ expect(source.listenerCount).toBe(1);
+
+ // Mount ComponentB with version 1 (but don't commit it)
+ act(() => {
+ ReactNoop.render(
+
+
+
+ ,
+ () => Scheduler.unstable_yieldValue('Sync effect'),
+ );
+ expect(Scheduler).toFlushAndYieldThrough([
+ 'a:one',
+ 'b:one',
+ 'Sync effect',
+ ]);
expect(source.listenerCount).toBe(1);
- // Mount ComponentB with version 1 (but don't commit it)
- act(() => {
- ReactNoop.render(
-
-
-
- ,
- () => Scheduler.unstable_yieldValue('Sync effect'),
- );
- expect(Scheduler).toFlushAndYieldThrough([
- 'a:one',
- 'b:one',
- 'Sync effect',
- ]);
- expect(source.listenerCount).toBe(1);
+ // Mutate -> schedule update for ComponentA
+ Scheduler.unstable_runWithPriority(
+ Scheduler.unstable_IdlePriority,
+ () => {
+ source.value = 'two';
+ },
+ );
- // Mutate -> schedule update for ComponentA
- Scheduler.unstable_runWithPriority(
- Scheduler.unstable_IdlePriority,
- () => {
- source.value = 'two';
- },
- );
+ // Commit ComponentB -> notice the change and schedule an update for ComponentB
+ expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
+ expect(source.listenerCount).toBe(2);
+ });
+ });
- // Commit ComponentB -> notice the change and schedule an update for ComponentB
- expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);
- expect(source.listenerCount).toBe(2);
+ if (__DEV__) {
+ describe('dev warnings', () => {
+ // @gate experimental
+ it('should warn if the subscribe function does not return an unsubscribe function', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ const brokenSubscribe = () => {};
+
+ expect(() => {
+ act(() => {
+ ReactNoop.render(
+ ,
+ );
+ });
+ }).toErrorDev(
+ 'Mutable source subscribe function must return an unsubscribe function.',
+ );
+ });
+
+ // @gate experimental
+ it('should error if multiple renderers of the same type use a mutable source at the same time', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ act(() => {
+ // Start a render that uses the mutable source.
+ ReactNoop.render(
+ <>
+
+
+ >,
+ );
+ expect(Scheduler).toFlushAndYieldThrough(['a:one']);
+
+ const PrevScheduler = Scheduler;
+
+ // Get a new copy of ReactNoop.
+ loadModules();
+
+ spyOnDev(console, 'error');
+
+ // Use the mutablesource again but with a different renderer.
+ ReactNoop.render(
+ ,
+ );
+ expect(Scheduler).toFlushAndYieldThrough(['c:one']);
+
+ expect(console.error.calls.argsFor(0)[0]).toContain(
+ 'Detected multiple renderers concurrently rendering the ' +
+ 'same mutable source. This is currently unsupported.',
+ );
+
+ // TODO (useMutableSource) Act will automatically flush remaining work from render 1,
+ // but at this point something in the hooks dispatcher has been broken by jest.resetModules()
+ // Figure out what this is and remove this catch.
+ expect(() =>
+ PrevScheduler.unstable_flushAllWithoutAsserting(),
+ ).toThrow('Invalid hook call');
+ });
+ });
+
+ // @gate experimental
+ it('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ act(() => {
+ // Start a render that uses the mutable source.
+ ReactNoop.render(
+ <>
+
+
+ >,
+ );
+ expect(Scheduler).toFlushAndYieldThrough(['a:one']);
+
+ const PrevScheduler = Scheduler;
+
+ // Get a new copy of ReactNoop.
+ loadModules();
+
+ spyOnDev(console, 'error');
+
+ // Mutate before the new render reads from the source.
+ source.value = 'two';
+
+ // Use the mutablesource again but with a different renderer.
+ ReactNoop.render(
+ ,
+ );
+ expect(Scheduler).toFlushAndYieldThrough(['c:two']);
+
+ expect(console.error.calls.argsFor(0)[0]).toContain(
+ 'Detected multiple renderers concurrently rendering the ' +
+ 'same mutable source. This is currently unsupported.',
+ );
+
+ // TODO (useMutableSource) Act will automatically flush remaining work from render 1,
+ // but at this point something in the hooks dispatcher has been broken by jest.resetModules()
+ // Figure out what this is and remove this catch.
+ expect(() =>
+ PrevScheduler.unstable_flushAllWithoutAsserting(),
+ ).toThrow('Invalid hook call');
+ });
});
});
-
- if (__DEV__) {
- describe('dev warnings', () => {
- it('should warn if the subscribe function does not return an unsubscribe function', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- const brokenSubscribe = () => {};
-
- expect(() => {
- act(() => {
- ReactNoop.render(
- ,
- );
- });
- }).toErrorDev(
- 'Mutable source subscribe function must return an unsubscribe function.',
- );
- });
-
- it('should error if multiple renderers of the same type use a mutable source at the same time', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- act(() => {
- // Start a render that uses the mutable source.
- ReactNoop.render(
- <>
-
-
- >,
- );
- expect(Scheduler).toFlushAndYieldThrough(['a:one']);
-
- const PrevScheduler = Scheduler;
-
- // Get a new copy of ReactNoop.
- loadModules();
-
- spyOnDev(console, 'error');
-
- // Use the mutablesource again but with a different renderer.
- ReactNoop.render(
- ,
- );
- expect(Scheduler).toFlushAndYieldThrough(['c:one']);
-
- expect(console.error.calls.argsFor(0)[0]).toContain(
- 'Detected multiple renderers concurrently rendering the ' +
- 'same mutable source. This is currently unsupported.',
- );
-
- // TODO (useMutableSource) Act will automatically flush remaining work from render 1,
- // but at this point something in the hooks dispatcher has been broken by jest.resetModules()
- // Figure out what this is and remove this catch.
- expect(() =>
- PrevScheduler.unstable_flushAllWithoutAsserting(),
- ).toThrow('Invalid hook call');
- });
- });
-
- it('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', () => {
- const source = createSource('one');
- const mutableSource = createMutableSource(source);
-
- act(() => {
- // Start a render that uses the mutable source.
- ReactNoop.render(
- <>
-
-
- >,
- );
- expect(Scheduler).toFlushAndYieldThrough(['a:one']);
-
- const PrevScheduler = Scheduler;
-
- // Get a new copy of ReactNoop.
- loadModules();
-
- spyOnDev(console, 'error');
-
- // Mutate before the new render reads from the source.
- source.value = 'two';
-
- // Use the mutablesource again but with a different renderer.
- ReactNoop.render(
- ,
- );
- expect(Scheduler).toFlushAndYieldThrough(['c:two']);
-
- expect(console.error.calls.argsFor(0)[0]).toContain(
- 'Detected multiple renderers concurrently rendering the ' +
- 'same mutable source. This is currently unsupported.',
- );
-
- // TODO (useMutableSource) Act will automatically flush remaining work from render 1,
- // but at this point something in the hooks dispatcher has been broken by jest.resetModules()
- // Figure out what this is and remove this catch.
- expect(() =>
- PrevScheduler.unstable_flushAllWithoutAsserting(),
- ).toThrow('Invalid hook call');
- });
- });
- });
- }
}
});
diff --git a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js
index fa6be59c7d..6a79960e38 100644
--- a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js
+++ b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js
@@ -62,13 +62,9 @@ describe('ReactDOMTracing', () => {
loadModules();
});
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
describe('interaction tracing', () => {
describe('hidden', () => {
+ // @gate experimental
it('traces interaction through hidden subtree', () => {
const Child = () => {
const [didMount, setDidMount] = React.useState(false);
@@ -145,6 +141,7 @@ describe('ReactDOMTracing', () => {
);
});
+ // @gate experimental
it('traces interaction through hidden subtree when there is other pending traced work', () => {
const Child = () => {
Scheduler.unstable_yieldValue('Child');
@@ -212,6 +209,7 @@ describe('ReactDOMTracing', () => {
).toHaveBeenLastNotifiedOfInteraction(interaction);
});
+ // @gate experimental
it('traces interaction through hidden subtree that schedules more idle/never work', () => {
const Child = () => {
const [didMount, setDidMount] = React.useState(false);
@@ -294,6 +292,7 @@ describe('ReactDOMTracing', () => {
);
});
+ // @gate experimental
it('does not continue interactions across pre-existing idle work', () => {
const Child = () => {
Scheduler.unstable_yieldValue('Child');
@@ -394,6 +393,7 @@ describe('ReactDOMTracing', () => {
});
});
+ // @gate experimental
it('should properly trace interactions when there is work of interleaved priorities', () => {
const Child = () => {
Scheduler.unstable_yieldValue('Child');
@@ -515,6 +515,7 @@ describe('ReactDOMTracing', () => {
});
});
+ // @gate experimental
it('should properly trace interactions through a multi-pass SuspenseList render', () => {
const SuspenseList = React.SuspenseList;
const Suspense = React.Suspense;
@@ -599,7 +600,8 @@ describe('ReactDOMTracing', () => {
});
describe('hydration', () => {
- it('traces interaction across hydration', async done => {
+ // @gate experimental
+ it('traces interaction across hydration', () => {
const ref = React.createRef();
function Child() {
@@ -644,11 +646,10 @@ describe('ReactDOMTracing', () => {
expect(
onInteractionScheduledWorkCompleted,
).toHaveBeenLastNotifiedOfInteraction(interaction);
-
- done();
});
- it('traces interaction across suspended hydration', async done => {
+ // @gate experimental
+ it('traces interaction across suspended hydration', async () => {
let suspend = false;
let resolve;
const promise = new Promise(
@@ -716,11 +717,10 @@ describe('ReactDOMTracing', () => {
expect(
onInteractionScheduledWorkCompleted,
).toHaveBeenLastNotifiedOfInteraction(interaction);
-
- done();
});
- it('traces interaction across client-rendered hydration', async done => {
+ // @gate experimental
+ it('traces interaction across client-rendered hydration', () => {
let suspend = false;
const promise = new Promise(() => {});
const ref = React.createRef();
@@ -788,8 +788,6 @@ describe('ReactDOMTracing', () => {
expect(
onInteractionScheduledWorkCompleted,
).toHaveBeenLastNotifiedOfInteraction(interaction);
-
- done();
});
});
});
diff --git a/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js b/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
index b5509ad7de..6c1df098d6 100644
--- a/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
+++ b/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
@@ -53,11 +53,7 @@ describe('ProfilerDOM', () => {
return props.text;
}
- if (!__EXPERIMENTAL__) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
+ // @gate experimental
it('should correctly trace interactions for async roots', async () => {
let resolve;
let thenable = {
diff --git a/packages/shared/__tests__/ReactError-test.internal.js b/packages/shared/__tests__/ReactError-test.internal.js
index cd9fc5697f..0672471ea7 100644
--- a/packages/shared/__tests__/ReactError-test.internal.js
+++ b/packages/shared/__tests__/ReactError-test.internal.js
@@ -36,29 +36,28 @@ describe('ReactError', () => {
}
});
- if (__DEV__) {
- it("empty test so Jest doesn't complain", () => {});
- } else {
- it('should error with minified error code', () => {
- expect(() => ReactDOM.render('Hi', null)).toThrowError(
- 'Minified React error #200; visit ' +
- 'https://reactjs.org/docs/error-decoder.html?invariant=200' +
- ' for the full message or use the non-minified dev environment' +
- ' for full errors and additional helpful warnings.',
- );
- });
- it('should serialize arguments', () => {
- function Oops() {
- return;
- }
- Oops.displayName = '#wtf';
- const container = document.createElement('div');
- expect(() => ReactDOM.render( , container)).toThrowError(
- 'Minified React error #152; visit ' +
- 'https://reactjs.org/docs/error-decoder.html?invariant=152&args[]=%23wtf' +
- ' for the full message or use the non-minified dev environment' +
- ' for full errors and additional helpful warnings.',
- );
- });
- }
+ // @gate build === "production"
+ it('should error with minified error code', () => {
+ expect(() => ReactDOM.render('Hi', null)).toThrowError(
+ 'Minified React error #200; visit ' +
+ 'https://reactjs.org/docs/error-decoder.html?invariant=200' +
+ ' for the full message or use the non-minified dev environment' +
+ ' for full errors and additional helpful warnings.',
+ );
+ });
+
+ // @gate build === "production"
+ it('should serialize arguments', () => {
+ function Oops() {
+ return;
+ }
+ Oops.displayName = '#wtf';
+ const container = document.createElement('div');
+ expect(() => ReactDOM.render( , container)).toThrowError(
+ 'Minified React error #152; visit ' +
+ 'https://reactjs.org/docs/error-decoder.html?invariant=152&args[]=%23wtf' +
+ ' for the full message or use the non-minified dev environment' +
+ ' for full errors and additional helpful warnings.',
+ );
+ });
});
diff --git a/packages/shared/__tests__/describeComponentFrame-test.js b/packages/shared/__tests__/describeComponentFrame-test.js
index 3b7b843cae..62fe858059 100644
--- a/packages/shared/__tests__/describeComponentFrame-test.js
+++ b/packages/shared/__tests__/describeComponentFrame-test.js
@@ -11,7 +11,6 @@
let React;
let ReactDOM;
-const ReactFeatureFlags = require('shared/ReactFeatureFlags');
describe('Component stack trace displaying', () => {
beforeEach(() => {
@@ -19,11 +18,7 @@ describe('Component stack trace displaying', () => {
ReactDOM = require('react-dom');
});
- if (ReactFeatureFlags.enableComponentStackLocations) {
- it("empty test so Jest doesn't complain", () => {});
- return;
- }
-
+ // @gate !enableComponentStackLocations || !__DEV__
it('should provide filenames in stack traces', () => {
class Component extends React.Component {
render() {
diff --git a/scripts/jest/TestFlags.js b/scripts/jest/TestFlags.js
index 903f406f4f..79d4f17896 100644
--- a/scripts/jest/TestFlags.js
+++ b/scripts/jest/TestFlags.js
@@ -35,7 +35,11 @@
const environmentFlags = {
__DEV__,
build: __DEV__ ? 'development' : 'production',
+
+ // TODO: Should "experimental" also imply "modern"? Maybe we should
+ // always compare to the channel?
experimental: __EXPERIMENTAL__,
+ // Similarly, should stable imply "classic"?
stable: !__EXPERIMENTAL__,
};
@@ -44,6 +48,18 @@ function getTestFlags() {
// not to but there are exceptions.
const featureFlags = require('shared/ReactFeatureFlags');
+ // TODO: This is a heuristic to detect the release channel by checking a flag
+ // that is known to only be enabled in www. What we should do instead is set
+ // the release channel explicitly in the each test config file.
+ const www = featureFlags.enableSuspenseCallback === true;
+ const releaseChannel = www
+ ? __EXPERIMENTAL__
+ ? 'modern'
+ : 'classic'
+ : __EXPERIMENTAL__
+ ? 'experimental'
+ : 'stable';
+
// Return a proxy so we can throw if you attempt to access a flag that
// doesn't exist.
return new Proxy(
@@ -52,6 +68,11 @@ function getTestFlags() {
old: featureFlags.enableNewReconciler === true,
new: featureFlags.enableNewReconciler === true,
+ channel: releaseChannel,
+ modern: releaseChannel === 'modern',
+ classic: releaseChannel === 'classic',
+ www,
+
...featureFlags,
...environmentFlags,
},