'use strict';
let React;
let ReactDOMClient;
let act;
let container;
let Fragment;
let Activity;
describe('FragmentRefs', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
Fragment = React.Fragment;
Activity = React.unstable_Activity;
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;
container = document.createElement('div');
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()', () => {
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('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('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);
function Wrapper({children}) {
return children;
}
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([]);
});
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']);
});
});
});
});