import { act, cleanup, render, screen } from '@testing-library/react'; import * as React from 'react'; import { afterEach, describe, expect, it } from 'vitest'; import { Freeze } from './freeze'; afterEach(() => { cleanup(); }); describe('Freeze', () => { it('renders children while not frozen', () => { render(Acme); expect(screen.getByText('Acme')).toBeInTheDocument(); }); it('holds the committed DOM when children change while frozen', () => { const { rerender } = render(Acme); rerender(Globex); expect(screen.getByText('Acme')).toBeInTheDocument(); expect(screen.queryByText('Globex')).toBeNull(); }); it('keeps the held DOM visible', () => { const { rerender } = render(Acme); rerender(Globex); expect(screen.getByText('Acme')).toBeVisible(); }); it('keeps the held DOM visible across further updates while frozen', () => { const { rerender } = render(Acme); rerender(Globex); rerender(Initech); expect(screen.getByText('Acme')).toBeVisible(); }); it('commits the pending children once unfrozen', () => { const { rerender } = render(Acme); rerender(Globex); rerender(Globex); expect(screen.getByText('Globex')).toBeInTheDocument(); expect(screen.queryByText('Acme')).toBeNull(); }); it('holds a state update raised by the subtree itself', () => { let bump = () => {}; function Counter() { const [count, setCount] = React.useState(0); bump = () => setCount(n => n + 1); return count: {count}; } const { rerender } = render( , ); rerender( , ); act(() => bump()); expect(screen.getByText('count: 0')).toBeInTheDocument(); rerender( , ); expect(screen.getByText('count: 1')).toBeInTheDocument(); }); it('holds a context change read from inside the frozen subtree', () => { const NameContext = React.createContext('Acme'); function Reader() { return {React.useContext(NameContext)}; } const { rerender } = render( , ); rerender( , ); expect(screen.getByText('Acme')).toBeInTheDocument(); rerender( , ); expect(screen.getByText('Globex')).toBeInTheDocument(); }); describe('inside a transition', () => { function Harness() { const [frozen, setFrozen] = React.useState(false); const [label, setLabel] = React.useState('Acme'); return ( <> {frozen ? 'frozen' : 'live'} {label} ); } it('holds the frame from before the flip, and lets the transition commit around it', async () => { render(); await act(() => screen.getByText('freeze').click()); expect(screen.getByRole('status').textContent).toBe('frozen'); expect(screen.getByText('Acme')).toBeInTheDocument(); expect(screen.queryByText('Globex')).toBeNull(); await act(() => screen.getByText('relabel').click()); expect(screen.getByText('Acme')).toBeInTheDocument(); expect(screen.queryByText('Initech')).toBeNull(); await act(() => screen.getByText('thaw').click()); expect(screen.getByText('Initech')).toBeInTheDocument(); }); }); describe('under scheduling', () => { function Child({ label, onEffect }: { label: string; onEffect: (label: string) => void }) { React.useEffect(() => { onEffect(label); }, [label, onEffect]); return {label}; } function Scheduled({ onEffect, strict = false }: { onEffect?: (label: string) => void; strict?: boolean }) { const [frozen, setFrozen] = React.useState(false); const [label, setLabel] = React.useState('Acme'); const [tick, setTick] = React.useState(0); const noteEffect = React.useCallback((value: string) => onEffect?.(value), [onEffect]); const tree = ( <> {frozen ? 'frozen' : 'live'}:{tick} ); return strict ? {tree} : tree; } const click = (name: string) => act(() => screen.getByText(name).click()); const status = () => screen.getByRole('status').textContent; it('does not run the effects of the frame it holds back', async () => { const effects: string[] = []; render( effects.push(label)} />); effects.length = 0; await click('freeze later'); expect(status()).toBe('frozen:0'); expect(screen.getByText('Acme')).toBeInTheDocument(); expect(effects).toEqual([]); await click('thaw later'); expect(screen.getByText('Globex')).toBeInTheDocument(); expect(effects).toEqual(['Globex']); }); it('releases a hold whose transition was thawed before it committed', async () => { render(); await act(async () => { screen.getByText('freeze later').click(); await Promise.resolve(); screen.getByText('thaw now').click(); }); expect(status()).toBe('live:0'); expect(screen.getByText('Globex')).toBeVisible(); }); it('keeps the transition alive through an unrelated sync update while it is pending', async () => { render(); await act(async () => { screen.getByText('freeze later').click(); await Promise.resolve(); screen.getByText('tick').click(); }); expect(status()).toBe('frozen:1'); expect(screen.getByText('Acme')).toBeInTheDocument(); expect(screen.queryByText('Globex')).toBeNull(); await click('thaw later'); expect(screen.getByText('Globex')).toBeVisible(); }); it('holds the frame committed by a thaw when frozen again right after it', async () => { render(); await click('freeze now'); await click('thaw now'); await click('relabel now'); expect(screen.getByText('Initech')).toBeVisible(); await click('freeze now'); await click('freeze later'); expect(status()).toBe('frozen:0'); expect(screen.getByText('Initech')).toBeInTheDocument(); expect(screen.queryByText('Globex')).toBeNull(); }); it('does not commit a thaw that is re-frozen in the same update', async () => { render(); await click('freeze now'); await act(() => { screen.getByText('thaw now').click(); screen.getByText('relabel now').click(); screen.getByText('freeze now').click(); }); expect(status()).toBe('frozen:0'); expect(screen.getByText('Acme')).toBeInTheDocument(); expect(screen.queryByText('Initech')).toBeNull(); }); it('holds the frame from before a transition flip under StrictMode too', async () => { render(); await click('freeze later'); expect(status()).toBe('frozen:0'); expect(screen.getByText('Acme')).toBeInTheDocument(); expect(screen.queryByText('Globex')).toBeNull(); await click('thaw now'); expect(screen.getByText('Globex')).toBeVisible(); }); it('can unmount while holding', async () => { const { unmount } = render(); await click('freeze later'); expect(() => unmount()).not.toThrow(); }); it('keeps an inner hold while the outer thaws', async () => { function Nested() { const [outer, setOuter] = React.useState(false); const [inner, setInner] = React.useState(false); const [label, setLabel] = React.useState('Acme'); return ( <> {label} ); } render(); await click('freeze inner'); await click('freeze outer'); await click('thaw outer and relabel'); expect(screen.getByText('Acme')).toBeInTheDocument(); expect(screen.queryByText('Globex')).toBeNull(); }); }); });