Skip to content

Commit

Permalink
[core] Replace enzyme in describeConformance (#42447)
Browse files Browse the repository at this point in the history
Co-authored-by: Aarón García Hervás <aaron@mui.com>
  • Loading branch information
DiegoAndai and aarongarciah committed Jun 18, 2024
1 parent ee4e109 commit da7b8cf
Show file tree
Hide file tree
Showing 30 changed files with 128 additions and 182 deletions.
2 changes: 1 addition & 1 deletion docs/pages/joy-ui/api/tooltip.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@
"isGlobal": false
}
],
"spread": true,
"spread": false,
"themeDefaultProps": true,
"muiName": "JoyTooltip",
"forwardsRefTo": "HTMLButtonElement",
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/material-ui/api/switch.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@
"isGlobal": false
}
],
"spread": false,
"spread": true,
"themeDefaultProps": false,
"muiName": "MuiSwitch",
"forwardsRefTo": "HTMLSpanElement",
Expand Down
160 changes: 66 additions & 94 deletions packages-internal/test-utils/src/describeConformance.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
/* eslint-env mocha */
import * as React from 'react';
import { expect } from 'chai';
import { ReactWrapper } from 'enzyme';
import ReactTestRenderer from 'react-test-renderer';
import createMount from './createMount';
import createDescribe from './createDescribe';
import findOutermostIntrinsic from './findOutermostIntrinsic';
import { MuiRenderResult } from './createRenderer';

function capitalize(string: string): string {
Expand Down Expand Up @@ -46,53 +43,30 @@ export interface ConformanceOptions {
after?: () => void;
inheritComponent?: React.ElementType;
render: (node: React.ReactElement<any>) => MuiRenderResult;
mount?: (node: React.ReactElement<any>) => ReactWrapper;
only?: Array<keyof typeof fullSuite>;
skip?: Array<keyof typeof fullSuite | 'classesRoot'>;
testComponentsRootPropWith?: string;
/**
* A custom React component to test if the component prop is implemented correctly.
*
* It must either:
* - Be a string that is a valid HTML tag, or
* - A component that spread props to the underlying rendered element.
*
* If not provided, the default 'em' element is used.
*/
testComponentPropWith?: string | React.ElementType;
testDeepOverrides?: SlotTestOverride | SlotTestOverride[];
testRootOverrides?: SlotTestOverride;
testStateOverrides?: { prop?: string; value?: any; styleKey: string };
testCustomVariant?: boolean;
testVariantProps?: object;
testLegacyComponentsProp?: boolean;
wrapMount?: (
mount: (node: React.ReactElement<any>) => ReactWrapper,
) => (node: React.ReactElement<any>) => ReactWrapper;
slots?: Record<string, SlotTestingOptions>;
ThemeProvider?: React.ElementType;
createTheme?: (arg: any) => any;
}

/**
* @param {object} node
* @returns
*/
function assertDOMNode(node: unknown) {
// duck typing a DOM node
expect(typeof (node as HTMLElement).nodeName).to.equal('string');
}

/**
* Utility method to make assertions about the ref on an element
* The element should have a component wrapped in withStyles as the root
*/
function testRef(
element: React.ReactElement<any>,
mount: ConformanceOptions['mount'],
onRef: (instance: unknown, wrapper: import('enzyme').ReactWrapper) => void = assertDOMNode,
) {
if (!mount) {
throwMissingPropError('mount');
}

const ref = React.createRef();

const wrapper = mount(<React.Fragment>{React.cloneElement(element, { ref })}</React.Fragment>);
onRef(ref.current, wrapper);
}

/**
* Glossary
* - root component:
Expand All @@ -102,18 +76,6 @@ function testRef(
* - has the type of `inheritComponent`
*/

/**
* Returns the component with the same constructor as `component` that renders
* the outermost host
*/
export function findRootComponent(wrapper: ReactWrapper, component: string | React.ElementType) {
const outermostHostElement = findOutermostIntrinsic(wrapper).getElement();

return wrapper.find(component as string).filterWhere((componentWrapper) => {
return componentWrapper.contains(outermostHostElement);
});
}

export function randomStringValue() {
return `s${Math.random().toString(36).slice(2)}`;
}
Expand All @@ -134,16 +96,20 @@ export function testClassName(
getOptions: () => ConformanceOptions,
) {
it('applies the className to the root component', () => {
const { mount } = getOptions();
if (!mount) {
throwMissingPropError('mount');
const { render } = getOptions();

if (!render) {
throwMissingPropError('render');
}

const className = randomStringValue();
const testId = randomStringValue();

const wrapper = mount(React.cloneElement(element, { className }));
const { getByTestId } = render(
React.cloneElement(element, { className, 'data-testid': testId }),
);

expect(findOutermostIntrinsic(wrapper).instance()).to.have.class(className);
expect(getByTestId(testId)).to.have.class(className);
});
}

Expand All @@ -157,45 +123,57 @@ export function testComponentProp(
) {
describe('prop: component', () => {
it('can render another root component with the `component` prop', () => {
const { mount, testComponentPropWith: component = 'em' } = getOptions();
if (!mount) {
throwMissingPropError('mount');
const { render, testComponentPropWith: component = 'em' } = getOptions();
if (!render) {
throwMissingPropError('render');
}

const wrapper = mount(React.cloneElement(element, { component }));
const testId = randomStringValue();

expect(findRootComponent(wrapper, component).exists()).to.equal(true);
if (typeof component === 'string') {
const { getByTestId } = render(
React.cloneElement(element, { component, 'data-testid': testId }),
);
expect(getByTestId(testId)).not.to.equal(null);
expect(getByTestId(testId).nodeName.toLowerCase()).to.eq(component);
} else {
const componentWithTestId = (props: {}) =>
React.createElement(component, { ...props, 'data-testid': testId });
const { getByTestId } = render(
React.cloneElement(element, {
component: componentWithTestId,
}),
);
expect(getByTestId(testId)).not.to.equal(null);
}
});
});
}

/**
* MUI components can spread additional props to a documented component.
* MUI components spread additional props to its root.
*/
export function testPropsSpread(
element: React.ReactElement<any>,
getOptions: () => ConformanceOptions,
) {
it(`spreads props to the root component`, () => {
// type def in ConformanceOptions
const { inheritComponent, mount } = getOptions();
if (!mount) {
throwMissingPropError('mount');
}
const { render } = getOptions();

if (inheritComponent === undefined) {
throw new TypeError(
'Unable to test props spread without `inheritComponent`. Either skip the test or pass a React element type.',
);
if (!render) {
throwMissingPropError('render');
}

const testProp = 'data-test-props-spread';
const value = randomStringValue();
const testId = randomStringValue();

const wrapper = mount(React.cloneElement(element, { [testProp]: value }));
const root = findRootComponent(wrapper, inheritComponent);
const { getByTestId } = render(
React.cloneElement(element, { [testProp]: value, 'data-testid': testId }),
);

expect(root.props()).to.have.property(testProp, value);
expect(getByTestId(testId)).to.have.attribute(testProp, value);
});
}

Expand All @@ -212,16 +190,17 @@ export function describeRef(
describe('ref', () => {
it(`attaches the ref`, () => {
// type def in ConformanceOptions
const { inheritComponent, mount, refInstanceof } = getOptions();
const { render, refInstanceof } = getOptions();

if (!render) {
throwMissingPropError('render');
}

testRef(element, mount, (instance, wrapper) => {
expect(instance).to.be.instanceof(refInstanceof);
const ref = React.createRef();

if (inheritComponent !== undefined && (instance as HTMLElement).nodeType === 1) {
const rootHost = findOutermostIntrinsic(wrapper);
expect(instance).to.equal(rootHost.instance());
}
});
render(React.cloneElement(element, { ref }));

expect(ref.current).to.be.instanceof(refInstanceof);
});
});
}
Expand Down Expand Up @@ -589,14 +568,18 @@ function testComponentsProp(
) {
describe('prop components:', () => {
it('can render another root component with the `components` prop', () => {
const { mount, testComponentsRootPropWith: component = 'em' } = getOptions();
if (!mount) {
throwMissingPropError('mount');
const { render, testComponentsRootPropWith: component = 'em' } = getOptions();
if (!render) {
throwMissingPropError('render');
}

const wrapper = mount(React.cloneElement(element, { components: { Root: component } }));
const testId = randomStringValue();

expect(findRootComponent(wrapper, component).exists()).to.equal(true);
const { getByTestId } = render(
React.cloneElement(element, { components: { Root: component }, 'data-testid': testId }),
);
expect(getByTestId(testId)).not.to.equal(null);
expect(getByTestId(testId).nodeName.toLowerCase()).to.eq(component);
});
});
}
Expand Down Expand Up @@ -1081,7 +1064,6 @@ function describeConformance(
only = Object.keys(fullSuite),
slots,
skip = [],
wrapMount,
} = getOptions();

let filteredTests = Object.keys(fullSuite).filter(
Expand All @@ -1096,21 +1078,11 @@ function describeConformance(
filteredTests = filteredTests.filter((testKey) => !slotBasedTests.includes(testKey));
}

const baseMount = createMount();
const mount = wrapMount !== undefined ? wrapMount(baseMount) : baseMount;

after(runAfterHook);

function getTestOptions(): ConformanceOptions {
return {
...getOptions(),
mount,
};
}

filteredTests.forEach((testKey) => {
const test = fullSuite[testKey];
test(minimalElement, getTestOptions);
test(minimalElement, getOptions);
});
}

Expand Down
20 changes: 19 additions & 1 deletion packages/mui-base/test/describeConformanceUnstyled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
SlotTestingOptions,
describeRef,
randomStringValue,
testClassName,
testComponentProp,
testReactTestRenderer,
} from '@mui/internal-test-utils';
Expand Down Expand Up @@ -269,6 +268,25 @@ function testSlotPropsProp(
});
}

function testClassName(element: React.ReactElement, getOptions: () => ConformanceOptions) {
it('applies the className to the root component', async () => {
const { render } = getOptions();

if (!render) {
throwMissingPropError('render');
}

const className = randomStringValue();
const testId = randomStringValue();

const { getByTestId } = await render(
React.cloneElement(element, { className, 'data-testid': testId }),
);

expect(getByTestId(testId)).to.have.class(className);
});
}

interface TestOwnerState {
'data-testid'?: string;
}
Expand Down
11 changes: 8 additions & 3 deletions packages/mui-joy/src/Autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,14 @@ const Autocomplete = React.forwardRef(function Autocomplete(
},
});

const defaultRenderOption = (optionProps: any, option: unknown) => (
<SlotOption {...optionProps}>{getOptionLabel(option)}</SlotOption>
);
const defaultRenderOption = (optionProps: any, option: unknown) => {
const { key, ...rest } = optionProps;
return (
<SlotOption key={key} {...rest}>
{getOptionLabel(option)}
</SlotOption>
);
};

const renderOption = renderOptionProp || defaultRenderOption;

Expand Down
6 changes: 0 additions & 6 deletions packages/mui-joy/src/Menu/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ describe('Joy <Menu />', () => {
<DropdownContext.Provider value={testContext}>{node}</DropdownContext.Provider>,
);
},
wrapMount: (mount) => (node: React.ReactNode) => {
const wrapper = mount(
<DropdownContext.Provider value={testContext}>{node}</DropdownContext.Provider>,
);
return wrapper.childAt(0);
},
ThemeProvider,
muiName: 'JoyMenu',
refInstanceof: window.HTMLUListElement,
Expand Down
6 changes: 0 additions & 6 deletions packages/mui-joy/src/MenuButton/MenuButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ describe('<MenuButton />', () => {
describeConformance(<MenuButton />, () => ({
classes,
inheritComponent: 'button',
wrapMount: (mount) => (node: React.ReactNode) => {
const wrapper = mount(
<DropdownContext.Provider value={testContext}>{node}</DropdownContext.Provider>,
);
return wrapper.childAt(0);
},
muiName: 'JoyMenuButton',
refInstanceof: window.HTMLButtonElement,
render: (node) => {
Expand Down
4 changes: 0 additions & 4 deletions packages/mui-joy/src/MenuItem/MenuItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ describe('Joy <MenuItem />', () => {
classes,
inheritComponent: ListItemButton,
render: (node) => render(<MenuProvider value={testContext}>{node}</MenuProvider>),
wrapMount: (mount) => (node) => {
const wrapper = mount(<MenuProvider value={testContext}>{node}</MenuProvider>);
return wrapper.childAt(0);
},
ThemeProvider,
refInstanceof: window.HTMLLIElement,
testComponentPropWith: 'a',
Expand Down
1 change: 0 additions & 1 deletion packages/mui-joy/src/Tab/Tab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ describe('Joy <Tab />', () => {
classes,
inheritComponent: 'button',
render: (node) => render(<TabsProvider defaultValue={0}>{node}</TabsProvider>),
wrapMount: (mount) => (node) => mount(<TabsProvider defaultValue={0}>{node}</TabsProvider>),
ThemeProvider,
muiName: 'JoyTab',
refInstanceof: window.HTMLButtonElement,
Expand Down
1 change: 0 additions & 1 deletion packages/mui-joy/src/TabList/TabList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ describe('Joy <TabList />', () => {
classes,
inheritComponent: 'div',
render: (node) => render(<TabsProvider defaultValue={0}>{node}</TabsProvider>),
wrapMount: (mount) => (node) => mount(<TabsProvider defaultValue={0}>{node}</TabsProvider>),
ThemeProvider,
muiName: 'JoyTabList',
refInstanceof: window.HTMLDivElement,
Expand Down
Loading

0 comments on commit da7b8cf

Please sign in to comment.