-
-
Notifications
You must be signed in to change notification settings - Fork 32.8k
[core] Replace enzyme in describeConformance #42447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
01660be
c984272
96d29af
a891f02
ae0b1da
d0b2593
aceac7d
3a6ae48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
|
|
@@ -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: | ||
|
|
@@ -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)}`; | ||
| } | ||
|
|
@@ -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 }), | ||
aarongarciah marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ); | ||
|
|
||
| expect(findOutermostIntrinsic(wrapper).instance()).to.have.class(className); | ||
| expect(getByTestId(testId)).to.have.class(className); | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, we lose the aspect of testing that the inherited component is being used and only maintain that the props are spread into whatever the component considers its root. This follows RTL's principle of not testing implementation details. There might be a way for us to test that the inherited component is the one used, but I think it would be better to test that on each component's tests, by testing the expected functionality, classes, or whatever the inherited component provides.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIK this is mostly used by the doc gen to add a message about additional props available on a component. I'm strongly in favor of removing this message and listing all available props on each component, so it's not necessary to jump between the pages to learn about available props. Afterwards, this test may be unnecessary. Not in this PR, of course :) |
||
| }); | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
| }); | ||
| }); | ||
| } | ||
|
|
@@ -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); | ||
| }); | ||
| }); | ||
| } | ||
|
|
@@ -1081,7 +1064,6 @@ function describeConformance( | |
| only = Object.keys(fullSuite), | ||
| slots, | ||
| skip = [], | ||
| wrapMount, | ||
| } = getOptions(); | ||
|
|
||
| let filteredTests = Object.keys(fullSuite).filter( | ||
|
|
@@ -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); | ||
| }); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,6 @@ import { | |
| SlotTestingOptions, | ||
| describeRef, | ||
| randomStringValue, | ||
| testClassName, | ||
| testComponentProp, | ||
| testReactTestRenderer, | ||
| } from '@mui/internal-test-utils'; | ||
|
|
@@ -269,6 +268,25 @@ function testSlotPropsProp( | |
| }); | ||
| } | ||
|
|
||
| function testClassName(element: React.ReactElement, getOptions: () => ConformanceOptions) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was required as Base UI's
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Async render functions will be required when Material UI uses Floating UI. Certain interactions require flushing the microtask queue, which is an async operation. This is not strictly required now, so I agree it's ok to minimize the number of changes in this PR. |
||
| 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; | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.