import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import createStyled from '@mui/system/createStyled'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import { createRenderer, screen } from '@mui/internal-test-utils'; describe('createStyled', () => { const { render } = createRenderer(); // These tests rely on implementation details (namely `displayName`) // Ideally we'd just test if the proper name appears in a React warning. // But React warnings are deduplicated during module lifetime. // We would need to reset modules to make the tests work in watchmode. describe.skipIf(process.env.NODE_ENV === 'production')('displayName', () => { it('uses the `componentName` if set', () => { const styled = createStyled({}); const SomeMuiComponent = styled('div', { name: 'SomeMuiComponent' })({}); expect(SomeMuiComponent).to.have.property('displayName', 'SomeMuiComponent'); }); it('falls back to the decorated tag name', () => { const styled = createStyled({}); const SomeMuiComponent = styled('div')({}); expect(SomeMuiComponent).to.have.property('displayName', 'Styled(div)'); }); it('falls back to the decorated computed displayName', () => { const styled = createStyled({}); const SomeMuiComponent = styled(function SomeMuiComponent() { return null; })({}); expect(SomeMuiComponent).to.have.property('displayName', 'Styled(SomeMuiComponent)'); }); it('has a fallback name if the display name cannot be computed', () => { const styled = createStyled({}); const SomeMuiComponent = styled(() => null)({}); expect(SomeMuiComponent).to.have.property('displayName', 'Styled(Component)'); }); }); describe('composition', () => { it('should call styleFunctionSx once', () => { const styled = createStyled(); const spySx = spy(); const Child = styled('div')({}); render(); expect(spySx.callCount).to.equal(2); // React 18 renders twice in strict mode. }); it('should still call styleFunctionSx once', () => { const styled = createStyled(); const spySx = spy(); const Child = styled('div')({}); const Parent = styled(Child)({}); render(); expect(spySx.callCount).to.equal(2); // React 18 renders twice in strict mode. }); it('both child and parent still accept `sx` prop', () => { const styled = createStyled(); const Child = styled('div')({}); const Parent = styled(Child)({}); render( , ); expect(screen.getByRole('parent')).toHaveComputedStyle({ color: 'rgb(0, 0, 255)' }); expect(screen.getByRole('child')).toHaveComputedStyle({ color: 'rgb(255, 0, 0)' }); }); }); it('default overridesResolver', () => { const styled = createStyled({}); const Button = styled('button', { name: 'MuiButton', slot: 'root', })({ display: 'flex', }); render( , ); expect(screen.getByTestId('button')).toHaveComputedStyle({ width: '300px', height: '200px', }); }); describe('styles', () => { it('styles of pseudo classes of variants are merged', () => { const theme = createTheme({ components: { MuiButton: { variants: [ { props: { variant: 'contained' }, style: { '&.Mui-disabled': { width: '300px', }, }, }, { props: { variant: 'contained', color: 'primary' }, style: { '&.Mui-disabled': { height: '200px', }, }, }, ], }, }, }); const styled = createStyled({}); const Button = styled('button', { shouldForwardProp: (prop) => prop !== 'color' && prop !== 'contained', name: 'MuiButton', slot: 'Root', overridesResolver: (props, styles) => styles.root, })({ display: 'flex', }); render( , ); expect(screen.getByTestId('button')).toHaveComputedStyle({ width: '300px', height: '200px', }); }); }); describe('styleOverrides callback', () => { const styled = createStyled({}); const ButtonRoot = styled('button', { name: 'MuiButton', slot: 'Root', overridesResolver: (props, styles) => [ styles.root, { [`& .MuiButton-avatar`]: styles.avatar }, ], })({}); const ButtonIcon = styled('span', { name: 'MuiButton', slot: 'Icon', overridesResolver: (props, styles) => styles.icon, })({}); function Button({ children, startIcon, endIcon, color = 'primary', ...props }) { const ownerState = { startIcon, endIcon, color, ...props }; return ( {startIcon && {startIcon}} {children} {endIcon && {endIcon}} ); } it('spread ownerState as props to the slot styleOverrides', () => { const finalTheme = createTheme({ components: { MuiButton: { styleOverrides: { avatar: () => { return { width: '100px', }; }, }, }, }, }); render( , ); expect(screen.getByTestId('button-avatar')).toHaveComputedStyle({ width: '100px', }); }); it('support slot as nested class', () => { const finalTheme = createTheme({ typography: { button: { fontSize: '20px', }, }, components: { MuiButton: { styleOverrides: { root: ({ ownerState, theme }) => { const { color, variant } = ownerState; const styles = []; if (color === 'primary') { styles.push({ width: 120, height: 48, }); } if (variant === 'contained') { styles.push(theme.typography.button); } return styles; }, icon: ({ ownerState }) => [ ownerState.startIcon && { marginRight: 8 }, ownerState.endIcon && { marginLeft: 8 }, ], }, }, }, }); const { container } = render( , ); expect(screen.getByRole('button')).toHaveComputedStyle({ width: '120px', height: '48px', fontSize: '20px', }); expect( container.firstChild.firstChild, // startIcon ).toHaveComputedStyle({ marginRight: '8px', }); }); it('support object return from the callback', () => { const finalTheme = createTheme({ components: { MuiButton: { styleOverrides: { root: () => ({ width: '300px', }), }, }, }, }); const { container } = render( , ); expect(container.firstChild).toHaveComputedStyle({ width: '300px', }); }); it('support template string return from the callback', () => { const finalTheme = createTheme({ components: { MuiButton: { styleOverrides: { root: () => ` width: 300px; `, }, }, }, }); const { container } = render( , ); expect(container.firstChild).toHaveComputedStyle({ width: '300px', }); }); it('works with sx', () => { const finalTheme = createTheme({ components: { MuiButton: { styleOverrides: { root: ({ theme }) => theme.unstable_sx({ pt: 10, }), icon: ({ ownerState, theme }) => [ ownerState.color === 'primary' && theme.unstable_sx({ mr: 10, }), ], }, }, }, }); const { container } = render( , ); expect(container.firstChild).toHaveComputedStyle({ paddingTop: '80px', }); expect(container.firstChild.firstChild).toHaveComputedStyle({ marginRight: '80px', }); }); }); it('does not spread `sx` prop to DOM', () => { const styled = createStyled({}); const Button = styled('button')({}); render( , ); expect(screen.getByTestId('button')).not.to.have.attribute('sx'); }); it('does not forward `ownerState` prop to DOM', () => { const styled = createStyled({}); const Button = styled('button')({}); render(, ); const button = screen.getByTestId('button'); expect(button.getAttribute('data-foo')).to.equal('bar'); expect(button.getAttribute('color')).to.equal('red'); // color is for Safari mask-icon link expect(button.getAttribute('shouldBeRemoved')).not.to.equal('true'); }); it('can use `as` prop', () => { const styled = createStyled({}); const Button = styled('button')({}); render(