From 7c539295261debc24c24167eba022cdd3597bb7d Mon Sep 17 00:00:00 2001 From: Janpot <2109932+Janpot@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:00:49 +0200 Subject: [PATCH] [test] Fix react@18/next nightly CI matrix failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_regressions: the 'A11y results committed?' check ran an unscoped git diff, but the react@17/18/next jobs pin React via package-overrides, which dirties package.json/pnpm-lock.yaml. Exclude those two files so the check still verifies the a11y results on every job. - ListItem: type slotProps.root as SlotProps so it accepts a callback (matches the conformance slotProps-callback test); regenerated propTypes + API docs. - Tab: the conformance theme tests wrap the element in a provider; slot Tabs inside that wrapper so Tabs clones the Tab(s), not the provider. Cloning the provider with Tab-internal props (fullWidth, indicator, …) tripped its exactProp check under React 18. TODO marks it for removal once React 18 is dropped (React 19 has no runtime exactProp validation). - useValueAsRef: compare the ref captured after each render settles so React 18 StrictMode's discarded initial-mount object doesn't skew the identity check. --- .circleci/config.yml | 5 +++- docs/pages/material-ui/api/list-item.json | 2 +- .../mui-material/src/ListItem/ListItem.d.ts | 10 ++++++- .../mui-material/src/ListItem/ListItem.js | 2 +- packages/mui-material/src/Tab/Tab.test.js | 12 ++++++-- .../src/useValueAsRef/useValueAsRef.test.tsx | 28 ++++++++++--------- 6 files changed, 40 insertions(+), 19 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 510b10c7bbc370..e3e309d723f511 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -269,7 +269,10 @@ jobs: command: xvfb-run pnpm test:regressions - run: name: A11y results committed? - command: git add -A && git diff --exit-code --staged + # The react@17/18/next jobs pin React via `package-overrides`, which rewrites + # package.json/pnpm-lock.yaml. Exclude those so this check still verifies the a11y + # results on every job without tripping over the pinned dependencies. + command: git add -A -- . ':(exclude)package.json' ':(exclude)pnpm-lock.yaml' && git diff --exit-code --staged - run: name: Upload screenshots to Argos CI command: pnpm test:argos diff --git a/docs/pages/material-ui/api/list-item.json b/docs/pages/material-ui/api/list-item.json index 8c2aa1f1a31d2b..82302285146abc 100644 --- a/docs/pages/material-ui/api/list-item.json +++ b/docs/pages/material-ui/api/list-item.json @@ -15,7 +15,7 @@ "slotProps": { "type": { "name": "shape", - "description": "{ root?: object, secondaryAction?: func
| object }" + "description": "{ root?: func
| object, secondaryAction?: func
| object }" }, "default": "{}" }, diff --git a/packages/mui-material/src/ListItem/ListItem.d.ts b/packages/mui-material/src/ListItem/ListItem.d.ts index d58e8f12e9c668..740365ff2c7989 100644 --- a/packages/mui-material/src/ListItem/ListItem.d.ts +++ b/packages/mui-material/src/ListItem/ListItem.d.ts @@ -5,6 +5,8 @@ import { OverridableComponent, OverrideProps } from '../OverridableComponent'; import { ListItemClasses } from './listItemClasses'; import { SlotProps } from '../utils/types'; +export interface ListItemRootSlotPropsOverrides {} + export interface ListItemSecondaryActionSlotPropsOverrides {} /** @@ -66,7 +68,13 @@ export interface ListItemOwnProps extends ListItemBaseProps { */ slotProps?: | { - root?: React.HTMLAttributes | undefined; + root?: + | SlotProps< + React.ElementType>, + ListItemRootSlotPropsOverrides, + ListItemOwnerState + > + | undefined; secondaryAction?: | SlotProps< React.ElementType>, diff --git a/packages/mui-material/src/ListItem/ListItem.js b/packages/mui-material/src/ListItem/ListItem.js index bc9d4b0946a4c3..a9c11708d6fcee 100644 --- a/packages/mui-material/src/ListItem/ListItem.js +++ b/packages/mui-material/src/ListItem/ListItem.js @@ -271,7 +271,7 @@ ListItem.propTypes /* remove-proptypes */ = { * @default {} */ slotProps: PropTypes.shape({ - root: PropTypes.object, + root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), secondaryAction: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), }), /** diff --git a/packages/mui-material/src/Tab/Tab.test.js b/packages/mui-material/src/Tab/Tab.test.js index 46f5500ebfdc82..c103df7d24ff06 100644 --- a/packages/mui-material/src/Tab/Tab.test.js +++ b/packages/mui-material/src/Tab/Tab.test.js @@ -16,9 +16,17 @@ describe('', () => { classes, inheritComponent: ButtonBase, render: (node) => { - const value = node.props.value ?? 0; + // `Tab` must be a direct child of `Tabs`, which injects state into its children via + // `cloneElement`. Some conformance tests wrap the element(s) in a provider (e.g. + // `ThemeProvider`), so we slot `Tabs` *inside* the wrapper around the `Tab`s. Otherwise + // `Tabs` would clone the provider with `Tab`-internal props (`fullWidth`, `indicator`, …), + // tripping the provider's `exactProp` check under React 18. + // TODO: React 19 dropped runtime propType/`exactProp` validation, so once we stop testing + // React 18 this can revert to rendering `node` directly: `{node}`. + const isWrapped = node.type !== Tab; + const tabs = {isWrapped ? node.props.children : node}; const { container, ...other } = render( - {React.cloneElement(node, { value })}, + isWrapped ? React.cloneElement(node, undefined, tabs) : tabs, ); return { diff --git a/packages/mui-utils/src/useValueAsRef/useValueAsRef.test.tsx b/packages/mui-utils/src/useValueAsRef/useValueAsRef.test.tsx index 83bd6490ca693c..9b419b2151db26 100644 --- a/packages/mui-utils/src/useValueAsRef/useValueAsRef.test.tsx +++ b/packages/mui-utils/src/useValueAsRef/useValueAsRef.test.tsx @@ -33,25 +33,27 @@ describe('useValueAsRef', () => { }); it('returns the same ref object across renders', () => { - let firstRef: object | undefined; - let nextRef: object | undefined; + let capturedRef: object | undefined; function Test(props: { value: number }) { - const valueRef = useValueAsRef(props.value); - - if (firstRef === undefined) { - firstRef = valueRef; - } else { - nextRef = valueRef; - } - + capturedRef = useValueAsRef(props.value); return null; } + // Collect the committed ref after each render settles. Reading inside the render body + // instead would capture React 18 StrictMode's discarded initial-mount object (a distinct + // transient `ValueRef`), which is never the one the component ends up using. + const refs: Array = []; const { setProps } = render(); - + refs.push(capturedRef); setProps({ value: 2 }); - - expect(nextRef).to.equal(firstRef); + refs.push(capturedRef); + setProps({ value: 3 }); + refs.push(capturedRef); + + expect(refs.length).to.be.at.least(2); + refs.forEach((ref) => { + expect(ref).to.equal(refs[0]); + }); }); });