/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails reactcore * @jest-environment node */ 'use strict'; let JSDOM; let React; let ReactDOMClient; let act; let document; let Fragment; let Node; describe('FragmentRefs', () => { beforeEach(() => { jest.resetModules(); JSDOM = require('jsdom'); React = require('react'); Fragment = React.Fragment; ReactDOMClient = require('react-dom/client'); act = require('internal-test-utils').act; const jsdom = new JSDOM.JSDOM(''); document = jsdom.window.document; Node = jsdom.window.Node; global.window = jsdom.window; global.document = global.window.document; global.navigator = global.window.navigator; global.Event = global.window.Event; global.MouseEvent = global.window.MouseEvent; global.Node = Node; }); describe('focus methods', () => { describe('blur()', () => { // @gate enableFragmentRefs it('throws when the nearest host parent is a Document container', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(document); await act(() => { root.render( A , ); }); await act(() => { // focus() would stop at , which is a child of the fragment // and usually already the activeElement. document.getElementById('child-a').focus(); }); expect(document.activeElement.id).toEqual('child-a'); await act(() => { fragmentRef.current.blur(); }); expect(document.activeElement).toEqual(document.body); }); }); }); describe('events', () => { describe('dispatchEvent()', () => { // @gate enableFragmentRefs it('fires events when the fragment is a child of a HostSingleton in a document root', async () => { const fragmentRef = React.createRef(); const bodyRef = React.createRef(); const root = ReactDOMClient.createRoot(document); await act(() => { root.render( , ); }); const fragmentListener = jest.fn(); fragmentRef.current.addEventListener('custom', fragmentListener); const bodyListener = jest.fn(); bodyRef.current.addEventListener('custom', bodyListener); // The is the fragment's host parent, so the // temporary event target is appended there. fragmentRef.current.dispatchEvent(new Event('custom', {bubbles: true})); expect(fragmentListener).toHaveBeenCalledTimes(1); expect(bodyListener).toHaveBeenCalledTimes(1); }); // @gate enableFragmentRefs it('dispatches to its own listeners when the container is a Document', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(document); const logs = []; await act(() => { root.render( <>
, ); }); fragmentRef.current.addEventListener('click', () => { logs.push('fragment'); }); document.addEventListener('click', () => { logs.push('document'); }); const isCancelable = !fragmentRef.current.dispatchEvent( new MouseEvent('click', {bubbles: true}), ); expect(logs).toEqual(['fragment', 'document']); expect(isCancelable).toBe(false); }); // @gate enableFragmentRefs it('does not propagate through its own children when wrapping documentElement', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(document); const logs = []; await act(() => { root.render(
, ); }); // This also registers the listener on the child. Because the // fragment's position is a sibling of , the event must not // propagate through it and fire the listener a second time. fragmentRef.current.addEventListener('click', () => { logs.push('fragment'); }); document.addEventListener('click', () => { logs.push('document'); }); fragmentRef.current.dispatchEvent( new MouseEvent('click', {bubbles: true}), ); expect(logs).toEqual(['fragment', 'document']); }); // @gate enableFragmentRefs it('dispatches non-bubbling events when the container is a Document', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(document); const logs = []; await act(() => { root.render( <>
, ); }); document.addEventListener('click', () => { logs.push('document'); }); const isCancelable = !fragmentRef.current.dispatchEvent( new MouseEvent('click', {bubbles: false}), ); expect(logs).toEqual([]); expect(isCancelable).toBe(false); }); }); describe('addEventListener()', () => { // @gate enableFragmentRefs it('attaches listeners to the host children inside singletons', async () => { const fragmentRef = React.createRef(); const childRef = React.createRef(); const root = ReactDOMClient.createRoot(document); await act(() => { root.render(
, ); }); const currentTargets = []; fragmentRef.current.addEventListener('click', event => { currentTargets.push(event.currentTarget); }); childRef.current.dispatchEvent(new Event('click', {bubbles: true})); // The singleton is the fragment's child, so the listener is // attached there and receives the bubbling event. expect(currentTargets).toEqual([document.documentElement]); }); // @gate enableFragmentRefs it('attaches listeners to a singleton mounted into the fragment, but not to its content', async () => { const fragmentRef = React.createRef(); const childRef = React.createRef(); const root = ReactDOMClient.createRoot(document); function Test({showShell}) { return ( {showShell && (
)} ); } await act(() => { root.render(); }); const currentTargets = []; fragmentRef.current.addEventListener('click', event => { currentTargets.push(event.currentTarget); }); await act(() => { root.render(); }); childRef.current.dispatchEvent(new Event('click', {bubbles: true})); // The placed singleton receives the fragment's listener as a // new child. Its content is not attributed to the fragment, so the // event only fires once when it bubbles to . expect(currentTargets).toEqual([document.documentElement]); }); // @gate enableFragmentRefs it('attributes new children inside a singleton to fragments below it, not above it', async () => { const outerFragmentRef = React.createRef(); const innerFragmentRef = React.createRef(); const lateChildRef = React.createRef(); const root = ReactDOMClient.createRoot(document); function Test({showLateChild}) { return (
{showLateChild && } ); } await act(() => { root.render(); }); const outerCurrentTargets = []; outerFragmentRef.current.addEventListener('click', event => { outerCurrentTargets.push(event.currentTarget); }); const innerCurrentTargets = []; innerFragmentRef.current.addEventListener('click', event => { innerCurrentTargets.push(event.currentTarget); }); await act(() => { root.render(); }); lateChildRef.current.dispatchEvent(new Event('click', {bubbles: true})); // The inner fragment owns the new child directly and attaches its // listener on insertion. The outer fragment's child is the // singleton, so the new child inside is not attributed to it // and its listener only fires once via bubbling. expect(innerCurrentTargets).toEqual([lateChildRef.current]); expect(outerCurrentTargets).toEqual([document.documentElement]); }); }); }); describe('getClientRects()', () => { // @gate enableFragmentRefs it('measures the host children inside singletons', async () => { const fragmentRef = React.createRef(); const childRef = React.createRef(); const root = ReactDOMClient.createRoot(document); await act(() => { root.render(
, ); }); childRef.current.getClientRects = jest.fn(() => ['child-rect']); document.documentElement.getClientRects = jest.fn(() => ['html-rect']); // The singleton is the fragment's child, so it is measured // instead of the elements inside it expect(fragmentRef.current.getClientRects()).toEqual(['html-rect']); }); }); 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); } // @gate enableFragmentRefs it('treats documentElement as containing the fragment', async () => { const fragmentRef = React.createRef(); const container = document.createElement('div'); document.body.appendChild(container); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
, ); }); expectPosition( fragmentRef.current.compareDocumentPosition(document.documentElement), { preceding: true, following: false, contains: true, containedBy: false, disconnected: false, implementationSpecific: false, }, ); }); }); });