import {
DataProvider,
getDefaultManagers,
useController,
useSuspense,
} from '@data-client/react';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { makeGetTodo, mockTodoState } from '__tests__/concurrentFixtures';
import {
expectAllCommitsPriority,
ImmediatePriority,
makeNotifyStore,
NormalPriority,
resetCommits,
} from '__tests__/reactCommitProbe';
import {
captureStream,
discoverPendingBoundary,
Gate,
getHydrateRoot,
holdDocumentLoading,
installShell,
makeGate,
pendingMarker,
recordConsoleErrors,
replayRest,
} from '__tests__/streamingHarness';
import React, {
startTransition,
Suspense,
useState,
useSyncExternalStore,
version,
} from 'react';
const LegacyReact = version.startsWith('16') || version.startsWith('17');
const describeConcurrent = LegacyReact ? describe.skip : describe;
const isReact18 = version.startsWith('18');
const HYDRATION_UPDATE_BEFORE_FINISH =
'This Suspense boundary received an update before it finished hydrating';
function isHydrationUpdateWarning(args: unknown[]): boolean {
return args.some(arg => {
const text =
typeof arg === 'string' ? arg
: arg instanceof Error ? arg.message
: '';
return text.includes(HYDRATION_UPDATE_BEFORE_FINISH);
});
}
/** Drain React 18's intended client-render warning so suite afterEach still requires a clean console. */
function takeHydrationUpdateWarnings(unexpected: unknown[][]): unknown[][] {
const taken = unexpected.filter(isHydrationUpdateWarning);
unexpected.splice(
0,
unexpected.length,
...unexpected.filter(args => !isHydrationUpdateWarning(args)),
);
return taken;
}
const fetchTodo = jest.fn(({ id }: { id: string }) =>
Promise.resolve({ id, title: `todo ${id}` }),
);
const getTodo = makeGetTodo(fetchTodo);
const stateAB = mockTodoState(getTodo, ['A', 'B']);
function TodoView({ id }: { id: string }) {
const todo = useSuspense(getTodo, { id });
return
{todo.title}
;
}
describeConcurrent('concurrent probe controls', () => {
let consoleRecorder: ReturnType;
beforeAll(() => {
consoleRecorder = recordConsoleErrors();
});
beforeEach(() => {
consoleRecorder.unexpected.length = 0;
resetCommits();
fetchTodo.mockClear();
});
afterEach(() => {
expect(consoleRecorder.unexpected).toEqual([]);
});
afterAll(() => {
consoleRecorder.restore();
});
it('a subscribed useSyncExternalStore store commits at Immediate priority, even inside startTransition', () => {
const store = makeNotifyStore();
function Consumer() {
const value = useSyncExternalStore(store.subscribe, store.getSnapshot);
return {value};
}
const { getByTestId } = render();
resetCommits();
act(() => {
store.notify();
});
expect(getByTestId('uses-value').textContent).toBe('1');
expectAllCommitsPriority(
'uSES notify was not Immediate',
ImmediatePriority,
);
resetCommits();
act(() => {
startTransition(() => {
store.notify();
});
});
expect(getByTestId('uses-value').textContent).toBe('2');
expectAllCommitsPriority(
'uSES notify inside startTransition was not Immediate',
ImmediatePriority,
);
});
it('a plain state update outside an event commits at Normal priority', async () => {
let setX: (n: number) => void = () => {};
function Sample() {
const [x, set] = useState(0);
setX = set;
return {x};
}
render();
resetCommits();
await act(async () => {
await Promise.resolve();
setX(1);
});
expect(document.querySelector('[data-testid="x"]')?.textContent).toBe('1');
expectAllCommitsPriority('plain setState was not Normal', NormalPriority);
});
it('a store publish from a discrete click handler is Immediate by React design', () => {
function ClickPublish() {
const controller = useController();
return (
);
}
const { getByText, getByTestId } = render(
loading A}>
,
);
resetCommits();
act(() => {
fireEvent.click(getByText('publish'));
});
expect(getByTestId('todo-A').textContent).toBe('todo A v2');
expectAllCommitsPriority(
'discrete setResponse was not Immediate',
ImmediatePriority,
);
});
describe('recreating Suspense elements above a pending boundary', () => {
let container: HTMLDivElement;
let finishDocument: (() => void) | undefined;
let prevActEnv: boolean | undefined;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
prevActEnv = (globalThis as any).IS_REACT_ACT_ENVIRONMENT;
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = false;
});
afterEach(() => {
finishDocument?.();
finishDocument = undefined;
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = prevActEnv;
container.remove();
});
it('client-renders the pending sibling so the streamed node is not adopted', async () => {
const serverGate = makeGate();
const clientGate = makeGate();
let bump = () => {};
function RecreatingTree({ gate }: { gate: ReturnType }) {
const [tick, setTick] = useState(0);
bump = () => setTick(t => t + 1);
return (
<>
loading A}>
loading B}>
>
);
}
function Page({ gate }: { gate: ReturnType }) {
return (
);
}
const { shell, rest } = await captureStream(, {
releaseAfterShell: [() => serverGate.release()],
});
installShell(container, shell);
const pendingB = discoverPendingBoundary(container, 'loading B');
finishDocument = holdDocumentLoading();
const root = getHydrateRoot()(container, );
await waitFor(() => {
expect(
container.querySelector('[data-testid="todo-A"]'),
).not.toBeNull();
});
expect(pendingMarker(container, pendingB)).toBe(true);
act(() => {
bump();
});
await waitFor(() => {
expect(pendingMarker(container, pendingB)).toBe(false);
});
expect(container.textContent).toContain('loading B');
const bStreamedNode = replayRest(container, rest, 'todo-B');
expect(bStreamedNode).toBeDefined();
const visibleB = container.querySelector('[data-testid="todo-B"]');
expect(visibleB === bStreamedNode).toBe(false);
root.unmount();
if (isReact18) {
// Intended client-render control on 18; other suites still require an empty console.
expect(
takeHydrationUpdateWarnings(consoleRecorder.unexpected).length,
).toBeGreaterThan(0);
}
});
});
});