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 (
<>
{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 = (
<>
>
);
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();
});
});
});