[test] Refactor Pickers tests to async user-event#22043
[test] Refactor Pickers tests to async user-event#22043LukasTy wants to merge 15 commits intomui:masterfrom
Conversation
Reapply the still-relevant parts of mui#14583 on top of the Vitest migration. The fake-clock removal is already in master; what remained was converting tests to async + `@testing-library/user-event`, refactoring the shared helpers, and dropping the deprecated sync variants. Infrastructure changes in `test/utils/pickers/`: - `fields.tsx`: removed sync `selectSection`, `pressKey`, `testFieldKeyPress`, and `testFieldChange`; the former `*Async` variants are now the canonical ones. `pressKey` maps navigation keys (`ArrowUp`, `Backspace`, `Home`, ...) to user-event's curly-brace syntax, treats `' '` as `{Space}`, and treats `''` (the legacy "clear section" signal) as `{Backspace}`. - `openPicker.ts`: removed sync variant; `openPicker(user, params)` is async. A small `clickTarget` helper falls back to `fireEvent.click` when user-event refuses to interact with `pointer-events: none` so `disabled` / `readOnly` open tests still work. - `viewHandlers.ts`: `timeClockHandler`, `digitalClockHandler`, and `multiSectionDigitalClockHandler` are now async and receive `user` as the first argument. `fireTouchChangedEvent` calls stay (no user-event equivalent for multi-touch) but are wrapped in `act(async () => ...)`. - `assertions.ts`: `expectPickerChangeHandlerValue(type, value, expected)` now takes the raw value instead of a sinon spy, matching the original PR's signature. - `describeValue/*`: `setNewValue` callbacks receive `{ user, selectSection, pressKey, ... }` and return `Promise<TValue>`. All four `test*.tsx` helpers (`testPickerOpenCloseLifeCycle`, `testPickerActionBar`, `testShortcuts`, `testControlledUnControlled`) migrated to `await user.click` / `user.keyboard`. - `describePicker.tsx`: two slot-forwarding tests migrated to `user.click`. Test files (~65 files across x-date-pickers and x-date-pickers-pro): all `fireEvent.*` / `fireUserEvent.*` / sync helper calls replaced with `await user.*` equivalents, tests made `async`. `fireEvent.click` kept for a small number of places where the exact synthetic-event sequence matters: - performance tests asserting render counts (user-event fires a fuller pointer sequence that triggers extra renders), - click-on-disabled-element tests where `user.click` refuses `pointer-events: none`, - drag-and-drop and touch helpers (no user-event equivalent). `useFieldRootProps.ts` had a latent select-all bug: the handler was checking `String.fromCharCode(event.keyCode) === 'A'`, but user-event v14 does not set the deprecated `keyCode` property. Added `event.key?.toUpperCase() === 'A'` as the primary check so Ctrl+A triggers select-all regardless of how the keyboard event was dispatched. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Deploy preview: https://deploy-preview-22043--material-ui-x.netlify.app/ Bundle size report
|
Pure formatting pass on top of the async/user-event refactor — no test behavior changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the "Open previous view" or "Open next view" button is clicked and the resulting state update disables that same button, focus stays on the now-disabled element. That traps keyboard events on an unreachable target, and in practice swallows the picker's Escape-to-dismiss handler — it was only observable because the refactored MobileTimePicker test needed a `user.click(dialog)` workaround to move focus off the disabled button before pressing Escape. Track the previous disabled state via a ref and, when a button transitions from enabled to disabled while it still owns `document.activeElement`, move focus to its still-enabled sibling (or blur as a fallback when both sides are disabled). Refs are attached via `useForkRef` so existing slot-level ref overrides keep working. This fixes the underlying issue so the test-level workaround in `describeValue.MobileTimePicker.test.tsx` is no longer needed, and it improves accessibility in real browsers too: keyboard users that press the arrow switcher land on a sensible focus target instead of relying on the browser's implicit blur-on-disable behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude Opus 4.6 reviewOverallWell-structured migration — 81 files, +1.6k/-1.6k lines, and the diff is largely mechanical. The approach is sound: consolidate on Component Fix (useFieldRootProps.ts)The Test Utilitiesfields.tsx — Clean consolidation. The openPicker.ts — The const clickTarget = async (user: MuiRenderResult['user'], target: Element) => {
const style = getComputedStyle(target);
if (style.pointerEvents === 'none') {
fireEvent.click(target);
return;
}
await user.click(target);
};This avoids string-matching on error messages which could break on user-event version bumps. viewHandlers.ts — Wrapping assertions.ts — Good change to decouple from sinon's spy API. Passing raw values makes the helper library-agnostic. describePicker:
|
Summary
Reapply the still-relevant parts of #14583 on top of the Vitest migration. The fake-clock removal is already in master; what remained was:
asyncand driving them with@testing-library/user-eventinstead of the syncfireEvent/fireUserEvent.test/utils/pickers/to expose only async APIs.useFieldRootPropsbug that preventedCtrl+Aselect-all from working when the keyboard event was dispatched via user-event (because the handler checked the deprecatedevent.keyCode).Infrastructure changes (
test/utils/pickers/)fields.tsx— removed the syncselectSection,pressKey,testFieldKeyPress, andtestFieldChangehelpers; the former*Asyncvariants are now the canonical ones.pressKeymaps navigation keys (ArrowUp,Backspace,Home, ...) to user-event's curly-brace syntax, treats' 'as{Space}, and treats''(the legacy "clear section" signal) as{Backspace}.openPicker.ts— removed the sync variant;openPicker(user, params)is now async. A smallclickTargethelper falls back tofireEvent.clickwhen user-event refuses to interact withpointer-events: nonesodisabled/readOnlyopen tests still work.viewHandlers.ts—timeClockHandler,digitalClockHandler, andmultiSectionDigitalClockHandlerare async and receiveuseras the first argument.fireTouchChangedEventcalls stay (no user-event equivalent for multi-touch) but are wrapped inact(async () => ...).assertions.ts—expectPickerChangeHandlerValue(type, value, expected)now takes the raw value instead of a sinon spy, matching the original PR's signature.describeValue/*—setNewValuecallbacks receive{ user, selectSection, pressKey, ... }and returnPromise<TValue>. All fourtest*.tsxhelpers (testPickerOpenCloseLifeCycle,testPickerActionBar,testShortcuts,testControlledUnControlled) migrated toawait user.click/user.keyboard.describePicker.tsx— two slot-forwarding tests migrated touser.click.Test files (~65 files across
x-date-pickersandx-date-pickers-pro)All
fireEvent.*/fireUserEvent.*/ sync helper calls replaced withawait user.*equivalents, tests madeasync.fireEvent.clickis kept for a small number of places where the exact synthetic-event sequence matters:user.clickfires a fuller pointer sequence (pointerover,pointerdown,pointerup, ...) that triggers extra hover/focus renders.user.clickrefusespointer-events: none, so the click is ignored before the assertion can run.dragstart/dropor multi-touch.Each such exception has an inline comment explaining why.
Component fix
useFieldRootProps.tshad a latent select-all bug: the handler was checkingString.fromCharCode(event.keyCode) === 'A', but user-event v14 doesn't set the deprecatedkeyCodeproperty. This made everyCtrl+Aselect-all test silently no-op when driven by user-event. Addedevent.key?.toUpperCase() === 'A'as the primary check, with the oldkeyCodepath kept as a fallback, soCtrl+Atriggers select-all regardless of how the keyboard event was dispatched.Why now
The original PR was parked waiting for the move away from mocha. That move is done (the repo runs on vitest now), and the flakiness introduced by sinon's fake clocks is gone. The async/user-event portion is the remaining value from the original PR: more realistic test interactions and no more sync
fireEvent/fireUserEventnoise in Pickers tests.Test plan
pnpm --filter "@mui/x-date-pickers*" run typescriptpnpm test:unit --project "x-date-pickers" --run— 3241 passed, 814 skippedpnpm test:unit --project "x-date-pickers-pro" --run— 986 passed, 299 skipped, 2 todopnpm eslintpnpm prettierfireUserEvent,selectSectionAsync,pressKeyAsync,openPickerAsync,clock: 'fake'in Pickers sourcesDraft because the diff is large (~81 files, +1.7k/-1.7k) and the exceptions above would benefit from a review pass. Happy to split into smaller commits if preferred.
🤖 Generated with Claude Code