Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions packages/react/src/checkbox/root/CheckboxRoot.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { expect, vi, describe, it } from 'vitest';
import * as React from 'react';
import { act, fireEvent, screen, waitFor } from '@mui/internal-test-utils';
import { act, fireEvent, screen, waitFor, reactMajor } from '@mui/internal-test-utils';
import { Checkbox } from '@base-ui/react/checkbox';
import { CheckboxGroup } from '@base-ui/react/checkbox-group';
import { Field } from '@base-ui/react/field';
import { Form } from '@base-ui/react/form';
import { createRenderer, describeConformance, isJSDOM } from '#test-utils';
import { createRenderer, createCommitPhases, describeConformance, isJSDOM } from '#test-utils';

describe('<Checkbox.Root />', () => {
const { render, renderToString } = createRenderer();
Expand Down Expand Up @@ -1790,4 +1790,34 @@ describe('<Checkbox.Root />', () => {
await user.click(checkbox);
expect(checkbox).toHaveAttribute('aria-checked', 'false');
});

// React 19 only: the recorded sequences are specific to its scheduling, and the legacy React
// workflow re-runs this whole suite against React 18.
describe.skipIf(isJSDOM || reactMajor < 19)('render counts', () => {
const rows = Array.from({ length: 10 }, (_, index) => index);

it('mounting 10 instances', async () => {
const phases = createCommitPhases();

await renderNonStrict(
phases.wrap(
<div>
{rows.map((row) => (
<Checkbox.Root key={row} aria-label={`Checkbox ${row + 1}`}>
<Checkbox.Indicator />
</Checkbox.Root>
))}
</div>,
),
);

await phases.waitForQuiescence();

expect(phases.get()).toMatchInlineSnapshot(`
[
"mount",
]
`);
});
});
});
94 changes: 93 additions & 1 deletion packages/react/src/combobox/root/ComboboxRoot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ignoreActWarnings,
reactMajor,
} from '@mui/internal-test-utils';
import { createRenderer, isJSDOM, popupConformanceTests } from '#test-utils';
import { createRenderer, createCommitPhases, isJSDOM, popupConformanceTests } from '#test-utils';
import { Combobox } from '@base-ui/react/combobox';
import { Autocomplete } from '@base-ui/react/autocomplete';
import { DirectionProvider } from '@base-ui/react/direction-provider';
Expand Down Expand Up @@ -13207,4 +13207,96 @@ describe('<Combobox.Root />', () => {
expect(input).toHaveFocus();
});
});

// React 19 only: the recorded sequences are specific to its scheduling, and the legacy React
// workflow re-runs this whole suite against React 18.
describe.skipIf(isJSDOM || reactMajor < 19)('render counts', () => {
const { render: renderNonStrict } = createRenderer({ strict: false });
// Referentially stable across renders, so `React.memo` on the items can legitimately bail:
// what makes them re-render per keystroke is whether they subscribe to a context that changes.
const items = Array.from({ length: 20 }, (_, index) => `Row ${index + 1}`);

function OpenCombobox() {
return (
<Combobox.Root items={items} defaultOpen>
<Combobox.Input data-testid="combobox-input" />
<Combobox.List>
{(item: string) => (
<Combobox.Item key={item} value={item}>
{item}
</Combobox.Item>
)}
</Combobox.List>
</Combobox.Root>
);
}

it('mounting an open list of 20 items', async () => {
const phases = createCommitPhases();

await renderNonStrict(phases.wrap(<OpenCombobox />));
await phases.waitForQuiescence();

expect(phases.get()).toMatchInlineSnapshot(`
[
"mount",
"update",
"nested-update",
"nested-update",
]
`);
});

it('typing a prefix that keeps every item mounted', async () => {
const phases = createCommitPhases();

const { user } = await renderNonStrict(phases.wrap(<OpenCombobox />));
await phases.waitForQuiescence();
phases.reset();

// Every item matches "Row ", so none unmount: this isolates the per-keystroke re-render
// cost of items that are already on screen.
await user.type(screen.getByTestId('combobox-input'), 'Row ');
await phases.waitForQuiescence();

expect(phases.get()).toMatchInlineSnapshot(`
[
"update",
"nested-update",
"update",
"nested-update",
"update",
"nested-update",
"update",
]
`);
});

it('typing a query that narrows the list', async () => {
const phases = createCommitPhases();

const { user } = await renderNonStrict(phases.wrap(<OpenCombobox />));
await phases.waitForQuiescence();
phases.reset();

// Narrows 20 items down to "Row 2" and "Row 20", mixing unmount cost with re-renders of
// the items that survive.
await user.type(screen.getByTestId('combobox-input'), 'Row 2');
await phases.waitForQuiescence();

expect(phases.get()).toMatchInlineSnapshot(`
[
"update",
"nested-update",
"update",
"nested-update",
"update",
"nested-update",
"update",
"update",
"nested-update",
]
`);
});
});
});
58 changes: 56 additions & 2 deletions packages/react/src/dialog/root/DialogRoot.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { expect, vi, describe, beforeEach, it, afterEach } from 'vitest';
import type { CDPSession } from '@vitest/browser-playwright';
import * as React from 'react';
import { act, fireEvent, screen, waitFor, flushMicrotasks } from '@mui/internal-test-utils';
import {
act,
fireEvent,
screen,
waitFor,
flushMicrotasks,
reactMajor,
} from '@mui/internal-test-utils';
import { AlertDialog } from '@base-ui/react/alert-dialog';
import { Dialog } from '@base-ui/react/dialog';
import { createRenderer, isJSDOM, popupConformanceTests, wait } from '#test-utils';
import {
createRenderer,
createCommitPhases,
isJSDOM,
popupConformanceTests,
wait,
} from '#test-utils';
import { Menu } from '@base-ui/react/menu';
import { Select } from '@base-ui/react/select';
import { NumberField } from '@base-ui/react/number-field';
Expand Down Expand Up @@ -2241,6 +2254,47 @@ describe('<Dialog.Root />', () => {
expect(field).not.toHaveFocus();
},
);

// React 19 only: the recorded sequences are specific to its scheduling, and the legacy React
// workflow re-runs this whole suite against React 18.
describe.skipIf(isJSDOM || reactMajor < 19)('render counts', () => {
const { render: renderNonStrict } = createRenderer({ strict: false });
const rows = Array.from({ length: 10 }, (_, index) => index);

it('mounting 10 instances', async () => {
const phases = createCommitPhases();

await renderNonStrict(
phases.wrap(
<div>
{rows.map((row) => (
<Dialog.Root key={row}>
<Dialog.Trigger aria-label={`Open Dialog ${row + 1}`}>
Dialog {row + 1}
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Popup>
<Dialog.Title>Dialog {row + 1}</Dialog.Title>
<Dialog.Description>Dialog content</Dialog.Description>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
))}
</div>,
),
);

await phases.waitForQuiescence();

expect(phases.get()).toMatchInlineSnapshot(`
[
"mount",
]
`);
});
});
});

// The viewport takes its overflow from <html>, falling back to <body> when <html> doesn't
Expand Down
88 changes: 88 additions & 0 deletions packages/react/src/menu/root/MenuRoot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ignoreActWarnings,
screen,
waitFor,
reactMajor,
} from '@mui/internal-test-utils';
import { DirectionProvider } from '@base-ui/react/direction-provider';
import { useRefWithInit } from '@base-ui/utils/useRefWithInit';
Expand All @@ -18,6 +19,7 @@ import { platform } from '@base-ui/utils/platform';
import userEvent from '@testing-library/user-event';
import {
createRenderer,
createCommitPhases,
enterWithMouse,
isJSDOM,
moveMouse,
Expand Down Expand Up @@ -3185,6 +3187,92 @@ describe('<Menu.Root />', () => {
expect(screen.queryByRole('menuitem', { name: 'Add to Library' })).toHaveFocus();
});
});

// React 19 only: the recorded sequences are specific to its scheduling, and the legacy React
// workflow re-runs this whole suite against React 18.
describe.skipIf(isJSDOM || reactMajor < 19)('render counts', () => {
const { render: renderNonStrict } = createRenderer({ strict: false });
const rows = Array.from({ length: 10 }, (_, index) => index);
const items = Array.from({ length: 5 }, (_, index) => index);

it('mounting 10 instances', async () => {
const phases = createCommitPhases();

await renderNonStrict(
phases.wrap(
<div>
{rows.map((row) => (
<Menu.Root key={row}>
<Menu.Trigger aria-label={`Open Menu ${row + 1}`}>Menu {row + 1}</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner sideOffset={8}>
<Menu.Popup>
{items.map((item) => (
<Menu.Item key={item}>Menu item {item + 1}</Menu.Item>
))}
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
))}
</div>,
),
);

await phases.waitForQuiescence();

expect(phases.get()).toMatchInlineSnapshot(`
[
"mount",
]
`);
});

it('opening a menu of 20 items', async () => {
const phases = createCommitPhases();
const menuItems = Array.from({ length: 20 }, (_, index) => index);

const { user } = await renderNonStrict(
phases.wrap(
<Menu.Root>
<Menu.Trigger data-testid="open-menu">Open menu</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner sideOffset={8} positionMethod="fixed">
<Menu.Popup>
{menuItems.map((item) => (
<Menu.Item key={item}>Menu item {item + 1}</Menu.Item>
))}
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>,
),
);

await phases.waitForQuiescence();
phases.reset();

await user.click(screen.getByTestId('open-menu'));
await screen.findByRole('menu');
await phases.waitForQuiescence();

expect(phases.get()).toMatchInlineSnapshot(`
[
"update",
"update",
"update",
"nested-update",
"nested-update",
"nested-update",
"nested-update",
"nested-update",
"update",
"update",
"nested-update",
]
`);
});
});
});

function ContainedTriggerMenu(props: TestMenuProps) {
Expand Down
Loading
Loading