'use strict';
let React;
let ReactDOMClient;
let ReactDOM;
let createPortal;
let act;
let container;
let Fragment;
let Activity;
let mockIntersectionObserver;
let simulateIntersection;
let setClientRects;
let assertConsoleErrorDev;
function Wrapper({children}) {
return children;
}
describe('FragmentRefs', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
Fragment = React.Fragment;
Activity = React.unstable_Activity;
ReactDOMClient = require('react-dom/client');
ReactDOM = require('react-dom');
createPortal = ReactDOM.createPortal;
act = require('internal-test-utils').act;
const IntersectionMocks = require('./utils/IntersectionMocks');
mockIntersectionObserver = IntersectionMocks.mockIntersectionObserver;
simulateIntersection = IntersectionMocks.simulateIntersection;
setClientRects = IntersectionMocks.setClientRects;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
container = document.createElement('div');
document.body.innerHTML = '';
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
it('attaches a ref to Fragment', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
await act(() =>
root.render(
<div id="parent">
<Fragment ref={fragmentRef}>
<div id="child">Hi</div>
</Fragment>
</div>,
),
);
expect(container.innerHTML).toEqual(
'<div id="parent"><div id="child">Hi</div></div>',
);
expect(fragmentRef.current).not.toBe(null);
});
it('accepts a ref callback', async () => {
let fragmentRef;
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<Fragment ref={ref => (fragmentRef = ref)}>
<div id="child">Hi</div>
</Fragment>,
);
});
expect(fragmentRef._fragmentFiber).toBeTruthy();
});
it('is available in effects', async () => {
function Test() {
const fragmentRef = React.useRef(null);
React.useLayoutEffect(() => {
expect(fragmentRef.current).not.toBe(null);
});
React.useEffect(() => {
expect(fragmentRef.current).not.toBe(null);
});
return (
<Fragment ref={fragmentRef}>
<div />
</Fragment>
);
}
const root = ReactDOMClient.createRoot(container);
await act(() => root.render(<Test />));
});
describe('focus methods', () => {
describe('focus()', () => {
it('focuses the first focusable child', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<div>
<Fragment ref={fragmentRef}>
<div id="child-a" />
<style>{`#child-c {}`}</style>
<a id="child-b" href="/">
B
</a>
<a id="child-c" href="/">
C
</a>
</Fragment>
</div>
);
}
await act(() => {
root.render(<Test />);
});
await act(() => {
fragmentRef.current.focus();
});
expect(document.activeElement.id).toEqual('child-b');
document.activeElement.blur();
});
it('focuses deeply nested focusable children, depth first', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<Fragment ref={fragmentRef}>
<div id="child-a">
<div tabIndex={0} id="grandchild-a">
<a id="greatgrandchild-a" href="/" />
</div>
</div>
<a id="child-b" href="/" />
</Fragment>
);
}
await act(() => {
root.render(<Test />);
});
await act(() => {
fragmentRef.current.focus();
});
expect(document.activeElement.id).toEqual('grandchild-a');
});
it('preserves document order when adding and removing children', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test({showA, showB}) {
return (
<Fragment ref={fragmentRef}>
{showA && <a href="/" id="child-a" />}
{showB && <a href="/" id="child-b" />}
</Fragment>
);
}
await act(() => {
root.render(<Test showA={true} showB={false} />);
});
await act(() => {
fragmentRef.current.focus();
});
expect(document.activeElement.id).toEqual('child-a');
document.activeElement.blur();
await act(() => {
root.render(<Test showA={true} showB={true} />);
});
await act(() => {
fragmentRef.current.focus();
});
expect(document.activeElement.id).toEqual('child-a');
document.activeElement.blur();
await act(() => {
root.render(<Test showA={false} showB={true} />);
});
await act(() => {
fragmentRef.current.focus();
});
expect(document.activeElement.id).toEqual('child-b');
document.activeElement.blur();
});
});
describe('focusLast()', () => {
it('focuses the last focusable child', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<div>
<Fragment ref={fragmentRef}>
<a id="child-a" href="/">
A
</a>
<a id="child-b" href="/">
B
</a>
<Wrapper>
<a id="child-c" href="/">
C
</a>
</Wrapper>
<div id="child-d" />
<style id="child-e">{`#child-d {}`}</style>
</Fragment>
</div>
);
}
await act(() => {
root.render(<Test />);
});
await act(() => {
fragmentRef.current.focusLast();
});
expect(document.activeElement.id).toEqual('child-c');
document.activeElement.blur();
});
it('focuses deeply nested focusable children, depth first', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<Fragment ref={fragmentRef}>
<div id="child-a" href="/">
<a id="grandchild-a" href="/" />
<a id="grandchild-b" href="/" />
</div>
<div tabIndex={0} id="child-b">
<a id="grandchild-a" href="/" />
<a id="grandchild-b" href="/" />
</div>
</Fragment>
);
}
await act(() => {
root.render(<Test />);
});
await act(() => {
fragmentRef.current.focusLast();
});
expect(document.activeElement.id).toEqual('grandchild-b');
});
});
describe('blur()', () => {
it('removes focus from an element inside of the Fragment', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<Fragment ref={fragmentRef}>
<a id="child-a" href="/">
A
</a>
</Fragment>
);
}
await act(() => {
root.render(<Test />);
});
await act(() => {
fragmentRef.current.focus();
});
expect(document.activeElement.id).toEqual('child-a');
await act(() => {
fragmentRef.current.blur();
});
expect(document.activeElement).toEqual(document.body);
});
it('does not remove focus from elements outside of the Fragment', async () => {
const fragmentRefA = React.createRef();
const fragmentRefB = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<Fragment ref={fragmentRefA}>
<a id="child-a" href="/">
A
</a>
<Fragment ref={fragmentRefB}>
<a id="child-b" href="/">
B
</a>
</Fragment>
</Fragment>
);
}
await act(() => {
root.render(<Test />);
});
await act(() => {
fragmentRefA.current.focus();
});
expect(document.activeElement.id).toEqual('child-a');
await act(() => {
fragmentRefB.current.blur();
});
expect(document.activeElement.id).toEqual('child-a');
});
});
});
describe('events', () => {
describe('add/remove event listeners', () => {
it('adds and removes event listeners from children', async () => {
const parentRef = React.createRef();
const fragmentRef = React.createRef();
const childARef = React.createRef();
const childBRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
let logs = [];
function handleFragmentRefClicks() {
logs.push('fragmentRef');
}
function Test() {
React.useEffect(() => {
fragmentRef.current.addEventListener(
'click',
handleFragmentRefClicks,
);
return () => {
fragmentRef.current.removeEventListener(
'click',
handleFragmentRefClicks,
);
};
}, []);
return (
<div ref={parentRef}>
<Fragment ref={fragmentRef}>
<>Text</>
<div ref={childARef}>A</div>
<>
<div ref={childBRef}>B</div>
</>
</Fragment>
</div>
);
}
await act(() => {
root.render(<Test />);
});
childARef.current.addEventListener('click', () => {
logs.push('A');
});
childBRef.current.addEventListener('click', () => {
logs.push('B');
});
parentRef.current.click();
expect(logs).toEqual([]);
childARef.current.click();
expect(logs).toEqual(['fragmentRef', 'A']);
logs = [];
childBRef.current.click();
expect(logs).toEqual(['fragmentRef', 'B']);
logs = [];
fragmentRef.current.removeEventListener(
'click',
handleFragmentRefClicks,
);
childARef.current.click();
expect(logs).toEqual(['A']);
logs = [];
childBRef.current.click();
expect(logs).toEqual(['B']);
});
it('adds and removes event listeners from children with multiple fragments', async () => {
const fragmentRef = React.createRef();
const nestedFragmentRef = React.createRef();
const nestedFragmentRef2 = React.createRef();
const childARef = React.createRef();
const childBRef = React.createRef();
const childCRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<div>
<Fragment ref={fragmentRef}>
<div ref={childARef}>A</div>
<div>
<Fragment ref={nestedFragmentRef}>
<div ref={childBRef}>B</div>
</Fragment>
</div>
<Fragment ref={nestedFragmentRef2}>
<div ref={childCRef}>C</div>
</Fragment>
</Fragment>
</div>,
);
});
let logs = [];
function handleFragmentRefClicks() {
logs.push('fragmentRef');
}
function handleNestedFragmentRefClicks() {
logs.push('nestedFragmentRef');
}
function handleNestedFragmentRef2Clicks() {
logs.push('nestedFragmentRef2');
}
fragmentRef.current.addEventListener('click', handleFragmentRefClicks);
nestedFragmentRef.current.addEventListener(
'click',
handleNestedFragmentRefClicks,
);
nestedFragmentRef2.current.addEventListener(
'click',
handleNestedFragmentRef2Clicks,
);
childBRef.current.click();
expect(logs).toEqual(['nestedFragmentRef', 'fragmentRef']);
logs = [];
childARef.current.click();
expect(logs).toEqual(['fragmentRef']);
logs = [];
childCRef.current.click();
expect(logs).toEqual(['fragmentRef', 'nestedFragmentRef2']);
logs = [];
fragmentRef.current.removeEventListener(
'click',
handleFragmentRefClicks,
);
nestedFragmentRef.current.removeEventListener(
'click',
handleNestedFragmentRefClicks,
);
childCRef.current.click();
expect(logs).toEqual(['nestedFragmentRef2']);
});
it('adds an event listener to a newly added child', async () => {
const fragmentRef = React.createRef();
const childRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
let showChild;
function Component() {
const [shouldShowChild, setShouldShowChild] = React.useState(false);
showChild = () => {
setShouldShowChild(true);
};
return (
<div>
<Fragment ref={fragmentRef}>
<div id="a">A</div>
{shouldShowChild && (
<div ref={childRef} id="b">
B
</div>
)}
</Fragment>
</div>
);
}
await act(() => {
root.render(<Component />);
});
expect(fragmentRef.current).not.toBe(null);
expect(childRef.current).toBe(null);
let hasClicked = false;
fragmentRef.current.addEventListener('click', () => {
hasClicked = true;
});
await act(() => {
showChild();
});
expect(childRef.current).not.toBe(null);
childRef.current.click();
expect(hasClicked).toBe(true);
});
it('applies event listeners to host children nested within non-host children', async () => {
const fragmentRef = React.createRef();
const childRef = React.createRef();
const nestedChildRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<div>
<Fragment ref={fragmentRef}>
<div ref={childRef}>Host A</div>
<Wrapper>
<Wrapper>
<Wrapper>
<div ref={nestedChildRef}>Host B</div>
</Wrapper>
</Wrapper>
</Wrapper>
</Fragment>
</div>,
);
});
const logs = [];
fragmentRef.current.addEventListener('click', e => {
logs.push(e.target.textContent);
});
expect(logs).toEqual([]);
childRef.current.click();
expect(logs).toEqual(['Host A']);
nestedChildRef.current.click();
expect(logs).toEqual(['Host A', 'Host B']);
});
it('allows adding and cleaning up listeners in effects', async () => {
const root = ReactDOMClient.createRoot(container);
let logs = [];
function logClick(e) {
logs.push(e.currentTarget.id);
}
let rerender;
let removeEventListeners;
function Test() {
const fragmentRef = React.useRef(null);
const [_, setState] = React.useState(0);
rerender = () => {
setState(p => p + 1);
};
removeEventListeners = () => {
fragmentRef.current.removeEventListener('click', logClick);
};
React.useEffect(() => {
fragmentRef.current.addEventListener('click', logClick);
return removeEventListeners;
});
return (
<Fragment ref={fragmentRef}>
<div id="child-a" />
</Fragment>
);
}
await act(() => root.render(<Test />));
expect(logs).toEqual([]);
document.querySelector('#child-a').click();
expect(logs).toEqual(['child-a']);
logs = [];
await act(rerender);
document.querySelector('#child-a').click();
expect(logs).toEqual(['child-a']);
});
it('does not apply removed event listeners to new children', async () => {
const root = ReactDOMClient.createRoot(container);
const fragmentRef = React.createRef(null);
function Test() {
return (
<Fragment ref={fragmentRef}>
<div id="child-a" />
</Fragment>
);
}
let logs = [];
function logClick(e) {
logs.push(e.currentTarget.id);
}
await act(() => {
root.render(<Test />);
});
fragmentRef.current.addEventListener('click', logClick);
const childA = document.querySelector('#child-a');
childA.click();
expect(logs).toEqual(['child-a']);
logs = [];
fragmentRef.current.removeEventListener('click', logClick);
childA.click();
expect(logs).toEqual([]);
});
it('applies event listeners to portaled children', async () => {
const fragmentRef = React.createRef();
const childARef = React.createRef();
const childBRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<Fragment ref={fragmentRef}>
<div id="child-a" ref={childARef} />
{createPortal(
<div id="child-b" ref={childBRef} />,
document.body,
)}
</Fragment>
);
}
await act(() => {
root.render(<Test />);
});
const logs = [];
fragmentRef.current.addEventListener('click', e => {
logs.push(e.target.id);
});
childARef.current.click();
expect(logs).toEqual(['child-a']);
logs.length = 0;
childBRef.current.click();
expect(logs).toEqual(['child-b']);
});
describe('with activity', () => {
it('does not apply event listeners to hidden trees', async () => {
const parentRef = React.createRef();
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<div ref={parentRef}>
<Fragment ref={fragmentRef}>
<div>Child 1</div>
<Activity mode="hidden">
<div>Child 2</div>
</Activity>
<div>Child 3</div>
</Fragment>
</div>
);
}
await act(() => {
root.render(<Test />);
});
const logs = [];
fragmentRef.current.addEventListener('click', e => {
logs.push(e.target.textContent);
});
const [child1, child2, child3] = parentRef.current.children;
child1.click();
child2.click();
child3.click();
expect(logs).toEqual(['Child 1', 'Child 3']);
});
it('applies event listeners to visible trees', async () => {
const parentRef = React.createRef();
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<div ref={parentRef}>
<Fragment ref={fragmentRef}>
<div>Child 1</div>
<Activity mode="visible">
<div>Child 2</div>
</Activity>
<div>Child 3</div>
</Fragment>
</div>
);
}
await act(() => {
root.render(<Test />);
});
const logs = [];
fragmentRef.current.addEventListener('click', e => {
logs.push(e.target.textContent);
});
const [child1, child2, child3] = parentRef.current.children;
child1.click();
child2.click();
child3.click();
expect(logs).toEqual(['Child 1', 'Child 2', 'Child 3']);
});
it('handles Activity modes switching', async () => {
const fragmentRef = React.createRef();
const fragmentRef2 = React.createRef();
const parentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test({mode}) {
return (
<div id="parent" ref={parentRef}>
<Fragment ref={fragmentRef}>
<Activity mode={mode}>
<div id="child1">Child</div>
<Fragment ref={fragmentRef2}>
<div id="child2">Child 2</div>
</Fragment>
</Activity>
</Fragment>
</div>
);
}
await act(() => {
root.render(<Test mode="visible" />);
});
let logs = [];
fragmentRef.current.addEventListener('click', () => {
logs.push('clicked 1');
});
fragmentRef2.current.addEventListener('click', () => {
logs.push('clicked 2');
});
parentRef.current.lastChild.click();
expect(logs).toEqual(['clicked 1', 'clicked 2']);
logs = [];
await act(() => {
root.render(<Test mode="hidden" />);
});
parentRef.current.firstChild.click();
parentRef.current.lastChild.click();
expect(logs).toEqual([]);
logs = [];
await act(() => {
root.render(<Test mode="visible" />);
});
parentRef.current.lastChild.click();
expect(logs).toEqual(['clicked 2', 'clicked 1']);
});
});
});
describe('dispatchEvent()', () => {
it('fires events on the host parent if bubbles=true', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
let logs = [];
function handleClick(e) {
logs.push([e.type, e.target.id, e.currentTarget.id]);
}
function Test({isMounted}) {
return (
<div onClick={handleClick} id="grandparent">
<div onClick={handleClick} id="parent">
{isMounted && (
<Fragment ref={fragmentRef}>
<div onClick={handleClick} id="child">
Hi
</div>
</Fragment>
)}
</div>
</div>
);
}
await act(() => {
root.render(<Test isMounted={true} />);
});
let isCancelable = !fragmentRef.current.dispatchEvent(
new MouseEvent('click', {bubbles: true}),
);
expect(logs).toEqual([
['click', 'parent', 'parent'],
['click', 'parent', 'grandparent'],
]);
expect(isCancelable).toBe(false);
const fragmentInstanceHandle = fragmentRef.current;
await act(() => {
root.render(<Test isMounted={false} />);
});
logs = [];
isCancelable = !fragmentInstanceHandle.dispatchEvent(
new MouseEvent('click', {bubbles: true}),
);
expect(logs).toEqual([]);
expect(isCancelable).toBe(false);
logs = [];
isCancelable = !fragmentInstanceHandle.dispatchEvent(
new MouseEvent('click', {bubbles: false}),
);
expect(logs).toEqual([]);
expect(isCancelable).toBe(false);
});
it('fires events on self, and only self if bubbles=false', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
let logs = [];
function handleClick(e) {
logs.push([e.type, e.target.id, e.currentTarget.id]);
}
function Test() {
return (
<div id="parent" onClick={handleClick}>
<Fragment ref={fragmentRef} />
</div>
);
}
await act(() => {
root.render(<Test />);
});
fragmentRef.current.addEventListener('click', handleClick);
fragmentRef.current.dispatchEvent(
new MouseEvent('click', {bubbles: true}),
);
expect(logs).toEqual([
['click', undefined, undefined],
['click', 'parent', 'parent'],
]);
logs = [];
fragmentRef.current.dispatchEvent(
new MouseEvent('click', {bubbles: false}),
);
expect(logs).toEqual([['click', undefined, undefined]]);
});
});
});
describe('observers', () => {
beforeEach(() => {
mockIntersectionObserver();
});
it('attaches intersection observers to children', async () => {
let logs = [];
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
logs.push(entry.target.id);
});
});
function Test({showB}) {
const fragmentRef = React.useRef(null);
React.useEffect(() => {
fragmentRef.current.observeUsing(observer);
const lastRefValue = fragmentRef.current;
return () => {
lastRefValue.unobserveUsing(observer);
};
}, []);
return (
<div id="parent">
<React.Fragment ref={fragmentRef}>
<div id="childA">A</div>
{showB && <div id="childB">B</div>}
</React.Fragment>
</div>
);
}
function simulateAllChildrenIntersecting() {
const parent = container.firstChild;
if (parent) {
const children = Array.from(parent.children).map(child => {
return [child, {y: 0, x: 0, width: 1, height: 1}, 1];
});
simulateIntersection(...children);
}
}
const root = ReactDOMClient.createRoot(container);
await act(() => root.render(<Test showB={false} />));
simulateAllChildrenIntersecting();
expect(logs).toEqual(['childA']);
logs = [];
await act(() => root.render(<Test showB={true} />));
simulateAllChildrenIntersecting();
expect(logs).toEqual(['childA', 'childB']);
logs = [];
await act(() => root.render(<Test showB={false} />));
simulateAllChildrenIntersecting();
expect(logs).toEqual(['childA']);
logs = [];
await act(() => root.render(null));
simulateAllChildrenIntersecting();
expect(logs).toEqual([]);
});
it('warns when unobserveUsing() is called with an observer that was not observed', async () => {
const fragmentRef = React.createRef();
const observer = new IntersectionObserver(() => {});
const observer2 = new IntersectionObserver(() => {});
function Test() {
return (
<React.Fragment ref={fragmentRef}>
<div />
</React.Fragment>
);
}
const root = ReactDOMClient.createRoot(container);
await act(() => root.render(<Test />));
fragmentRef.current.unobserveUsing(observer);
assertConsoleErrorDev(
[
'You are calling unobserveUsing() with an observer that is not being observed with this fragment ' +
'instance. First attach the observer with observeUsing()',
],
{withoutStack: true},
);
fragmentRef.current.observeUsing(observer);
fragmentRef.current.unobserveUsing(observer2);
assertConsoleErrorDev(
[
'You are calling unobserveUsing() with an observer that is not being observed with this fragment ' +
'instance. First attach the observer with observeUsing()',
],
{withoutStack: true},
);
});
});
describe('getClientRects', () => {
it('returns the bounding client rects of all children', async () => {
const fragmentRef = React.createRef();
const childARef = React.createRef();
const childBRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<React.Fragment ref={fragmentRef}>
<div ref={childARef} />
<div ref={childBRef} />
</React.Fragment>
);
}
await act(() => root.render(<Test />));
setClientRects(childARef.current, [
{
x: 1,
y: 2,
width: 3,
height: 4,
},
{
x: 5,
y: 6,
width: 7,
height: 8,
},
]);
setClientRects(childBRef.current, [{x: 9, y: 10, width: 11, height: 12}]);
const clientRects = fragmentRef.current.getClientRects();
expect(clientRects.length).toBe(3);
expect(clientRects[0].left).toBe(1);
expect(clientRects[1].left).toBe(5);
expect(clientRects[2].left).toBe(9);
});
});
describe('getRootNode', () => {
it('returns the root node of the parent', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<div>
<React.Fragment ref={fragmentRef}>
<div />
</React.Fragment>
</div>
);
}
await act(() => root.render(<Test />));
expect(fragmentRef.current.getRootNode()).toBe(document);
});
it('returns the topmost disconnected element if the fragment and parent are unmounted', async () => {
const containerRef = React.createRef();
const parentRef = React.createRef();
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test({mounted}) {
return (
<div ref={containerRef} id="container">
{mounted && (
<div ref={parentRef} id="parent">
<React.Fragment ref={fragmentRef}>
<div />
</React.Fragment>
</div>
)}
</div>
);
}
await act(() => root.render(<Test mounted={true} />));
expect(fragmentRef.current.getRootNode()).toBe(document);
const fragmentHandle = fragmentRef.current;
await act(() => root.render(<Test mounted={false} />));
expect(fragmentHandle.getRootNode()).toBe(fragmentHandle);
});
it('returns self when only the fragment was unmounted', async () => {
const fragmentRef = React.createRef();
const parentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test({mounted}) {
return (
<div ref={parentRef} id="parent">
{mounted && (
<React.Fragment ref={fragmentRef}>
<div />
</React.Fragment>
)}
</div>
);
}
await act(() => root.render(<Test mounted={true} />));
expect(fragmentRef.current.getRootNode()).toBe(document);
const fragmentHandle = fragmentRef.current;
await act(() => root.render(<Test mounted={false} />));
expect(fragmentHandle.getRootNode()).toBe(fragmentHandle);
});
});
describe('compareDocumentPosition', () => {
function expectPosition(position, spec) {
const positionResult = {
following: (position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0,
preceding: (position & Node.DOCUMENT_POSITION_PRECEDING) !== 0,
contains: (position & Node.DOCUMENT_POSITION_CONTAINS) !== 0,
containedBy: (position & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0,
disconnected: (position & Node.DOCUMENT_POSITION_DISCONNECTED) !== 0,
implementationSpecific:
(position & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) !== 0,
};
expect(positionResult).toEqual(spec);
}
it('returns the relationship between the fragment instance and a given node', async () => {
const fragmentRef = React.createRef();
const beforeRef = React.createRef();
const afterRef = React.createRef();
const middleChildRef = React.createRef();
const firstChildRef = React.createRef();
const lastChildRef = React.createRef();
const containerRef = React.createRef();
const disconnectedElement = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<div ref={containerRef}>
<div ref={beforeRef} />
<React.Fragment ref={fragmentRef}>
<div ref={firstChildRef} />
<div ref={middleChildRef} />
<div ref={lastChildRef} />
</React.Fragment>
<div ref={afterRef} />
</div>
);
}
await act(() => root.render(<Test />));
expectPosition(
fragmentRef.current.compareDocumentPosition(document.body),
{
preceding: true,
following: false,
contains: true,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(beforeRef.current),
{
preceding: true,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(afterRef.current),
{
preceding: false,
following: true,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(firstChildRef.current),
{
preceding: false,
following: false,
contains: false,
containedBy: true,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(middleChildRef.current),
{
preceding: false,
following: false,
contains: false,
containedBy: true,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(lastChildRef.current),
{
preceding: false,
following: false,
contains: false,
containedBy: true,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(containerRef.current),
{
preceding: true,
following: false,
contains: true,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(disconnectedElement),
{
preceding: false,
following: true,
contains: false,
containedBy: false,
disconnected: true,
implementationSpecific: true,
},
);
});
it('handles fragment instances with one child', async () => {
const fragmentRef = React.createRef();
const beforeRef = React.createRef();
const afterRef = React.createRef();
const containerRef = React.createRef();
const onlyChildRef = React.createRef();
const disconnectedElement = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<div id="container" ref={containerRef}>
<div>
<div ref={beforeRef} id="before" />
<React.Fragment ref={fragmentRef}>
<div ref={onlyChildRef} id="within" />
</React.Fragment>
<div id="after" ref={afterRef} />
</div>
</div>
);
}
await act(() => root.render(<Test />));
expectPosition(
fragmentRef.current.compareDocumentPosition(beforeRef.current),
{
preceding: true,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(afterRef.current),
{
preceding: false,
following: true,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(onlyChildRef.current),
{
preceding: false,
following: false,
contains: false,
containedBy: true,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(containerRef.current),
{
preceding: true,
following: false,
contains: true,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(disconnectedElement),
{
preceding: false,
following: true,
contains: false,
containedBy: false,
disconnected: true,
implementationSpecific: true,
},
);
});
it('handles empty fragment instances', async () => {
const fragmentRef = React.createRef();
const beforeParentRef = React.createRef();
const beforeRef = React.createRef();
const afterRef = React.createRef();
const afterParentRef = React.createRef();
const containerRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<>
<div id="before-container" ref={beforeParentRef} />
<div id="container" ref={containerRef}>
<div id="before" ref={beforeRef} />
<React.Fragment ref={fragmentRef} />
<div id="after" ref={afterRef} />
</div>
<div id="after-container" ref={afterParentRef} />
</>
);
}
await act(() => root.render(<Test />));
expectPosition(
fragmentRef.current.compareDocumentPosition(document.body),
{
preceding: true,
following: false,
contains: true,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(beforeRef.current),
{
preceding: true,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(beforeParentRef.current),
{
preceding: true,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(afterRef.current),
{
preceding: false,
following: true,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(afterParentRef.current),
{
preceding: false,
following: true,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(containerRef.current),
{
preceding: false,
following: false,
contains: true,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
});
it('returns disconnected for comparison with an unmounted fragment instance', async () => {
const fragmentRef = React.createRef();
const containerRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test({mount}) {
return (
<div ref={containerRef}>
{mount && (
<Fragment ref={fragmentRef}>
<div />
</Fragment>
)}
</div>
);
}
await act(() => root.render(<Test mount={true} />));
const fragmentHandle = fragmentRef.current;
expectPosition(
fragmentHandle.compareDocumentPosition(containerRef.current),
{
preceding: true,
following: false,
contains: true,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
await act(() => {
root.render(<Test mount={false} />);
});
expectPosition(
fragmentHandle.compareDocumentPosition(containerRef.current),
{
preceding: false,
following: false,
contains: false,
containedBy: false,
disconnected: true,
implementationSpecific: false,
},
);
});
describe('with portals', () => {
it('handles portaled elements', async () => {
const fragmentRef = React.createRef();
const portaledSiblingRef = React.createRef();
const portaledChildRef = React.createRef();
function Test() {
return (
<div>
{createPortal(<div ref={portaledSiblingRef} />, document.body)}
<Fragment ref={fragmentRef}>
{createPortal(<div ref={portaledChildRef} />, document.body)}
<div />
</Fragment>
</div>
);
}
const root = ReactDOMClient.createRoot(container);
await act(() => root.render(<Test />));
expectPosition(
fragmentRef.current.compareDocumentPosition(
portaledSiblingRef.current,
),
{
preceding: true,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(portaledChildRef.current),
{
preceding: false,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
});
it('handles multiple portals to the same element', async () => {
const root = ReactDOMClient.createRoot(container);
const fragmentRef = React.createRef();
const childARef = React.createRef();
const childBRef = React.createRef();
const childCRef = React.createRef();
function Test() {
const [c, setC] = React.useState(false);
React.useEffect(() => {
setC(true);
});
return (
<>
{createPortal(
<Fragment ref={fragmentRef}>
<div id="A" ref={childARef} />
{c ? <div id="C" ref={childCRef} /> : null}
</Fragment>,
document.body,
)}
{createPortal(<p id="B" ref={childBRef} />, document.body)}
</>
);
}
await act(() => root.render(<Test />));
expect(document.body.innerHTML).toBe(
'<div></div>' +
'<div id="A"></div>' +
'<p id="B"></p>' +
'<div id="C"></div>',
);
expectPosition(
fragmentRef.current.compareDocumentPosition(document.body),
{
preceding: true,
following: false,
contains: true,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(childARef.current),
{
preceding: false,
following: false,
contains: false,
containedBy: true,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(childBRef.current),
{
preceding: false,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(childCRef.current),
{
preceding: false,
following: false,
contains: false,
containedBy: true,
disconnected: false,
implementationSpecific: false,
},
);
});
it('handles empty fragments', async () => {
const fragmentRef = React.createRef();
const childARef = React.createRef();
const childBRef = React.createRef();
function Test() {
return (
<>
<div id="A" ref={childARef} />
{createPortal(<Fragment ref={fragmentRef} />, document.body)}
<div id="B" ref={childBRef} />
</>
);
}
const root = ReactDOMClient.createRoot(container);
await act(() => root.render(<Test />));
expectPosition(
fragmentRef.current.compareDocumentPosition(document.body),
{
preceding: true,
following: false,
contains: true,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(childARef.current),
{
preceding: true,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
expectPosition(
fragmentRef.current.compareDocumentPosition(childBRef.current),
{
preceding: true,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
});
});
});
});