From a91cce95b31763826e6e82b41f1e66caee0bc724 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Fri, 18 Jun 2021 21:26:08 +0200 Subject: [PATCH 1/3] [test] Convert remaining enzyme tests to testing-library --- .../src/styled/styled.test.js | 29 ++++++------ packages/material-ui/src/utils/index.test.js | 46 ++++++++----------- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/packages/material-ui-styles/src/styled/styled.test.js b/packages/material-ui-styles/src/styled/styled.test.js index 358a1a13e8963f..0b36f3de19793c 100644 --- a/packages/material-ui-styles/src/styled/styled.test.js +++ b/packages/material-ui-styles/src/styled/styled.test.js @@ -2,14 +2,14 @@ import * as React from 'react'; import { expect } from 'chai'; import PropTypes from 'prop-types'; import { SheetsRegistry } from 'jss'; -import { createMount } from 'test/utils'; +import { createClientRender, screen } from 'test/utils'; import { createGenerateClassName } from '@material-ui/styles'; import styled from './styled'; import StylesProvider from '../StylesProvider'; describe('styled', () => { // StrictModeViolation: uses makeStyles - const mount = createMount({ strict: false }); + const render = createClientRender({ strict: false }); let StyledButton; before(() => { @@ -28,7 +28,7 @@ describe('styled', () => { const sheetsRegistry = new SheetsRegistry(); const generateClassName = createGenerateClassName(); - mount( + render( Styled Components , @@ -39,26 +39,26 @@ describe('styled', () => { }); describe('prop: clone', () => { - let wrapper; + let view; beforeEach(() => { - wrapper = mount( - + view = render( +
Styled Components
, ); }); it('should be able to pass props to cloned element', () => { - expect(wrapper.find('div').props()['data-test']).to.equal('enzyme'); + expect(view.container.firstChild).to.have.attribute('data-test', 'styled'); }); it('should be able to clone the child element', () => { - expect(wrapper.getDOMNode().nodeName).to.equal('DIV'); - wrapper.setProps({ + expect(view.container.firstChild).to.have.tagName('DIV'); + view.setProps({ clone: false, }); - expect(wrapper.getDOMNode().nodeName).to.equal('BUTTON'); + expect(view.container.firstChild).to.have.tagName('BUTTON'); }); }); @@ -75,13 +75,12 @@ describe('styled', () => { style.filterProps = ['color']; style.propTypes = {}; const StyledDiv = styled('div')(style); - const wrapper = mount( - + render( + Styled Components , ); - expect(wrapper.find('div').props().color).to.equal(undefined); - expect(wrapper.find('div').props()['data-test']).to.equal('enzyme'); + expect(screen.getByTestId('styled')).to.have.attribute('data-color', 'blue'); }); describe('warnings', () => { @@ -102,6 +101,6 @@ describe('styled', () => { }); it('should accept a child function', () => { - mount({(props) =>
Styled Components
}
); + render({(props) =>
Styled Components
}
); }); }); diff --git a/packages/material-ui/src/utils/index.test.js b/packages/material-ui/src/utils/index.test.js index 3f3ece8688aa4d..cee26636609dda 100644 --- a/packages/material-ui/src/utils/index.test.js +++ b/packages/material-ui/src/utils/index.test.js @@ -1,12 +1,13 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; -import PropTypes from 'prop-types'; -import { mount } from 'enzyme'; +import { createClientRender, screen } from 'test/utils'; import { isMuiElement, setRef, useForkRef } from '.'; import { Input, ListItemSecondaryAction, SvgIcon } from '..'; describe('utils/index.js', () => { + const render = createClientRender(); + describe('isMuiElement', () => { it('should match static muiName property', () => { const Component = () => null; @@ -75,14 +76,10 @@ describe('utils/index.js', () => { return
{ownRef.current ? 'has a ref' : 'has no ref'}
; } - Component.propTypes = { - innerRef: PropTypes.any, - }; - const outerRef = React.createRef(); expect(() => { - mount(); + render(); }).not.toErrorDev(); expect(outerRef.current.textContent).to.equal('has a ref'); }); @@ -93,14 +90,18 @@ describe('utils/index.js', () => { const handleOwnRef = React.useCallback(() => setHasRef(true), []); const handleRef = useForkRef(handleOwnRef, ref); - return
{String(hasRef)}
; + return ( +
+ {String(hasRef)} +
+ ); }); - let wrapper; expect(() => { - wrapper = mount(); + render(); }).not.toErrorDev(); - expect(wrapper.containsMatchingElement(
true
)).to.equal(true); + + expect(screen.getByTestId('hasRef')).to.have.text('true'); }); it('does nothing if none of the forked branches requires a ref', () => { @@ -111,14 +112,12 @@ describe('utils/index.js', () => { return React.cloneElement(children, { ref: handleRef }); }); - Outer.propTypes = { children: PropTypes.element.isRequired }; - function Inner() { return
; } expect(() => { - mount( + render( , @@ -127,8 +126,6 @@ describe('utils/index.js', () => { }); describe('changing refs', () => { - // use named props rather than ref attribute because enzyme ignores - // ref attributes on the root component function Div(props) { const { leftRef, rightRef, ...other } = props; const handleRef = useForkRef(leftRef, rightRef); @@ -136,21 +133,16 @@ describe('utils/index.js', () => { return
; } - Div.propTypes = { - leftRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - rightRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - }; - it('handles changing from no ref to some ref', () => { - let wrapper; + let view; expect(() => { - wrapper = mount(
); + view = render(
); }).not.toErrorDev(); const ref = React.createRef(); expect(() => { - wrapper.setProps({ leftRef: ref }); + view.setProps({ leftRef: ref }); }).not.toErrorDev(); expect(ref.current.id).to.equal('test'); }); @@ -159,16 +151,16 @@ describe('utils/index.js', () => { const firstLeftRef = React.createRef(); const firstRightRef = React.createRef(); const secondRightRef = React.createRef(); - let wrapper; + let view; expect(() => { - wrapper = mount(
); + view = render(
); }).not.toErrorDev(); expect(firstLeftRef.current.id).to.equal('test'); expect(firstRightRef.current.id).to.equal('test'); expect(secondRightRef.current).to.equal(null); - wrapper.setProps({ rightRef: secondRightRef }); + view.setProps({ rightRef: secondRightRef }); expect(firstLeftRef.current.id).to.equal('test'); expect(firstRightRef.current).to.equal(null); From f57dba0ad5eb90ec8727e835b7554578923daf4d Mon Sep 17 00:00:00 2001 From: eps1lon Date: Fri, 18 Jun 2021 21:27:53 +0200 Subject: [PATCH 2/3] [test] Remove unused enzyme utils --- test/utils/createShallow.js | 45 --------------- test/utils/getClasses.js | 22 ------- test/utils/index.js | 2 - test/utils/until.js | 28 --------- test/utils/until.test.js | 111 ------------------------------------ 5 files changed, 208 deletions(-) delete mode 100644 test/utils/createShallow.js delete mode 100644 test/utils/getClasses.js delete mode 100644 test/utils/until.js delete mode 100644 test/utils/until.test.js diff --git a/test/utils/createShallow.js b/test/utils/createShallow.js deleted file mode 100644 index 05789d93ac0ed8..00000000000000 --- a/test/utils/createShallow.js +++ /dev/null @@ -1,45 +0,0 @@ -import { shallow as enzymeShallow } from 'enzyme'; -import until from './until'; - -/** - * @typedef {object} ExtendedShallowOptions - * @property {typeof import('enzyme').shallow} shallow; - * @property {boolean} dive - * @property {import('enzyme').EnzymeSelector} untilSelector - * - * @typedef {import('enzyme').ShallowRendererProps & ExtendedShallowOptions} ShallowOptions - */ - -/** - * Generate an enhanced shallow function. - * @param {Partial} [options1] - * @returns {typeof import('enzyme').shallow} - */ -export default function createShallow(options1 = {}) { - const { shallow = enzymeShallow, dive = false, untilSelector = false, ...other1 } = options1; - - const shallowWithContext = function shallowWithContext(node, options2 = {}) { - const options = { - ...other1, - ...options2, - context: { - ...other1.context, - ...options2.context, - }, - }; - - const wrapper = shallow(node, options); - - if (dive) { - return wrapper.dive(); - } - - if (untilSelector) { - return until.call(wrapper, untilSelector, options); - } - - return wrapper; - }; - - return shallowWithContext; -} diff --git a/test/utils/getClasses.js b/test/utils/getClasses.js deleted file mode 100644 index 3f8dfc5669d4ec..00000000000000 --- a/test/utils/getClasses.js +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import createShallow from './createShallow'; - -const shallow = createShallow(); - -/** - * Extracts the available classes for the `classes` prop of the given component - * @param {React.ReactElement} element - An element created from a Material-UI component that implements the `classes` prop. - * @returns {Record} - */ -export default function getClasses(element) { - const { useStyles = () => null } = element.type; - - let classes; - function Listener() { - classes = useStyles(element.props); - return null; - } - shallow(); - - return classes; -} diff --git a/test/utils/index.js b/test/utils/index.js index f5cf5b4708d3cf..7201f45b484669 100644 --- a/test/utils/index.js +++ b/test/utils/index.js @@ -4,14 +4,12 @@ export { default as describeConformanceV5 } from './describeConformanceV5'; export * from './createClientRender'; export { default as createMount } from './createMount'; export { default as createServerRender } from './createServerRender'; -export { default as createShallow } from './createShallow'; export { default as findOutermostIntrinsic, wrapsIntrinsicElement } from './findOutermostIntrinsic'; export { default as focusVisible, simulatePointerDevice, programmaticFocusTriggersFocusVisible, } from './focusVisible'; -export { default as getClasses } from './getClasses'; export {} from './initMatchers'; export * as fireDiscreteEvent from './fireDiscreteEvent'; export * as userEvent from './userEvent'; diff --git a/test/utils/until.js b/test/utils/until.js deleted file mode 100644 index aab33f8085bf51..00000000000000 --- a/test/utils/until.js +++ /dev/null @@ -1,28 +0,0 @@ -function shallowRecursively(wrapper, selector, { context, ...other }) { - if (wrapper.isEmptyRender() || typeof wrapper.getElement().type === 'string') { - return wrapper; - } - - let newContext = context; - - const instance = wrapper.root().instance(); - // The instance can be null with a stateless functional component and react >= 16. - if (instance && instance.getChildContext) { - newContext = { - ...context, - ...instance.getChildContext(), - }; - } - - const nextWrapper = wrapper.shallow({ context: newContext, ...other }); - - if (selector && wrapper.is(selector)) { - return nextWrapper; - } - - return shallowRecursively(nextWrapper, selector, { context: newContext }); -} - -export default function until(selector, options = {}) { - return this.single('until', () => shallowRecursively(this, selector, options)); -} diff --git a/test/utils/until.test.js b/test/utils/until.test.js deleted file mode 100644 index c80020c8d38f5d..00000000000000 --- a/test/utils/until.test.js +++ /dev/null @@ -1,111 +0,0 @@ -import * as React from 'react'; -import { expect } from 'chai'; -import PropTypes from 'prop-types'; -import { shallow } from 'enzyme'; -import until from './until'; - -const Div = () =>
; -const hoc = (Component) => () => ; - -describe('until', () => { - it('shallow renders the current wrapper one level deep', () => { - const EnhancedDiv = hoc(Div); - const wrapper = until.call(shallow(), 'Div'); - expect(wrapper.contains(
)).to.equal(true); - }); - - it('shallow renders the current wrapper several levels deep', () => { - const EnhancedDiv = hoc(hoc(hoc(Div))); - const wrapper = until.call(shallow(), 'Div'); - expect(wrapper.contains(
)).to.equal(true); - }); - - it('stops shallow rendering when the wrapper is empty', () => { - const nullHoc = () => () => null; - const EnhancedDiv = nullHoc(); - const wrapper = until.call(shallow(), 'Div'); - expect(wrapper.html()).to.equal(null); - }); - - it('shallow renders as much as possible when no selector is provided', () => { - const EnhancedDiv = hoc(hoc(Div)); - const wrapper = until.call(shallow()); - expect(wrapper.contains(
)).to.equal(true); - }); - - it('shallow renders the current wrapper even if the selector never matches', () => { - const EnhancedDiv = hoc(Div); - const wrapper = until.call(shallow(), 'NotDiv'); - expect(wrapper.contains(
)).to.equal(true); - }); - - it('stops shallow rendering when it encounters a HTML element', () => { - const wrapper = until.call( - shallow( -
-
-
, - ), - 'Div', - ); - expect( - wrapper.contains( -
-
-
, - ), - ).to.equal(true); - }); - - it('throws when until called on an empty wrapper', () => { - expect(() => { - until.call(shallow(
).find('Foo'), 'div'); - }).to.throw(Error); - }); - - it('shallow renders non-root wrappers', () => { - const Container = () => ( -
-
-
- ); - const wrapper = until.call(shallow().find(Div)); - expect(wrapper.contains(
)).to.equal(true); - }); - - // eslint-disable-next-line react/prefer-stateless-function - class Foo extends React.Component { - render() { - return
; - } - } - - Foo.contextTypes = { - quux: PropTypes.bool.isRequired, - }; - - it('context propagation passes down context from the root component', () => { - const EnhancedFoo = hoc(Foo); - const options = { context: { quux: true } }; - const wrapper = until.call(shallow(, options), 'Foo', options); - expect(wrapper.context('quux')).to.equal(true); - expect(wrapper.contains(
)).to.equal(true); - }); - - class Bar extends React.Component { - static childContextTypes = { quux: PropTypes.bool }; - - getChildContext = () => ({ quux: true }); - - render() { - return ; - } - } - - it('context propagation passes down context from an intermediary component', () => { - const EnhancedBar = hoc(Bar); - const wrapper = until.call(shallow(), 'Foo'); - expect(wrapper.context('quux')).to.equal(true); - expect(wrapper.contains(
)).to.equal(true); - }); -}); From b99e8aa0d34b91f22863398fff41e1f947897da4 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Fri, 18 Jun 2021 21:56:18 +0200 Subject: [PATCH 3/3] Migrate createServerRender from enzyme to react-dom/server --- .../src/Portal/Portal.test.js | 25 +++-------- .../material-ui/src/Button/Button.test.js | 4 +- packages/material-ui/src/Fab/Fab.test.js | 4 +- packages/material-ui/src/NoSsr/NoSsr.test.js | 40 +++++++---------- packages/material-ui/src/Tabs/Tabs.test.js | 4 +- .../src/ToggleButton/ToggleButton.test.js | 4 +- .../src/useMediaQuery/useMediaQuery.test.js | 45 +++++++++---------- test/utils/createServerRender.js | 8 +++- 8 files changed, 56 insertions(+), 78 deletions(-) diff --git a/packages/material-ui-unstyled/src/Portal/Portal.test.js b/packages/material-ui-unstyled/src/Portal/Portal.test.js index 71d017db7da715..a3d79b6d755a7b 100644 --- a/packages/material-ui-unstyled/src/Portal/Portal.test.js +++ b/packages/material-ui-unstyled/src/Portal/Portal.test.js @@ -5,7 +5,7 @@ import { createServerRender, createClientRender } from 'test/utils'; import Portal from './Portal'; describe('', () => { - const serverRender = createServerRender(); + const serverRender = createServerRender({ expectUseLayoutEffectWarning: true }); const render = createClientRender(); describe('server-side', () => { @@ -17,25 +17,12 @@ describe('', () => { }); it('render nothing on the server', () => { - const markup1 = serverRender(
Bar
); - expect(markup1.text()).to.equal('Bar'); - - let markup2; - expect(() => { - markup2 = serverRender( - -
Bar
-
, - ); - }).toErrorDev( - // Known issue due to using SSR APIs in a browser environment. - // We use 2x useLayoutEffect in the component. - [ - 'Warning: useLayoutEffect does nothing on the server', - 'Warning: useLayoutEffect does nothing on the server', - ], + const container = serverRender( + +
Bar
+
, ); - expect(markup2.text()).to.equal(''); + expect(container.firstChild).to.equal(null); }); }); diff --git a/packages/material-ui/src/Button/Button.test.js b/packages/material-ui/src/Button/Button.test.js index 78acab5187569c..2cb756d192d5c5 100644 --- a/packages/material-ui/src/Button/Button.test.js +++ b/packages/material-ui/src/Button/Button.test.js @@ -362,8 +362,8 @@ describe('); - expect(markup.text()).to.equal('Hello World'); + const container = serverRender(); + expect(container.firstChild).to.have.text('Hello World'); }); }); diff --git a/packages/material-ui/src/Fab/Fab.test.js b/packages/material-ui/src/Fab/Fab.test.js index c8e182abf3ea8d..23bd45699ce43b 100644 --- a/packages/material-ui/src/Fab/Fab.test.js +++ b/packages/material-ui/src/Fab/Fab.test.js @@ -163,8 +163,8 @@ describe('', () => { }); it('should server-side render', () => { - const markup = serverRender(Fab); - expect(markup.text()).to.equal('Fab'); + const container = serverRender(Fab); + expect(container.firstChild).to.have.text('Fab'); }); }); }); diff --git a/packages/material-ui/src/NoSsr/NoSsr.test.js b/packages/material-ui/src/NoSsr/NoSsr.test.js index 9f90ea9707d6ef..3e123a91d8e7b7 100644 --- a/packages/material-ui/src/NoSsr/NoSsr.test.js +++ b/packages/material-ui/src/NoSsr/NoSsr.test.js @@ -5,22 +5,17 @@ import NoSsr from '@material-ui/core/NoSsr'; describe('', () => { const render = createClientRender(); - const serverRender = createServerRender(); + const serverRender = createServerRender({ expectUseLayoutEffectWarning: true }); describe('server-side rendering', () => { it('should not render the children as the width is unknown', () => { - let wrapper; - expect(() => { - wrapper = serverRender( - - Hello - , - ); - }).toErrorDev( - // Known issue due to using SSR APIs in a browser environment. - ['Warning: useLayoutEffect does nothing on the server'], + const container = serverRender( + + Hello + , ); - expect(wrapper.text()).to.equal(''); + + expect(container.firstChild).to.equal(null); }); }); @@ -37,20 +32,15 @@ describe('', () => { describe('prop: fallback', () => { it('should render the fallback', () => { - let wrapper; - expect(() => { - wrapper = serverRender( -
- - Hello - -
, - ); - }).toErrorDev( - // Known issue due to using SSR APIs in a browser environment. - ['Warning: useLayoutEffect does nothing on the server'], + const container = serverRender( +
+ + Hello + +
, ); - expect(wrapper.text()).to.equal('fallback'); + + expect(container.firstChild).to.have.text('fallback'); }); }); diff --git a/packages/material-ui/src/Tabs/Tabs.test.js b/packages/material-ui/src/Tabs/Tabs.test.js index d8701d01dbf4ec..dca49518ea9fd2 100644 --- a/packages/material-ui/src/Tabs/Tabs.test.js +++ b/packages/material-ui/src/Tabs/Tabs.test.js @@ -800,13 +800,13 @@ describe('', () => { const serverRender = createServerRender({ expectUseLayoutEffectWarning: true }); it('should let the selected render the indicator server-side', () => { - const markup = serverRender( + const container = serverRender( , ); - const indicator = markup.find(`button > .${classes.indicator}`); + const indicator = container.firstChild.querySelectorAll(`button > .${classes.indicator}`); expect(indicator).to.have.lengthOf(1); }); }); diff --git a/packages/material-ui/src/ToggleButton/ToggleButton.test.js b/packages/material-ui/src/ToggleButton/ToggleButton.test.js index e080429c844662..5a42eee70f6228 100644 --- a/packages/material-ui/src/ToggleButton/ToggleButton.test.js +++ b/packages/material-ui/src/ToggleButton/ToggleButton.test.js @@ -143,8 +143,8 @@ describe('', () => { const serverRender = createServerRender({ expectUseLayoutEffectWarning: true }); it('should server-side render', () => { - const markup = serverRender(Hello World); - expect(markup.text()).to.equal('Hello World'); + const container = serverRender(Hello World); + expect(container.firstChild).to.have.text('Hello World'); }); }); }); diff --git a/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js b/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js index 36d8da32c2b0f1..70acd7e9d3e3cd 100644 --- a/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js +++ b/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js @@ -253,37 +253,34 @@ describe('useMediaQuery', () => { }); describe('server-side', () => { - const serverRender = createServerRender(); + const serverRender = createServerRender({ expectUseLayoutEffectWarning: true }); it('should use the ssr match media ponyfill', () => { - let markup; - expect(() => { - function MyComponent() { - const matches = useMediaQuery('(min-width:2000px)'); + function MyComponent() { + const matches = useMediaQuery('(min-width:2000px)'); - return {`${matches}`}; - } + return {`${matches}`}; + } - const Test = () => { - const ssrMatchMedia = (query) => ({ - matches: mediaQuery.match(query, { - width: 3000, - }), - }); + const Test = () => { + const ssrMatchMedia = (query) => ({ + matches: mediaQuery.match(query, { + width: 3000, + }), + }); - return ( - - - - ); - }; + return ( + + + + ); + }; - markup = serverRender(); - }).toErrorDev(['Warning: useLayoutEffect does nothing on the server']); + const container = serverRender(); - expect(markup.text()).to.equal('true'); + expect(container.firstChild).to.have.text('true'); }); }); diff --git a/test/utils/createServerRender.js b/test/utils/createServerRender.js index 117e323b3f8c72..670caaa44c6e84 100644 --- a/test/utils/createServerRender.js +++ b/test/utils/createServerRender.js @@ -1,5 +1,5 @@ /* eslint-env mocha */ -import { render as enzymeRender } from 'enzyme'; +import * as ReactDOMServer from 'react-dom/server'; import { stub } from 'sinon'; /** @@ -28,6 +28,10 @@ export default function createServerRender(options = {}) { }); return function render(node) { - return enzymeRender(node); + const markup = ReactDOMServer.renderToStaticMarkup(node); + const container = document.createElement('div'); + container.innerHTML = markup; + + return container; }; }