diff --git a/package.json b/package.json index 4b92648ea98a89..295dd95ed16989 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "test:regressions": "yarn test:regressions:build && rimraf test/regressions/screenshots/chrome/* && vrtest run --config test/vrtest.config.js --record", "test:regressions:build": "webpack --config test/regressions/webpack.config.js", "test:umd": "node packages/material-ui/test/umd/run.js", - "test:unit": "cross-env NODE_ENV=test mocha 'packages/**/*.test.js' 'docs/**/*.test.js' 'scripts/**/*.test.js' --exclude '**/node_modules/**'", + "test:unit": "cross-env NODE_ENV=test mocha 'packages/**/*.test.js' 'docs/**/*.test.js' 'scripts/**/*.test.js' 'test/utils/**/*.test.js' --exclude '**/node_modules/**'", "test:watch": "yarn test:unit --watch", "typescript": "lerna run typescript --parallel" }, diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js index edb42e7baab110..d10643e23f04a3 100644 --- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js +++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js @@ -3,7 +3,6 @@ import { expect } from 'chai'; import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import describeConformance from '@material-ui/core/test-utils/describeConformance'; -import consoleErrorMock, { consoleWarnMock } from 'test/utils/consoleErrorMock'; import { spy } from 'sinon'; import { act, createClientRender, fireEvent, screen } from 'test/utils/createClientRender'; import { createFilterOptions } from '../useAutocomplete/useAutocomplete'; @@ -1021,16 +1020,6 @@ describe('', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - consoleWarnMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - consoleWarnMock.reset(); - }); - it('warn if getOptionLabel do not return a string', () => { const handleChange = spy(); render( @@ -1045,14 +1034,18 @@ describe('', () => { ); const textbox = screen.getByRole('textbox'); - fireEvent.change(textbox, { target: { value: 'a' } }); - fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(() => { + fireEvent.change(textbox, { target: { value: 'a' } }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + }).toErrorDev([ + 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', + // strict mode renders twice + 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', + 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', + 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', + ]); expect(handleChange.callCount).to.equal(1); expect(handleChange.args[0][1]).to.equal('a'); - expect(consoleErrorMock.callCount()).to.equal(4); // strict mode renders twice - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', - ); }); it('warn if getOptionSelected match multiple values for a given option', () => { @@ -1079,11 +1072,10 @@ describe('', () => { ); const textbox = screen.getByRole('textbox'); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + }).toErrorDev( 'The component expects a single value to match a given option but found 2 matches.', ); }); @@ -1092,19 +1084,22 @@ describe('', () => { const value = 'not a good value'; const options = ['first option', 'second option']; - render( - } - />, - ); - - expect(consoleWarnMock.callCount()).to.equal(4); // strict mode renders twice - expect(consoleWarnMock.messages()[0]).to.include( + expect(() => { + render( + } + />, + ); + }).toWarnDev([ 'None of the options match with `"not a good value"`', - ); + // strict mode renders twice + 'None of the options match with `"not a good value"`', + 'None of the options match with `"not a good value"`', + 'None of the options match with `"not a good value"`', + ]); }); it('warn if groups options are not sorted', () => { @@ -1117,21 +1112,24 @@ describe('', () => { { group: 2, value: 'F' }, { group: 1, value: 'C' }, ]; - const { getAllByRole } = render( - option.value} - renderInput={(params) => } - groupBy={(option) => option.group} - />, - ); - - const options = getAllByRole('option').map((el) => el.textContent); + expect(() => { + render( + option.value} + renderInput={(params) => } + groupBy={(option) => option.group} + />, + ); + }).toWarnDev([ + // strict mode renders twice + 'returns duplicated headers', + 'returns duplicated headers', + ]); + const options = screen.getAllByRole('option').map((el) => el.textContent); expect(options).to.have.length(7); expect(options).to.deep.equal(['A', 'D', 'E', 'B', 'G', 'F', 'C']); - expect(consoleWarnMock.callCount()).to.equal(2); // strict mode renders twice - expect(consoleWarnMock.messages()[0]).to.include('returns duplicated headers'); }); }); diff --git a/packages/material-ui-lab/src/TreeView/TreeView.test.js b/packages/material-ui-lab/src/TreeView/TreeView.test.js index 50c9f5d05cf543..056a9fcbc9704d 100644 --- a/packages/material-ui-lab/src/TreeView/TreeView.test.js +++ b/packages/material-ui-lab/src/TreeView/TreeView.test.js @@ -2,10 +2,10 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import { createClientRender, fireEvent, screen } from 'test/utils/createClientRender'; +import { ErrorBoundary } from 'test/utils/components'; import describeConformance from '@material-ui/core/test-utils/describeConformance'; import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import TreeView from './TreeView'; import TreeItem from '../TreeItem'; @@ -28,14 +28,6 @@ describe('', () => { })); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should warn when switching from controlled to uncontrolled of the expanded prop', () => { const { setProps } = render( @@ -43,8 +35,9 @@ describe('', () => { , ); - setProps({ expanded: undefined }); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + setProps({ expanded: undefined }); + }).toErrorDev( 'Material-UI: A component is changing the controlled expanded state of TreeView to be uncontrolled.', ); }); @@ -56,8 +49,9 @@ describe('', () => { , ); - setProps({ selected: undefined }); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + setProps({ selected: undefined }); + }).toErrorDev( 'Material-UI: A component is changing the controlled selected state of TreeView to be uncontrolled.', ); }); @@ -65,26 +59,6 @@ describe('', () => { // should not throw eventually or with a better error message // FIXME: https://github.com/mui-org/material-ui/issues/20832 it('crashes when unmounting with duplicate ids', () => { - class ErrorBoundary extends React.Component { - state = { error: null }; - - errors = []; - - static getDerivedStateFromError(error) { - return { error }; - } - - componentDidCatch(error) { - this.errors.push(error); - } - - render() { - if (this.state.error) { - return null; - } - return this.props.children; - } - } const CustomTreeItem = () => { return ; }; @@ -113,7 +87,12 @@ describe('', () => { , ); - screen.getByRole('button').click(); + expect(() => { + screen.getByRole('button').click(); + }).toErrorDev([ + 'RangeError: Maximum call stack size exceeded', + 'The above error occurred in the component', + ]); const { current: { errors }, diff --git a/packages/material-ui-styles/src/StylesProvider/StylesProvider.test.js b/packages/material-ui-styles/src/StylesProvider/StylesProvider.test.js index ce42cb69848cf3..379997f4c1c864 100644 --- a/packages/material-ui-styles/src/StylesProvider/StylesProvider.test.js +++ b/packages/material-ui-styles/src/StylesProvider/StylesProvider.test.js @@ -6,7 +6,6 @@ import createMount from 'test/utils/createMount'; import StylesProvider, { StylesContext } from './StylesProvider'; import makeStyles from '../makeStyles'; import createGenerateClassName from '../createGenerateClassName'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; function Test() { const options = React.useContext(StylesContext); @@ -136,25 +135,16 @@ describe('StylesProvider', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should support invalid input', () => { const jss = create(); - mount( - - - , - ); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: You cannot use the jss and injectFirst props at the same time', - ); + + expect(() => { + mount( + + + , + ); + }).toErrorDev('Material-UI: You cannot use the jss and injectFirst props at the same time'); }); }); }); diff --git a/packages/material-ui-styles/src/ThemeProvider/ThemeProvider.test.js b/packages/material-ui-styles/src/ThemeProvider/ThemeProvider.test.js index 9013d9095500ad..46238825dfaf8d 100644 --- a/packages/material-ui-styles/src/ThemeProvider/ThemeProvider.test.js +++ b/packages/material-ui-styles/src/ThemeProvider/ThemeProvider.test.js @@ -1,6 +1,5 @@ import React from 'react'; import { expect } from 'chai'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { createClientRender } from 'test/utils/createClientRender'; import makeStyles from '../makeStyles'; import useTheme from '../useTheme'; @@ -121,37 +120,35 @@ describe('ThemeProvider', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should warn about missing provider', () => { - render( - theme}> - - , - ); - expect(consoleErrorMock.callCount()).to.equal(2); // strict mode renders twice - expect(consoleErrorMock.messages()[0]).to.include('However, no outer theme is present.'); + expect(() => { + render( + theme}> + + , + ); + }).toErrorDev([ + 'However, no outer theme is present.', + // strict mode renders twice + 'However, no outer theme is present.', + ]); }); it('should warn about wrong theme function', () => { - render( - - {}}> - - - , - , - ); - expect(consoleErrorMock.callCount()).to.equal(2); // strict mode renders twice - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + render( + + {}}> + + + , + , + ); + }).toErrorDev([ 'Material-UI: You should return an object from your theme function', - ); + // strict mode renders twice + 'Material-UI: You should return an object from your theme function', + ]); }); }); }); diff --git a/packages/material-ui-styles/src/createGenerateClassName/createGenerateClassName.test.js b/packages/material-ui-styles/src/createGenerateClassName/createGenerateClassName.test.js index be1982354475e4..c747fdcceb5c25 100644 --- a/packages/material-ui-styles/src/createGenerateClassName/createGenerateClassName.test.js +++ b/packages/material-ui-styles/src/createGenerateClassName/createGenerateClassName.test.js @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import createGenerateClassName from './createGenerateClassName'; import nested from '../ThemeProvider/nested'; @@ -125,12 +124,10 @@ describe('createGenerateClassName', () => { before(() => { nodeEnv = env.NODE_ENV; env.NODE_ENV = 'production'; - consoleErrorMock.spy(); }); after(() => { env.NODE_ENV = nodeEnv; - consoleErrorMock.reset(); }); it('should output a short representation', () => { diff --git a/packages/material-ui-styles/src/createGenerateClassName/createGenerateClassNameHash.test.js b/packages/material-ui-styles/src/createGenerateClassName/createGenerateClassNameHash.test.js index 774e025b57fd61..62c86322fd64ca 100644 --- a/packages/material-ui-styles/src/createGenerateClassName/createGenerateClassNameHash.test.js +++ b/packages/material-ui-styles/src/createGenerateClassName/createGenerateClassNameHash.test.js @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import createGenerateClassNameHash from './createGenerateClassNameHash'; describe('createGenerateClassNameHash', () => { @@ -232,12 +231,10 @@ describe('createGenerateClassNameHash', () => { before(() => { nodeEnv = env.NODE_ENV; env.NODE_ENV = 'production'; - consoleErrorMock.spy(); }); after(() => { env.NODE_ENV = nodeEnv; - consoleErrorMock.reset(); }); it('should output a short representation', () => { diff --git a/packages/material-ui-styles/src/makeStyles/makeStyles.test.js b/packages/material-ui-styles/src/makeStyles/makeStyles.test.js index ecf6b8b430f4c9..cce608f6bd4c1f 100644 --- a/packages/material-ui-styles/src/makeStyles/makeStyles.test.js +++ b/packages/material-ui-styles/src/makeStyles/makeStyles.test.js @@ -6,7 +6,6 @@ import { act } from 'react-dom/test-utils'; import createMount from 'test/utils/createMount'; import { createMuiTheme } from '@material-ui/core/styles'; import createGenerateClassName from '../createGenerateClassName'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import makeStyles from './makeStyles'; import useTheme from '../useTheme'; import StylesProvider from '../StylesProvider'; @@ -69,76 +68,63 @@ describe('makeStyles', () => { describe('warnings', () => { const mountWithProps = createGetClasses({ root: {} }); - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should warn if providing a unknown key', () => { const output = mountWithProps(); + + expect(() => { + output.wrapper.setProps({ classes: { bar: 'foo' } }); + }).toErrorDev('Material-UI: The key `bar` provided to the classes prop is not implemented'); + const baseClasses = output.classes; - output.wrapper.setProps({ classes: { bar: 'foo' } }); const extendedClasses = output.classes; expect(extendedClasses).to.deep.equal({ root: baseClasses.root, bar: 'undefined foo' }); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: The key `bar` provided to the classes prop is not implemented', - ); }); it('should warn if providing a string', () => { const output = mountWithProps(); - output.wrapper.setProps({ classes: 'foo' }); - expect(consoleErrorMock.callCount() >= 1).to.equal(true); - const messages = consoleErrorMock.messages(); - expect(messages[messages.length - 1]).to.include( - 'You might want to use the className prop instead', - ); + + expect(() => { + output.wrapper.setProps({ classes: 'foo' }); + }).toErrorDev(['You might want to use the className prop instead']); }); it('should warn if providing a non string', () => { const output = mountWithProps(); const baseClasses = output.classes; - output.wrapper.setProps({ classes: { root: {} } }); + + expect(() => { + output.wrapper.setProps({ classes: { root: {} } }); + }).toErrorDev('Material-UI: The key `root` provided to the classes prop is not valid'); + const extendedClasses = output.classes; expect(extendedClasses).to.deep.equal({ root: `${baseClasses.root} [object Object]` }); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: The key `root` provided to the classes prop is not valid', - ); }); it('should warn if missing theme', () => { const styles = (theme) => ({ root: { padding: theme.spacing(2) } }); const mountWithProps2 = createGetClasses(styles); + expect(() => { - mountWithProps2({}); - }).to.throw('theme.spacing is not a function'); - expect(consoleErrorMock.callCount()).to.equal(4); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + mountWithProps2({}); + }).to.throw('theme.spacing is not a function'); + }).toErrorDev([ 'Material-UI: The `styles` argument provided is invalid.\nYou are providing a function without a theme in the context.', - ); - expect(consoleErrorMock.messages()[1]).to.include( 'Material-UI: The `styles` argument provided is invalid.\nYou are providing a function without a theme in the context.', - ); - expect(consoleErrorMock.messages()[2]).to.include( 'Uncaught [TypeError: theme.spacing is not a function', - ); - expect(consoleErrorMock.messages()[3]).to.include( 'The above error occurred in the component', - ); + ]); }); it('should warn but not throw if providing an invalid styles type', () => { - const mountWithProps2 = createGetClasses(undefined); + let mountWithProps2; - expect(consoleErrorMock.messages()).to.have.length(1); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + mountWithProps2 = createGetClasses(undefined); + }).toErrorDev( 'Material-UI: The `styles` argument provided is invalid.\nYou need to provide a function generating the styles or a styles object.', ); + expect(() => { mountWithProps2({}); }).not.to.throw(); diff --git a/packages/material-ui-styles/src/styled/styled.test.js b/packages/material-ui-styles/src/styled/styled.test.js index 55c6ab50edf322..18c244abe7bd35 100644 --- a/packages/material-ui-styles/src/styled/styled.test.js +++ b/packages/material-ui-styles/src/styled/styled.test.js @@ -5,7 +5,6 @@ import styled from './styled'; import { SheetsRegistry } from 'jss'; import createMount from 'test/utils/createMount'; import { createGenerateClassName } from '@material-ui/styles'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import StylesProvider from '../StylesProvider'; describe('styled', () => { @@ -87,26 +86,18 @@ describe('styled', () => { describe('warnings', () => { beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('warns if it cant detect the secondary action properly', () => { - PropTypes.checkPropTypes( - StyledButton.propTypes, - { clone: true, component: 'div' }, - 'prop', - 'StyledButton', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'You can not use the clone and component prop at the same time', - ); + expect(() => { + PropTypes.checkPropTypes( + StyledButton.propTypes, + { clone: true, component: 'div' }, + 'prop', + 'StyledButton', + ); + }).toErrorDev('You can not use the clone and component prop at the same time'); }); }); diff --git a/packages/material-ui-styles/src/withStyles/withStyles.test.js b/packages/material-ui-styles/src/withStyles/withStyles.test.js index c9c4fa1d34fc91..d2a231d0328c80 100644 --- a/packages/material-ui-styles/src/withStyles/withStyles.test.js +++ b/packages/material-ui-styles/src/withStyles/withStyles.test.js @@ -7,7 +7,6 @@ import { Input } from '@material-ui/core'; import createMount from 'test/utils/createMount'; import { isMuiElement } from '@material-ui/core/utils'; import { createMuiTheme } from '@material-ui/core/styles'; -// import consoleErrorMock from 'test/utils/consoleErrorMock'; import StylesProvider from '../StylesProvider'; import createGenerateClassName from '../createGenerateClassName'; import ThemeProvider from '../ThemeProvider'; diff --git a/packages/material-ui-styles/src/withTheme/withTheme.test.js b/packages/material-ui-styles/src/withTheme/withTheme.test.js index 307e6161a7f1d6..aa751716e747c0 100644 --- a/packages/material-ui-styles/src/withTheme/withTheme.test.js +++ b/packages/material-ui-styles/src/withTheme/withTheme.test.js @@ -4,7 +4,6 @@ import createMount from 'test/utils/createMount'; import { Input } from '@material-ui/core'; import { isMuiElement } from '@material-ui/core/utils'; import PropTypes from 'prop-types'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import withTheme from './withTheme'; import ThemeProvider from '../ThemeProvider'; @@ -75,27 +74,20 @@ describe('withTheme', () => { describe('innerRef', () => { beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('is deprecated', () => { const ThemedDiv = withTheme('div'); - PropTypes.checkPropTypes( - ThemedDiv.propTypes, - { innerRef: React.createRef() }, - 'prop', - 'ThemedDiv', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Warning: Failed prop type: Material-UI: The `innerRef` prop is deprecated', - ); + + expect(() => { + PropTypes.checkPropTypes( + ThemedDiv.propTypes, + { innerRef: React.createRef() }, + 'prop', + 'ThemedDiv', + ); + }).toErrorDev('Warning: Failed prop type: Material-UI: The `innerRef` prop is deprecated'); }); }); }); diff --git a/packages/material-ui-system/src/spacing.test.js b/packages/material-ui-system/src/spacing.test.js index 3bb621f9d2264c..5e9c066dfd38af 100644 --- a/packages/material-ui-system/src/spacing.test.js +++ b/packages/material-ui-system/src/spacing.test.js @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import spacing from './spacing'; describe('spacing', () => { @@ -40,47 +39,37 @@ describe('spacing', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should warn if the value overflow', () => { - const output = spacing({ - theme: { - spacing: [0, 3, 5], - }, - p: 3, - }); - expect(output).to.deep.equal({ padding: undefined }); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.match( - /Material-UI: The value provided \(3\) overflows\./, - ); - expect(consoleErrorMock.messages()[0]).to.match(/The supported values are: \[0,3,5\]\./); - expect(consoleErrorMock.messages()[0]).to.match( - /3 > 2, you need to add the missing values\./, + let output; + expect(() => { + output = spacing({ + theme: { + spacing: [0, 3, 5], + }, + p: 3, + }); + }).toErrorDev( + 'Material-UI: The value provided (3) overflows.\n' + + 'The supported values are: [0,3,5].\n' + + '3 > 2, you need to add the missing values.', ); + expect(output).to.deep.equal({ padding: undefined }); }); it('should warn if the theme transformer is invalid', () => { - const output = spacing({ - theme: { - spacing: {}, - }, - p: 3, - }); - expect(output).to.deep.equal({ padding: undefined }); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.match( - /Material-UI: The `theme.spacing` value \(\[object Object\]\) is invalid\./, - ); - expect(consoleErrorMock.messages()[0]).to.match( - /It should be a number, an array or a function\./, + let output; + expect(() => { + output = spacing({ + theme: { + spacing: {}, + }, + p: 3, + }); + }).toErrorDev( + 'Material-UI: The `theme.spacing` value ([object Object]) is invalid.\n' + + 'It should be a number, an array or a function.', ); + expect(output).to.deep.equal({ padding: undefined }); }); }); diff --git a/packages/material-ui-utils/src/chainPropTypes.test.js b/packages/material-ui-utils/src/chainPropTypes.test.js index 44352941a82a85..a4e1907d54f094 100644 --- a/packages/material-ui-utils/src/chainPropTypes.test.js +++ b/packages/material-ui-utils/src/chainPropTypes.test.js @@ -1,6 +1,5 @@ import { expect } from 'chai'; import PropTypes from 'prop-types'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import chainPropTypes from './chainPropTypes'; describe('chainPropTypes', () => { @@ -10,14 +9,9 @@ describe('chainPropTypes', () => { const location = 'prop'; beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should have the right shape', () => { expect(typeof chainPropTypes).to.equal('function'); }); @@ -31,19 +25,19 @@ describe('chainPropTypes', () => { location, componentName, ); - expect(consoleErrorMock.callCount()).to.equal(0); + expect(() => {}).not.toErrorDev(); }); it('should return an error for unsupported props', () => { - PropTypes.checkPropTypes( - { - [propName]: chainPropTypes(PropTypes.string, () => new Error('something is wrong')), - }, - props, - location, - componentName, - ); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.match(/something is wrong/); + expect(() => { + PropTypes.checkPropTypes( + { + [propName]: chainPropTypes(PropTypes.string, () => new Error('something is wrong')), + }, + props, + location, + componentName, + ); + }).toErrorDev(['something is wrong']); }); }); diff --git a/packages/material-ui-utils/src/elementAcceptingRef.test.js b/packages/material-ui-utils/src/elementAcceptingRef.test.js index 3146bafeca3e54..cb27c87ea69085 100644 --- a/packages/material-ui-utils/src/elementAcceptingRef.test.js +++ b/packages/material-ui-utils/src/elementAcceptingRef.test.js @@ -3,7 +3,6 @@ import { expect } from 'chai'; import * as PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import elementAcceptingRef from './elementAcceptingRef'; describe('elementAcceptingRef', () => { @@ -17,34 +16,28 @@ describe('elementAcceptingRef', () => { } beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - describe('acceptance when not required', () => { let rootNode; function assertPass(element, options = {}) { - const { failsOnMount = false, shouldMount = true } = options; - - checkPropType(element); - if (shouldMount) { - ReactDOM.render( - }> - {React.cloneElement(element, { ref: React.createRef() })} - , - rootNode, - ); + const { shouldMount = true } = options; + + function testAct() { + checkPropType(element); + if (shouldMount) { + ReactDOM.render( + }> + {React.cloneElement(element, { ref: React.createRef() })} + , + rootNode, + ); + } } - expect(consoleErrorMock.callCount()).to.equal( - failsOnMount ? 1 : 0, - `but got '${consoleErrorMock.messages()[0]}'`, - ); + expect(testAct).not.toErrorDev(); } before(() => { @@ -118,25 +111,24 @@ describe('elementAcceptingRef', () => { describe('rejections', () => { function assertFail(Component, hint) { - checkPropType(Component); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + checkPropType(Component); + }).toErrorDev( 'Invalid props `children` supplied to `DummyComponent`. ' + `Expected an element that can hold a ref. ${hint}`, ); } it('rejects undefined values when required', () => { - checkPropType(undefined, true); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include('marked as required'); + expect(() => { + checkPropType(undefined, true); + }).toErrorDev('marked as required'); }); it('rejects null values when required', () => { - checkPropType(null, true); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include('marked as required'); + expect(() => { + checkPropType(null, true); + }).toErrorDev('marked as required'); }); it('rejects function components', () => { diff --git a/packages/material-ui-utils/src/elementTypeAcceptingRef.test.js b/packages/material-ui-utils/src/elementTypeAcceptingRef.test.js index 9625f1572f79e1..7e6b527e635113 100644 --- a/packages/material-ui-utils/src/elementTypeAcceptingRef.test.js +++ b/packages/material-ui-utils/src/elementTypeAcceptingRef.test.js @@ -3,7 +3,6 @@ import { expect } from 'chai'; import * as PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import elementTypeAcceptingRef from './elementTypeAcceptingRef'; describe('elementTypeAcceptingRef', () => { @@ -17,34 +16,32 @@ describe('elementTypeAcceptingRef', () => { } beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - describe('acceptance', () => { let rootNode; function assertPass(Component, options = {}) { const { failsOnMount = false, shouldMount = true } = options; - checkPropType(Component); - if (shouldMount) { - ReactDOM.render( - }> - - , - rootNode, - ); + function testAct() { + checkPropType(Component); + if (shouldMount) { + ReactDOM.render( + }> + + , + rootNode, + ); + } } - expect(consoleErrorMock.callCount()).to.equal( - failsOnMount ? 1 : 0, - `but got '${consoleErrorMock.messages()[0]}'`, - ); + if (failsOnMount) { + expect(testAct).toErrorDev(''); + } else { + expect(testAct).not.toErrorDev(); + } } before(() => { @@ -118,10 +115,9 @@ describe('elementTypeAcceptingRef', () => { describe('rejections', () => { function assertFail(Component, hint) { - checkPropType(Component); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + checkPropType(Component); + }).toErrorDev( 'Invalid props `component` supplied to `DummyComponent`. ' + `Expected an element type that can hold a ref. ${hint}`, ); diff --git a/packages/material-ui/src/Accordion/Accordion.test.js b/packages/material-ui/src/Accordion/Accordion.test.js index 250363bbb6a84c..0ea7bceda5ec2a 100644 --- a/packages/material-ui/src/Accordion/Accordion.test.js +++ b/packages/material-ui/src/Accordion/Accordion.test.js @@ -5,7 +5,6 @@ import { spy } from 'sinon'; import { getClasses, findOutermostIntrinsic } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import describeConformance from '../test-utils/describeConformance'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import Paper from '../Paper'; import Accordion from './Accordion'; import AccordionSummary from '../AccordionSummary'; @@ -130,38 +129,29 @@ describe('', () => { describe('prop: children', () => { describe('first child', () => { beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('requires at least one child', () => { - PropTypes.checkPropTypes( - Accordion.Naked.propTypes, - { classes: {}, children: [] }, - 'prop', - 'MockedName', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include('Material-UI: Expected the first child'); + expect(() => { + PropTypes.checkPropTypes( + Accordion.Naked.propTypes, + { classes: {}, children: [] }, + 'prop', + 'MockedName', + ); + }).toErrorDev(['Material-UI: Expected the first child']); }); it('needs a valid element as the first child', () => { - PropTypes.checkPropTypes( - Accordion.Naked.propTypes, - { classes: {}, children: }, - 'prop', - 'MockedName', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - "Material-UI: The Accordion doesn't accept a Fragment", - ); + expect(() => { + PropTypes.checkPropTypes( + Accordion.Naked.propTypes, + { classes: {}, children: }, + 'prop', + 'MockedName', + ); + }).toErrorDev(["Material-UI: The Accordion doesn't accept a Fragment"]); }); }); @@ -175,31 +165,19 @@ describe('', () => { }); }); - describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - - it('should warn when switching from controlled to uncontrolled', () => { - const wrapper = mount({minimalChildren}); + it('should warn when switching from controlled to uncontrolled', () => { + const wrapper = mount({minimalChildren}); - wrapper.setProps({ expanded: undefined }); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: A component is changing the controlled expanded state of Accordion to be uncontrolled.', - ); - }); + expect(() => wrapper.setProps({ expanded: undefined })).to.toErrorDev( + 'Material-UI: A component is changing the controlled expanded state of Accordion to be uncontrolled.', + ); + }); - it('should warn when switching between uncontrolled to controlled', () => { - const wrapper = mount({minimalChildren}); + it('should warn when switching between uncontrolled to controlled', () => { + const wrapper = mount({minimalChildren}); - wrapper.setProps({ expanded: true }); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: A component is changing the uncontrolled expanded state of Accordion to be controlled.', - ); - }); + expect(() => wrapper.setProps({ expanded: true })).toErrorDev( + 'Material-UI: A component is changing the uncontrolled expanded state of Accordion to be controlled.', + ); }); }); diff --git a/packages/material-ui/src/Breadcrumbs/Breadcrumbs.test.js b/packages/material-ui/src/Breadcrumbs/Breadcrumbs.test.js index ed0e37f21478a3..f5c603b4563d48 100644 --- a/packages/material-ui/src/Breadcrumbs/Breadcrumbs.test.js +++ b/packages/material-ui/src/Breadcrumbs/Breadcrumbs.test.js @@ -4,8 +4,7 @@ import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import describeConformance from '../test-utils/describeConformance'; import Breadcrumbs from './Breadcrumbs'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; -import { createClientRender } from 'test/utils/createClientRender'; +import { createClientRender, screen } from 'test/utils/createClientRender'; describe('', () => { const mount = createMount(); @@ -82,17 +81,9 @@ describe('', () => { expect(getAllByRole('listitem', { hidden: false })).to.have.length(9); }); - describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - - it('should warn about invalid input', () => { - const { getAllByRole, getByRole } = render( + it('should warn about invalid input', () => { + expect(() => { + render( first second @@ -100,12 +91,11 @@ describe('', () => { fourth , ); - expect(getAllByRole('listitem', { hidden: false })).to.have.length(4); - expect(getByRole('list')).to.have.text('first/second/third/fourth'); - expect(consoleErrorMock.callCount()).to.equal(2); // strict mode renders twice - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: You have provided an invalid combination of props to the Breadcrumbs.\nitemsAfterCollapse={2} + itemsBeforeCollapse={2} >= maxItems={3}', - ); - }); + }).toErrorDev([ + 'Material-UI: You have provided an invalid combination of props to the Breadcrumbs.\nitemsAfterCollapse={2} + itemsBeforeCollapse={2} >= maxItems={3}', + 'Material-UI: You have provided an invalid combination of props to the Breadcrumbs.\nitemsAfterCollapse={2} + itemsBeforeCollapse={2} >= maxItems={3}', + ]); + expect(screen.getAllByRole('listitem', { hidden: false })).to.have.length(4); + expect(screen.getByRole('list')).to.have.text('first/second/third/fourth'); }); }); diff --git a/packages/material-ui/src/ButtonBase/ButtonBase.test.js b/packages/material-ui/src/ButtonBase/ButtonBase.test.js index 4450945c1177c7..e2c4c80147e7d1 100644 --- a/packages/material-ui/src/ButtonBase/ButtonBase.test.js +++ b/packages/material-ui/src/ButtonBase/ButtonBase.test.js @@ -7,7 +7,6 @@ import createMount from 'test/utils/createMount'; import describeConformance from '../test-utils/describeConformance'; import TouchRipple from './TouchRipple'; import ButtonBase from './ButtonBase'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { act, createClientRender, fireEvent } from 'test/utils/createClientRender'; import * as PropTypes from 'prop-types'; @@ -923,14 +922,9 @@ describe('', () => { describe('warnings', () => { beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('warns on invalid `component` prop: ref forward', () => { // Only run the test on node. On the browser the thrown error is not caught if (!/jsdom/.test(window.navigator.userAgent)) { @@ -945,15 +939,15 @@ describe('', () => { return ; } - PropTypes.checkPropTypes( - // @ts-ignore `Naked` is internal - ButtonBase.Naked.propTypes, - { classes: {}, component: Component }, - 'prop', - 'MockedName', - ); - - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + PropTypes.checkPropTypes( + // @ts-ignore `Naked` is internal + ButtonBase.Naked.propTypes, + { classes: {}, component: Component }, + 'prop', + 'MockedName', + ); + }).toErrorDev( 'Invalid prop `component` supplied to `MockedName`. Expected an element type that can hold a ref', ); }); @@ -966,10 +960,10 @@ describe('', () => { )); // cant match the error message here because flakiness with mocha watchmode - render(); - expect(consoleErrorMock.messages()[0]).to.include( - 'Please make sure the children prop is rendered in this custom component.', - ); + + expect(() => { + render(); + }).toErrorDev('Please make sure the children prop is rendered in this custom component.'); }); }); }); diff --git a/packages/material-ui/src/CardMedia/CardMedia.test.js b/packages/material-ui/src/CardMedia/CardMedia.test.js index fc5d76141d2291..8296bc64cf2cc3 100644 --- a/packages/material-ui/src/CardMedia/CardMedia.test.js +++ b/packages/material-ui/src/CardMedia/CardMedia.test.js @@ -5,7 +5,6 @@ import createMount from 'test/utils/createMount'; import { createClientRender } from 'test/utils/createClientRender'; import describeConformance from '../test-utils/describeConformance'; import CardMedia from './CardMedia'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import PropTypes from 'prop-types'; describe('', () => { @@ -78,18 +77,13 @@ describe('', () => { describe('warnings', () => { before(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - after(() => { - consoleErrorMock.reset(); - }); - it('warns when neither `children`, nor `image`, nor `src`, nor `component` are provided', () => { - PropTypes.checkPropTypes(CardMedia.Naked.propTypes, { classes: {} }, 'prop', 'MockedName'); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.contain( + expect(() => { + PropTypes.checkPropTypes(CardMedia.Naked.propTypes, { classes: {} }, 'prop', 'MockedName'); + }).toErrorDev( 'Material-UI: Either `children`, `image`, `src` or `component` prop must be specified.', ); }); diff --git a/packages/material-ui/src/GridList/GridList.test.js b/packages/material-ui/src/GridList/GridList.test.js index 910f7f09e015e8..d33db2efbac691 100644 --- a/packages/material-ui/src/GridList/GridList.test.js +++ b/packages/material-ui/src/GridList/GridList.test.js @@ -4,7 +4,6 @@ import { createShallow, getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import describeConformance from '../test-utils/describeConformance'; import GridList from './GridList'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; const tilesData = [ { @@ -157,26 +156,13 @@ describe('', () => { }); }); - describe('warnings', () => { - before(() => { - consoleErrorMock.spy(); - }); - - after(() => { - consoleErrorMock.reset(); - }); - - it('warns a Fragment is passed as a child', () => { + it('warns a Fragment is passed as a child', () => { + expect(() => { mount( , ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - "Material-UI: The GridList component doesn't accept a Fragment as a child.", - ); - }); + }).toErrorDev("Material-UI: The GridList component doesn't accept a Fragment as a child."); }); }); diff --git a/packages/material-ui/src/Hidden/HiddenCss.test.js b/packages/material-ui/src/Hidden/HiddenCss.test.js index e8d4b6cc8dda9c..d4ab4260cc64f1 100644 --- a/packages/material-ui/src/Hidden/HiddenCss.test.js +++ b/packages/material-ui/src/Hidden/HiddenCss.test.js @@ -4,7 +4,6 @@ import { createShallow, getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import HiddenCss from './HiddenCss'; import { createMuiTheme, MuiThemeProvider } from '../styles'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; const Foo = () => bar; @@ -140,26 +139,15 @@ describe('', () => { }); }); - describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - - it('warns about excess props (potentially undeclared breakpoints)', () => { + it('warns about excess props (potentially undeclared breakpoints)', () => { + expect(() => { mount( , ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: Unsupported props received by ``: xxlUp.', - ); - }); + }).toErrorDev( + 'Material-UI: Unsupported props received by ``: xxlUp.', + ); }); }); diff --git a/packages/material-ui/src/IconButton/IconButton.test.js b/packages/material-ui/src/IconButton/IconButton.test.js index b35cd77f3b2f82..f03f00e1c2d449 100644 --- a/packages/material-ui/src/IconButton/IconButton.test.js +++ b/packages/material-ui/src/IconButton/IconButton.test.js @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import describeConformance from '../test-utils/describeConformance'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { createClientRender } from 'test/utils/createClientRender'; import Icon from '../Icon'; import ButtonBase from '../ButtonBase'; @@ -124,28 +123,14 @@ describe('', () => { }); }); - describe('Firefox onClick', () => { - beforeEach(() => { - consoleErrorMock.spy(); - PropTypes.resetWarningCache(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - - it('should raise a warning', () => { + it('should raise a warning about onClick in children because of Firefox', () => { + expect(() => { PropTypes.checkPropTypes( IconButton.Naked.propTypes, { classes: {}, children: {}} /> }, 'prop', 'MockedName', ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: You are providing an onClick event listener', - ); - }); + }).toErrorDev(['Material-UI: You are providing an onClick event listener']); }); }); diff --git a/packages/material-ui/src/InputAdornment/InputAdornment.test.js b/packages/material-ui/src/InputAdornment/InputAdornment.test.js index d870e4378c33d5..9a6ac9ea565313 100644 --- a/packages/material-ui/src/InputAdornment/InputAdornment.test.js +++ b/packages/material-ui/src/InputAdornment/InputAdornment.test.js @@ -3,7 +3,6 @@ import { expect } from 'chai'; import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import describeConformance from '../test-utils/describeConformance'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { createClientRender } from 'test/utils/createClientRender'; import Typography from '../Typography'; import InputAdornment from './InputAdornment'; @@ -128,16 +127,8 @@ describe('', () => { expect(adornment).to.have.class(classes.filled); }); - describe('warnings', () => { - before(() => { - consoleErrorMock.spy(); - }); - - after(() => { - consoleErrorMock.reset(); - }); - - it('should warn if the variant supplied is equal to the variant inferred', () => { + it('should warn if the variant supplied is equal to the variant inferred', () => { + expect(() => { render( ', () => { /> , ); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.equal( - 'Material-UI: The `InputAdornment` variant infers the variant ' + - 'prop you do not have to provide one.', - ); - }); + }).toErrorDev( + 'Material-UI: The `InputAdornment` variant infers the variant ' + + 'prop you do not have to provide one.', + ); }); }); diff --git a/packages/material-ui/src/InputBase/InputBase.test.js b/packages/material-ui/src/InputBase/InputBase.test.js index fbcb007732f8d9..01db66fcae4812 100644 --- a/packages/material-ui/src/InputBase/InputBase.test.js +++ b/packages/material-ui/src/InputBase/InputBase.test.js @@ -6,7 +6,6 @@ import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import describeConformance from '../test-utils/describeConformance'; import { act, createClientRender, fireEvent } from 'test/utils/createClientRender'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import FormControl, { useFormControl } from '../FormControl'; import InputAdornment from '../InputAdornment'; import TextareaAutosize from '../TextareaAutosize'; @@ -479,39 +478,28 @@ describe('', () => { }); describe('registering input', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it("should warn if more than one input is rendered regarless how it's nested", () => { - render( - - - - {/* should work regarless how it's nested */} + expect(() => { + render( + - - , - ); - - expect(consoleErrorMock.callCount()).to.eq(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: There are multiple InputBase components inside a FormControl.', - ); + + {/* should work regarless how it's nested */} + + + , + ); + }).toErrorDev('Material-UI: There are multiple InputBase components inside a FormControl.'); }); it('should not warn if only one input is rendered', () => { - render( - - - , - ); - - expect(consoleErrorMock.callCount()).to.eq(0); + expect(() => { + render( + + + , + ); + }).not.toErrorDev(); }); it('should not warn when toggling between inputs', () => { @@ -536,9 +524,9 @@ describe('', () => { }; const { getByText } = render(); - fireEvent.click(getByText('toggle')); - - expect(consoleErrorMock.callCount()).to.eq(0); + expect(() => { + fireEvent.click(getByText('toggle')); + }).not.toErrorDev(); }); }); }); diff --git a/packages/material-ui/src/LinearProgress/LinearProgress.test.js b/packages/material-ui/src/LinearProgress/LinearProgress.test.js index 3ac0a871c54118..abe30ebe8a66b8 100644 --- a/packages/material-ui/src/LinearProgress/LinearProgress.test.js +++ b/packages/material-ui/src/LinearProgress/LinearProgress.test.js @@ -1,6 +1,5 @@ import * as React from 'react'; import { expect } from 'chai'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { getClasses } from '@material-ui/core/test-utils'; import { createClientRender, screen } from 'test/utils/createClientRender'; import createMount from 'test/utils/createMount'; @@ -151,31 +150,19 @@ describe('', () => { }); describe('prop: value', () => { - before(() => { - consoleErrorMock.spy(); - }); - - after(() => { - consoleErrorMock.reset(); - }); - it('should warn when not used as expected', () => { - const { rerender } = render(); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.match( - /Material-UI: You need to provide a value prop/, - ); - - rerender(); - - expect(consoleErrorMock.callCount()).to.equal(3); - expect(consoleErrorMock.messages()[1]).to.match( - /Material-UI: You need to provide a value prop/, - ); - expect(consoleErrorMock.messages()[2]).to.match( - /Material-UI: You need to provide a valueBuffer prop/, - ); + let rerender; + + expect(() => { + ({ rerender } = render()); + }).toErrorDev('Material-UI: You need to provide a value prop'); + + expect(() => { + rerender(); + }).toErrorDev([ + 'Material-UI: You need to provide a value prop', + 'Material-UI: You need to provide a valueBuffer prop', + ]); }); }); }); diff --git a/packages/material-ui/src/ListItem/ListItem.test.js b/packages/material-ui/src/ListItem/ListItem.test.js index f7db609f9ab0d1..25de891e7ddcfb 100644 --- a/packages/material-ui/src/ListItem/ListItem.test.js +++ b/packages/material-ui/src/ListItem/ListItem.test.js @@ -4,8 +4,7 @@ import PropTypes from 'prop-types'; import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import describeConformance from '../test-utils/describeConformance'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; -import { act, createClientRender, fireEvent, queries } from 'test/utils/createClientRender'; +import { act, createClientRender, fireEvent, queries, screen } from 'test/utils/createClientRender'; import ListItemText from '../ListItemText'; import ListItemSecondaryAction from '../ListItemSecondaryAction'; import ListItem from './ListItem'; @@ -155,39 +154,30 @@ describe('', () => { describe('warnings', () => { beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('warns if it cant detect the secondary action properly', () => { - PropTypes.checkPropTypes( - ListItem.Naked.propTypes, - { - classes: {}, - children: [ - I should have come last :(, - My position doesn not matter., - ], - }, - 'prop', - 'MockedName', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Warning: Failed prop type: Material-UI: You used an element', - ); + expect(() => { + PropTypes.checkPropTypes( + ListItem.Naked.propTypes, + { + classes: {}, + children: [ + I should have come last :(, + My position doesn not matter., + ], + }, + 'prop', + 'MockedName', + ); + }).toErrorDev('Warning: Failed prop type: Material-UI: You used an element'); }); it('should warn (but not error) with autoFocus with a function component with no content', () => { - render(); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + render(); + }).toErrorDev( 'Material-UI: Unable to set focus to a ListItem whose component has not been rendered.', ); }); @@ -205,18 +195,16 @@ describe('', () => { return ; } } - const { getByRole } = render( - - - - , - ); - - expect(getByRole('listitem')).toHaveFocus(); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'findDOMNode is deprecated in StrictMode', - ); + expect(() => { + render( + + + + , + ); + }).toErrorDev('findDOMNode is deprecated in StrictMode'); + + expect(screen.getByRole('listitem')).toHaveFocus(); }); }); }); diff --git a/packages/material-ui/src/Menu/Menu.test.js b/packages/material-ui/src/Menu/Menu.test.js index 300b5f31c7feb9..89f33e4cc7732f 100644 --- a/packages/material-ui/src/Menu/Menu.test.js +++ b/packages/material-ui/src/Menu/Menu.test.js @@ -7,7 +7,6 @@ import describeConformance from '../test-utils/describeConformance'; import Popover from '../Popover'; import Menu from './Menu'; import MenuList from '../MenuList'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; const MENU_LIST_HEIGHT = 100; @@ -217,25 +216,18 @@ describe('', () => { }); describe('warnings', () => { - before(() => { - consoleErrorMock.spy(); - }); - - after(() => { - consoleErrorMock.reset(); - }); - it('warns a Fragment is passed as a child', () => { - mount( - - - , - ); - - expect(consoleErrorMock.callCount()).to.equal(2); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + mount( + + + , + ); + }).toErrorDev([ + "Material-UI: The Menu component doesn't accept a Fragment as a child.", + // twice in StrictMode "Material-UI: The Menu component doesn't accept a Fragment as a child.", - ); + ]); }); }); }); diff --git a/packages/material-ui/src/Modal/Modal.test.js b/packages/material-ui/src/Modal/Modal.test.js index 0a9efe8afd9558..871214b6e4a99e 100644 --- a/packages/material-ui/src/Modal/Modal.test.js +++ b/packages/material-ui/src/Modal/Modal.test.js @@ -3,7 +3,6 @@ import * as ReactDOM from 'react-dom'; import { expect } from 'chai'; import { useFakeTimers, spy } from 'sinon'; import PropTypes from 'prop-types'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { createClientRender, fireEvent, within } from 'test/utils/createClientRender'; import { createMuiTheme } from '@material-ui/core/styles'; import createMount from 'test/utils/createMount'; @@ -390,12 +389,9 @@ describe('', () => { initialFocus.tabIndex = 0; document.body.appendChild(initialFocus); initialFocus.focus(); - - consoleErrorMock.spy(); }); afterEach(() => { - consoleErrorMock.reset(); document.body.removeChild(initialFocus); }); @@ -417,7 +413,7 @@ describe('', () => { const { getByTestId, setProps } = render( - + , ); diff --git a/packages/material-ui/src/Paper/Paper.test.js b/packages/material-ui/src/Paper/Paper.test.js index d29bf375806b65..d2cd527c166066 100644 --- a/packages/material-ui/src/Paper/Paper.test.js +++ b/packages/material-ui/src/Paper/Paper.test.js @@ -6,7 +6,6 @@ import * as PropTypes from 'prop-types'; import describeConformance from '../test-utils/describeConformance'; import Paper from './Paper'; import { createMuiTheme, ThemeProvider } from '../styles'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; describe('', () => { const mount = createMount(); @@ -68,26 +67,18 @@ describe('', () => { describe('warnings', () => { beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('warns if the given `elevation` is not implemented in the theme', () => { - PropTypes.checkPropTypes( - Paper.Naked.propTypes, - { classes: { elevation24: 'elevation-24', elevation26: 'elevation-26' }, elevation: 25 }, - 'prop', - 'MockedPaper', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: This elevation `25` is not implemented.', - ); + expect(() => { + PropTypes.checkPropTypes( + Paper.Naked.propTypes, + { classes: { elevation24: 'elevation-24', elevation26: 'elevation-26' }, elevation: 25 }, + 'prop', + 'MockedPaper', + ); + }).toErrorDev('Material-UI: This elevation `25` is not implemented.'); }); }); }); diff --git a/packages/material-ui/src/Popover/Popover.test.js b/packages/material-ui/src/Popover/Popover.test.js index 92b0b6502860d9..24549a60b0f500 100644 --- a/packages/material-ui/src/Popover/Popover.test.js +++ b/packages/material-ui/src/Popover/Popover.test.js @@ -5,7 +5,6 @@ import { findOutermostIntrinsic, getClasses } from '@material-ui/core/test-utils import createMount from 'test/utils/createMount'; import * as PropTypes from 'prop-types'; import describeConformance from '../test-utils/describeConformance'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import Grow from '../Grow'; import Modal from '../Modal'; import Paper from '../Paper'; @@ -428,36 +427,29 @@ describe('', () => { describe('warnings', () => { beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should warn if anchorEl is not valid', () => { - PropTypes.checkPropTypes( - Popover.Naked.propTypes, - { classes: {}, open: true }, - 'prop', - 'MockedPopover', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include('It should be an Element instance'); + expect(() => { + PropTypes.checkPropTypes( + Popover.Naked.propTypes, + { classes: {}, open: true }, + 'prop', + 'MockedPopover', + ); + }).toErrorDev('It should be an Element instance'); }); it('warns if a component for the Paper is used that cant hold a ref', () => { - PropTypes.checkPropTypes( - Popover.Naked.propTypes, - { ...defaultProps, classes: {}, PaperProps: { component: () => , elevation: 4 } }, - 'prop', - 'MockedPopover', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + PropTypes.checkPropTypes( + Popover.Naked.propTypes, + { ...defaultProps, classes: {}, PaperProps: { component: () => , elevation: 4 } }, + 'prop', + 'MockedPopover', + ); + }).toErrorDev( 'Warning: Failed prop type: Invalid prop `PaperProps.component` supplied to `MockedPopover`. Expected an element type that can hold a ref.', ); }); diff --git a/packages/material-ui/src/Popper/Popper.test.js b/packages/material-ui/src/Popper/Popper.test.js index 50c78b8a61e33a..4e12bf128f2723 100644 --- a/packages/material-ui/src/Popper/Popper.test.js +++ b/packages/material-ui/src/Popper/Popper.test.js @@ -6,7 +6,6 @@ import createMount from 'test/utils/createMount'; import { ThemeProvider, createMuiTheme } from '@material-ui/core/styles'; import describeConformance from '@material-ui/core/test-utils/describeConformance'; import { createClientRender, fireEvent } from 'test/utils/createClientRender'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import PopperJs from 'popper.js'; import Grow from '../Grow'; import Popper from './Popper'; @@ -272,28 +271,22 @@ describe('', () => { describe('warnings', () => { beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should warn if anchorEl is not valid', () => { - PropTypes.checkPropTypes( - Popper.propTypes, - { - ...defaultProps, - open: true, - anchorEl: null, - }, - 'prop', - 'MockedPopper', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include('It should be an HTML element instance'); + expect(() => { + PropTypes.checkPropTypes( + Popper.propTypes, + { + ...defaultProps, + open: true, + anchorEl: null, + }, + 'prop', + 'MockedPopper', + ); + }).toErrorDev('It should be an HTML element instance'); }); }); }); diff --git a/packages/material-ui/src/Portal/Portal.test.js b/packages/material-ui/src/Portal/Portal.test.js index 4e47d3fc61b0e5..85fc0a6a29d55b 100644 --- a/packages/material-ui/src/Portal/Portal.test.js +++ b/packages/material-ui/src/Portal/Portal.test.js @@ -2,7 +2,6 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import createServerRender from 'test/utils/createServerRender'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { createClientRender } from 'test/utils/createClientRender'; import Portal from './Portal'; @@ -16,22 +15,25 @@ describe('', () => { return; } - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('render nothing on the server', () => { const markup1 = serverRender(Bar); expect(markup1.text()).to.equal('Bar'); - const markup2 = serverRender( - - Bar - , + let markup2; + expect(() => { + markup2 = serverRender( + + Bar + , + ); + }).toErrorDev( + // Known issue due to using SSR APIs in a browser environment. + // We use 3x useLayoutEffect in the component. + [ + 'Warning: useLayoutEffect does nothing on the server', + 'Warning: useLayoutEffect does nothing on the server', + 'Warning: useLayoutEffect does nothing on the server', + ], ); expect(markup2.text()).to.equal(''); }); diff --git a/packages/material-ui/src/RadioGroup/RadioGroup.test.js b/packages/material-ui/src/RadioGroup/RadioGroup.test.js index 718377c5d8714d..915663e43a3144 100644 --- a/packages/material-ui/src/RadioGroup/RadioGroup.test.js +++ b/packages/material-ui/src/RadioGroup/RadioGroup.test.js @@ -9,7 +9,6 @@ import { createClientRender } from 'test/utils/createClientRender'; import FormGroup from '../FormGroup'; import Radio from '../Radio'; import RadioGroup from './RadioGroup'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import useRadioGroup from './useRadioGroup'; describe('', () => { @@ -211,15 +210,6 @@ describe('', () => { }); describe('with non-string values', () => { - before(() => { - // swallow prop-types warnings - consoleErrorMock.spy(); - }); - - after(() => { - consoleErrorMock.reset(); - }); - it('passes the value of the selected Radio as a string', () => { function selectNth(wrapper, n) { return wrapper.find('input[type="radio"]').at(n).simulate('change'); @@ -325,14 +315,6 @@ describe('', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should warn when switching from controlled to uncontrolled', () => { const wrapper = mount( @@ -340,8 +322,9 @@ describe('', () => { , ); - wrapper.setProps({ value: undefined }); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + wrapper.setProps({ value: undefined }); + }).toErrorDev( 'Material-UI: A component is changing the controlled value state of RadioGroup to be uncontrolled.', ); }); @@ -353,8 +336,9 @@ describe('', () => { , ); - wrapper.setProps({ value: 'foo' }); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + wrapper.setProps({ value: 'foo' }); + }).toErrorDev( 'Material-UI: A component is changing the uncontrolled value state of RadioGroup to be controlled.', ); }); diff --git a/packages/material-ui/src/Select/Select.test.js b/packages/material-ui/src/Select/Select.test.js index ac3f47dafbe5f3..cd36a6618b13ec 100644 --- a/packages/material-ui/src/Select/Select.test.js +++ b/packages/material-ui/src/Select/Select.test.js @@ -4,7 +4,7 @@ import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import describeConformance from '@material-ui/core/test-utils/describeConformance'; import { act, createClientRender, fireEvent, screen } from 'test/utils/createClientRender'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; +import { ErrorBoundary } from 'test/utils/components'; import MenuItem from '../MenuItem'; import Input from '../Input'; import InputLabel from '../InputLabel'; @@ -808,30 +808,34 @@ describe('', () => { }); describe('errors', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should throw if non array', function test() { + // TODO is this fixed? if (!/jsdom/.test(window.navigator.userAgent)) { // can't catch render errors in the browser for unknown reason // tried try-catch + error boundary + window onError preventDefault this.skip(); } + const errorRef = React.createRef(); expect(() => { render( - - Ten - Twenty - Thirty - , + + + Ten + Twenty + Thirty + + , ); - }).to.throw(/Material-UI: The `value` prop must be an array/); + }).toErrorDev([ + 'Material-UI: The `value` prop must be an array', + 'The above error occurred in the component', + ]); + const { + current: { errors }, + } = errorRef; + expect(errors).to.have.length(1); + expect(errors[0].toString()).to.include('Material-UI: The `value` prop must be an array'); }); }); diff --git a/packages/material-ui/src/Slider/Slider.test.js b/packages/material-ui/src/Slider/Slider.test.js index c6fe1873a6877d..427291f96184e5 100644 --- a/packages/material-ui/src/Slider/Slider.test.js +++ b/packages/material-ui/src/Slider/Slider.test.js @@ -7,7 +7,6 @@ import createMount from 'test/utils/createMount'; import describeConformance from '@material-ui/core/test-utils/describeConformance'; import { ThemeProvider, createMuiTheme } from '@material-ui/core/styles'; import { createClientRender, fireEvent } from 'test/utils/createClientRender'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import Slider from './Slider'; function createTouches(touches) { @@ -481,47 +480,37 @@ describe('', () => { describe('warnings', () => { beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should warn if aria-valuetext is provided', () => { - PropTypes.checkPropTypes( - Slider.Naked.propTypes, - { classes: {}, value: [20, 50], 'aria-valuetext': 'hot' }, - 'prop', - 'MockedSlider', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: You need to use the `getAriaValueText` prop instead of', - ); + expect(() => { + PropTypes.checkPropTypes( + Slider.Naked.propTypes, + { classes: {}, value: [20, 50], 'aria-valuetext': 'hot' }, + 'prop', + 'MockedSlider', + ); + }).toErrorDev('Material-UI: You need to use the `getAriaValueText` prop instead of'); }); it('should warn if aria-label is provided', () => { - PropTypes.checkPropTypes( - Slider.Naked.propTypes, - { classes: {}, value: [20, 50], 'aria-label': 'hot' }, - 'prop', - 'MockedSlider', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: You need to use the `getAriaLabel` prop instead of', - ); + expect(() => { + PropTypes.checkPropTypes( + Slider.Naked.propTypes, + { classes: {}, value: [20, 50], 'aria-label': 'hot' }, + 'prop', + 'MockedSlider', + ); + }).toErrorDev('Material-UI: You need to use the `getAriaLabel` prop instead of'); }); it('should warn when switching from controlled to uncontrolled', () => { const { setProps } = render(); - setProps({ value: undefined }); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + setProps({ value: undefined }); + }).toErrorDev( 'Material-UI: A component is changing the controlled value state of Slider to be uncontrolled.', ); }); @@ -529,8 +518,9 @@ describe('', () => { it('should warn when switching between uncontrolled to controlled', () => { const { setProps } = render(); - setProps({ value: [20, 50] }); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + setProps({ value: [20, 50] }); + }).toErrorDev( 'Material-UI: A component is changing the uncontrolled value state of Slider to be controlled.', ); }); diff --git a/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js b/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js index d5e8afd82d6e43..36fd0634825094 100644 --- a/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js +++ b/packages/material-ui/src/SwipeableDrawer/SwipeableDrawer.test.js @@ -4,7 +4,6 @@ import { spy } from 'sinon'; import createMount from 'test/utils/createMount'; import describeConformance from '@material-ui/core/test-utils/describeConformance'; import PropTypes, { checkPropTypes } from 'prop-types'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import Drawer from '../Drawer'; import SwipeableDrawer, { reset } from './SwipeableDrawer'; import SwipeArea from './SwipeArea'; @@ -508,48 +507,41 @@ describe('', () => { describe('warnings', () => { beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('warns if a component for the Paper is used that cant hold a ref', () => { - checkPropTypes( - SwipeableDrawer.propTypes, - { - onOpen: () => {}, - onClose: () => {}, - open: false, - PaperProps: { component: () => , elevation: 4 }, - }, - 'prop', - 'MockedSwipeableDrawer', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + checkPropTypes( + SwipeableDrawer.propTypes, + { + onOpen: () => {}, + onClose: () => {}, + open: false, + PaperProps: { component: () => , elevation: 4 }, + }, + 'prop', + 'MockedSwipeableDrawer', + ); + }).toErrorDev( 'Warning: Failed prop type: Invalid prop `PaperProps.component` supplied to `MockedSwipeableDrawer`. Expected an element type that can hold a ref.', ); }); it('warns if a component for the Backdrop is used that cant hold a ref', () => { - checkPropTypes( - SwipeableDrawer.propTypes, - { - onOpen: () => {}, - onClose: () => {}, - open: false, - ModalProps: { BackdropProps: { component: () => , 'data-backdrop': true } }, - }, - 'prop', - 'MockedSwipeableDrawer', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + checkPropTypes( + SwipeableDrawer.propTypes, + { + onOpen: () => {}, + onClose: () => {}, + open: false, + ModalProps: { BackdropProps: { component: () => , 'data-backdrop': true } }, + }, + 'prop', + 'MockedSwipeableDrawer', + ); + }).toErrorDev( 'Warning: Failed prop type: Invalid prop `ModalProps.BackdropProps.component` supplied to `MockedSwipeableDrawer`. Expected an element type that can hold a ref.', ); }); diff --git a/packages/material-ui/src/TablePagination/TablePagination.test.js b/packages/material-ui/src/TablePagination/TablePagination.test.js index 772665fc973d99..f8108f1ed4732d 100644 --- a/packages/material-ui/src/TablePagination/TablePagination.test.js +++ b/packages/material-ui/src/TablePagination/TablePagination.test.js @@ -6,7 +6,6 @@ import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import { fireEvent, createClientRender } from 'test/utils/createClientRender'; import describeConformance from '../test-utils/describeConformance'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import TableFooter from '../TableFooter'; import TableCell from '../TableCell'; import TableRow from '../TableRow'; @@ -352,31 +351,25 @@ describe('', () => { describe('warnings', () => { before(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - after(() => { - consoleErrorMock.reset(); - }); - it('should raise a warning if the page prop is out of range', () => { - PropTypes.checkPropTypes( - TablePagination.Naked.propTypes, - { - classes: {}, - page: 2, - count: 20, - rowsPerPage: 10, - onChangePage: noop, - onChangeRowsPerPage: noop, - }, - 'prop', - 'MockedTablePagination', - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + PropTypes.checkPropTypes( + TablePagination.Naked.propTypes, + { + classes: {}, + page: 2, + count: 20, + rowsPerPage: 10, + onChangePage: noop, + onChangeRowsPerPage: noop, + }, + 'prop', + 'MockedTablePagination', + ); + }).toErrorDev( 'Material-UI: The page prop of a TablePagination is out of range (0 to 1, but page is 2).', ); }); diff --git a/packages/material-ui/src/Tabs/Tabs.test.js b/packages/material-ui/src/Tabs/Tabs.test.js index 6d293c3318063f..d5cd9ab156854f 100644 --- a/packages/material-ui/src/Tabs/Tabs.test.js +++ b/packages/material-ui/src/Tabs/Tabs.test.js @@ -1,7 +1,6 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy, useFakeTimers } from 'sinon'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import { createClientRender, fireEvent, screen } from 'test/utils/createClientRender'; @@ -82,19 +81,16 @@ describe('', () => { }); describe('warnings', () => { - before(() => { - consoleErrorMock.spy(); - }); - - after(() => { - consoleErrorMock.reset(); - }); - it('should warn if the input is invalid', () => { - render(); - expect(consoleErrorMock.messages()[0]).to.match( - /Material-UI: You can not use the `centered={true}` and `variant="scrollable"`/, - ); + expect(() => { + render(); + }).toErrorDev([ + 'Material-UI: You can not use the `centered={true}` and `variant="scrollable"`', + // StrictMode renders twice + 'Material-UI: You can not use the `centered={true}` and `variant="scrollable"`', + 'Material-UI: You can not use the `centered={true}` and `variant="scrollable"`', + 'Material-UI: You can not use the `centered={true}` and `variant="scrollable"`', + ]); }); }); @@ -260,25 +256,21 @@ describe('', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('warns when the value is not present in any tab', () => { - render( - - - - , - ); - expect(consoleErrorMock.callCount()).to.equal(4); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + render( + + + + , + ); + }).toErrorDev([ 'You can provide one of the following values: 1, 3', - ); + // StrictMode renders twice + 'You can provide one of the following values: 1, 3', + 'You can provide one of the following values: 1, 3', + 'You can provide one of the following values: 1, 3', + ]); }); }); }); diff --git a/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js b/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js index 09daadd961d059..ef6533f4ba9571 100644 --- a/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js +++ b/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js @@ -4,7 +4,6 @@ import sinon, { spy, stub, useFakeTimers } from 'sinon'; import createMount from 'test/utils/createMount'; import { createClientRender, fireEvent } from 'test/utils/createClientRender'; import describeConformance from '@material-ui/core/test-utils/describeConformance'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import TextareaAutosize from './TextareaAutosize'; describe('', () => { @@ -236,14 +235,6 @@ describe('', () => { }); describe('warnings', () => { - before(() => { - consoleErrorMock.spy(); - }); - - after(() => { - consoleErrorMock.reset(); - }); - it('warns if layout is unstable but not crash', () => { const { container, forceUpdate } = render(); const input = container.querySelector('textarea[aria-hidden=null]'); @@ -259,10 +250,15 @@ describe('', () => { return 15 + index; }, }); - forceUpdate(); - expect(consoleErrorMock.callCount()).to.equal(3); // strict mode renders twice - expect(consoleErrorMock.messages()[0]).to.include('Material-UI: Too many re-renders.'); + expect(() => { + forceUpdate(); + }).toErrorDev([ + 'Material-UI: Too many re-renders.', + // strict mode renders twice + 'Material-UI: Too many re-renders.', + 'Material-UI: Too many re-renders.', + ]); }); }); }); diff --git a/packages/material-ui/src/Tooltip/Tooltip.test.js b/packages/material-ui/src/Tooltip/Tooltip.test.js index 792c1adeda95da..396c4eb239c546 100644 --- a/packages/material-ui/src/Tooltip/Tooltip.test.js +++ b/packages/material-ui/src/Tooltip/Tooltip.test.js @@ -1,7 +1,6 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy, useFakeTimers } from 'sinon'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import { act, createClientRender, fireEvent } from 'test/utils/createClientRender'; @@ -380,36 +379,29 @@ describe('', () => { }); describe('disabled button warning', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should not raise a warning if title is empty', () => { - render( - - - Hello World - - , - ); - expect(consoleErrorMock.callCount()).to.equal(0); + expect(() => { + render( + + + Hello World + + , + ); + }).not.toErrorDev(); }); it('should raise a warning when we are uncontrolled and can not listen to events', () => { - render( - - - Hello World - - , - ); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.match( - /Material-UI: You are providing a disabled `button` child to the Tooltip component/, + expect(() => { + render( + + + Hello World + + , + ); + }).toErrorDev( + 'Material-UI: You are providing a disabled `button` child to the Tooltip component', ); }); @@ -421,7 +413,7 @@ describe('', () => { , ); - expect(consoleErrorMock.callCount()).to.equal(0); + expect(() => {}).not.toErrorDev(); }); }); @@ -593,14 +585,6 @@ describe('', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should warn when switching between uncontrolled to controlled', () => { const { setProps } = render( @@ -610,8 +594,9 @@ describe('', () => { , ); - setProps({ open: true }); - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + setProps({ open: true }); + }).toErrorDev( 'Material-UI: A component is changing the uncontrolled open state of Tooltip to be controlled.', ); }); diff --git a/packages/material-ui/src/Unstable_TrapFocus/Unstable_TrapFocus.test.js b/packages/material-ui/src/Unstable_TrapFocus/Unstable_TrapFocus.test.js index e8664c58f6c1e3..bd09c86bab3871 100644 --- a/packages/material-ui/src/Unstable_TrapFocus/Unstable_TrapFocus.test.js +++ b/packages/material-ui/src/Unstable_TrapFocus/Unstable_TrapFocus.test.js @@ -2,7 +2,6 @@ import * as React from 'react'; import { useFakeTimers } from 'sinon'; import { expect } from 'chai'; import TrapFocus from './Unstable_TrapFocus'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { createClientRender, fireEvent, screen } from 'test/utils/createClientRender'; describe('', () => { @@ -15,8 +14,6 @@ describe('', () => { let initialFocus = null; beforeEach(() => { - consoleErrorMock.spy(); - initialFocus = document.createElement('button'); initialFocus.tabIndex = 0; document.body.appendChild(initialFocus); @@ -24,7 +21,6 @@ describe('', () => { }); afterEach(() => { - consoleErrorMock.reset(); document.body.removeChild(initialFocus); }); @@ -62,20 +58,17 @@ describe('', () => { it('should warn if the modal content is not focusable', () => { const UnfocusableDialog = React.forwardRef((_, ref) => ); - render( - - - , - ); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: The modal content node does not accept focus', - ); + expect(() => { + render( + + + , + ); + }).toErrorDev('Material-UI: The modal content node does not accept focus'); }); it('should not attempt to focus nonexistent children', () => { - const EmptyDialog = () => null; + const EmptyDialog = React.forwardRef(() => null); render( diff --git a/packages/material-ui/src/internal/SwitchBase.test.js b/packages/material-ui/src/internal/SwitchBase.test.js index 41a38bc0734ad6..27b143abf62107 100644 --- a/packages/material-ui/src/internal/SwitchBase.test.js +++ b/packages/material-ui/src/internal/SwitchBase.test.js @@ -4,20 +4,12 @@ import { spy } from 'sinon'; import { getClasses } from '@material-ui/core/test-utils'; import createMount from 'test/utils/createMount'; import describeConformance from '../test-utils/describeConformance'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { createClientRender } from 'test/utils/createClientRender'; import SwitchBase from './SwitchBase'; import FormControl, { useFormControl } from '../FormControl'; import IconButton from '../IconButton'; -const shouldSuccessOnce = (name) => (func) => () => { - global.successOnce = global.successOnce || {}; - - if (!global.successOnce[name]) { - func(); - global.successOnce[name] = true; - } -}; +let didWarnControlledToUncontrolled; describe('', () => { const render = createClientRender(); @@ -353,51 +345,44 @@ describe('', () => { }); describe('check transitioning between controlled states throws errors', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); + it('should error when uncontrolled and changed to controlled', function test() { + if (didWarnControlledToUncontrolled) { + this.skip(); + } - it( - 'should error when uncontrolled and changed to controlled', - shouldSuccessOnce('didWarnUncontrolledToControlled')(() => { - const wrapper = render( + let setProps; + expect(() => { + ({ setProps } = render( , - ); - - expect(consoleErrorMock.callCount()).to.equal(0); + )); + }).not.toErrorDev(); + + expect(() => { + setProps({ checked: true }); + }).toErrorDev([ + 'Warning: A component is changing an uncontrolled input of type checkbox to be controlled.', + 'Material-UI: A component is changing the uncontrolled checked state of SwitchBase to be controlled.', + ]); + }); - wrapper.setProps({ checked: true }); - expect(consoleErrorMock.callCount()).to.equal(2); - expect(consoleErrorMock.messages()[0]).to.include( - 'Warning: A component is changing an uncontrolled input of type checkbox to be controlled.', - ); - expect(consoleErrorMock.messages()[1]).to.include( - 'Material-UI: A component is changing the uncontrolled checked state of SwitchBase to be controlled.', - ); - }), - ); + it('should error when controlled and changed to uncontrolled', function test() { + if (didWarnControlledToUncontrolled) { + this.skip(); + } - it( - 'should error when controlled and changed to uncontrolled', - shouldSuccessOnce('didWarnControlledToUncontrolled')(() => { - const { setProps } = render( + let setProps; + expect(() => { + ({ setProps } = render( , - ); - expect(consoleErrorMock.callCount()).to.equal(0); + )); + }).not.toErrorDev(); + expect(() => { setProps({ checked: undefined }); - expect(consoleErrorMock.callCount()).to.equal(2); - expect(consoleErrorMock.messages()[0]).to.include( - 'Warning: A component is changing a controlled input of type checkbox to be uncontrolled.', - ); - expect(consoleErrorMock.messages()[1]).to.include( - 'Material-UI: A component is changing the controlled checked state of SwitchBase to be uncontrolled.', - ); - }), - ); + }).toErrorDev([ + 'Warning: A component is changing a controlled input of type checkbox to be uncontrolled.', + 'Material-UI: A component is changing the controlled checked state of SwitchBase to be uncontrolled.', + ]); + }); }); }); diff --git a/packages/material-ui/src/styles/colorManipulator.test.js b/packages/material-ui/src/styles/colorManipulator.test.js index 2f2ac91237523f..6c5e53f98c868a 100644 --- a/packages/material-ui/src/styles/colorManipulator.test.js +++ b/packages/material-ui/src/styles/colorManipulator.test.js @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { recomposeColor, hexToRgb, @@ -15,14 +14,6 @@ import { } from './colorManipulator'; describe('utils/colorManipulator', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - describe('recomposeColor', () => { it('converts a decomposed rgb color object to a string` ', () => { expect( @@ -246,13 +237,15 @@ describe('utils/colorManipulator', () => { }); it("doesn't overshoot if an above-range coefficient is supplied", () => { - expect(darken('rgb(0, 127, 255)', 1.5)).to.equal('rgb(0, 0, 0)'); - expect(consoleErrorMock.callCount()).to.equal(1); + expect(() => { + expect(darken('rgb(0, 127, 255)', 1.5)).to.equal('rgb(0, 0, 0)'); + }).toErrorDev('Material-UI: The value provided 1.5 is out of range [0, 1].'); }); it("doesn't overshoot if a below-range coefficient is supplied", () => { - expect(darken('rgb(0, 127, 255)', -0.1)).to.equal('rgb(0, 127, 255)'); - expect(consoleErrorMock.callCount()).to.equal(1); + expect(() => { + expect(darken('rgb(0, 127, 255)', -0.1)).to.equal('rgb(0, 127, 255)'); + }).toErrorDev('Material-UI: The value provided -0.1 is out of range [0, 1].'); }); it('darkens rgb white to black when coefficient is 1', () => { @@ -298,13 +291,15 @@ describe('utils/colorManipulator', () => { }); it("doesn't overshoot if an above-range coefficient is supplied", () => { - expect(lighten('rgb(0, 127, 255)', 1.5)).to.equal('rgb(255, 255, 255)'); - expect(consoleErrorMock.callCount()).to.equal(1); + expect(() => { + expect(lighten('rgb(0, 127, 255)', 1.5)).to.equal('rgb(255, 255, 255)'); + }).toErrorDev('Material-UI: The value provided 1.5 is out of range [0, 1].'); }); it("doesn't overshoot if a below-range coefficient is supplied", () => { - expect(lighten('rgb(0, 127, 255)', -0.1)).to.equal('rgb(0, 127, 255)'); - expect(consoleErrorMock.callCount()).to.equal(1); + expect(() => { + expect(lighten('rgb(0, 127, 255)', -0.1)).to.equal('rgb(0, 127, 255)'); + }).toErrorDev('Material-UI: The value provided -0.1 is out of range [0, 1].'); }); it('lightens rgb black to white when coefficient is 1', () => { diff --git a/packages/material-ui/src/styles/createMuiTheme.test.js b/packages/material-ui/src/styles/createMuiTheme.test.js index 9e89726cbe1860..0a44017d3656d8 100644 --- a/packages/material-ui/src/styles/createMuiTheme.test.js +++ b/packages/material-ui/src/styles/createMuiTheme.test.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import createMuiTheme from './createMuiTheme'; import { deepOrange, green } from '../colors'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; describe('createMuiTheme', () => { it('should have a palette', () => { @@ -90,28 +89,24 @@ describe('createMuiTheme', () => { }); describe('overrides', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should warn when trying to override an internal state the wrong way', () => { let theme; - theme = createMuiTheme({ overrides: { Button: { disabled: { color: 'blue' } } } }); + expect(() => { + theme = createMuiTheme({ overrides: { Button: { disabled: { color: 'blue' } } } }); + }).not.toErrorDev(); expect(Object.keys(theme.overrides.Button.disabled).length).to.equal(1); - expect(consoleErrorMock.messages().length).to.equal(0); - theme = createMuiTheme({ overrides: { MuiButton: { root: { color: 'blue' } } } }); - expect(consoleErrorMock.messages().length).to.equal(0); - theme = createMuiTheme({ overrides: { MuiButton: { disabled: { color: 'blue' } } } }); - expect(Object.keys(theme.overrides.MuiButton.disabled).length).to.equal(0); - expect(consoleErrorMock.messages().length).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.match( - /Material-UI: The `MuiButton` component increases the CSS specificity of the `disabled` internal state./, + + expect(() => { + theme = createMuiTheme({ overrides: { MuiButton: { root: { color: 'blue' } } } }); + }).not.toErrorDev(); + + expect(() => { + theme = createMuiTheme({ overrides: { MuiButton: { disabled: { color: 'blue' } } } }); + }).toErrorDev( + 'Material-UI: The `MuiButton` component increases the CSS specificity of the `disabled` internal state.', ); + expect(Object.keys(theme.overrides.MuiButton.disabled).length).to.equal(0); }); }); diff --git a/packages/material-ui/src/styles/createPalette.test.js b/packages/material-ui/src/styles/createPalette.test.js index a482270dddae77..ab46abf194e03c 100644 --- a/packages/material-ui/src/styles/createPalette.test.js +++ b/packages/material-ui/src/styles/createPalette.test.js @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { deepOrange, indigo, pink } from '../colors'; import { darken, lighten } from './colorManipulator'; import createPalette, { dark, light } from './createPalette'; @@ -136,20 +135,10 @@ describe('createPalette()', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('throws an exception when an invalid type is specified', () => { - createPalette({ type: 'foo' }); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: The palette type `foo` is not supported', - ); + expect(() => { + createPalette({ type: 'foo' }); + }).toErrorDev('Material-UI: The palette type `foo` is not supported'); }); it('throws an exception when a wrong color is provided', () => { @@ -162,16 +151,19 @@ describe('createPalette()', () => { }); it('logs an error when the contrast ratio does not reach AA', () => { - const { getContrastText } = createPalette({ - contrastThreshold: 0, - }); - - getContrastText('#fefefe'); - - expect(consoleErrorMock.callCount()).to.equal(3); - expect(consoleErrorMock.messages()[0]).to.include( + let getContrastText; + expect(() => { + ({ getContrastText } = createPalette({ + contrastThreshold: 0, + })); + }).toErrorDev([ 'falls below the WCAG recommended absolute minimum contrast ratio of 3:1', - ); + 'falls below the WCAG recommended absolute minimum contrast ratio of 3:1', + ]); + + expect(() => { + getContrastText('#fefefe'); + }).toErrorDev('falls below the WCAG recommended absolute minimum contrast ratio of 3:1'); }); }); }); diff --git a/packages/material-ui/src/styles/createSpacing.test.js b/packages/material-ui/src/styles/createSpacing.test.js index dc29b62899e1b4..ecacde6ed4b259 100644 --- a/packages/material-ui/src/styles/createSpacing.test.js +++ b/packages/material-ui/src/styles/createSpacing.test.js @@ -1,6 +1,5 @@ import { expect } from 'chai'; import createSpacing from './createSpacing'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; describe('createSpacing', () => { it('should work as expected', () => { @@ -47,32 +46,20 @@ describe('createSpacing', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - // TODO v5: remove it('should warn for the deprecated API', () => { const spacing = createSpacing(11); - expect(spacing.unit).to.equal(11); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'theme.spacing.unit usage has been deprecated', - ); + expect(() => { + expect(spacing.unit).to.equal(11); + }).toErrorDev('theme.spacing.unit usage has been deprecated'); }); it('should warn for wrong input', () => { - createSpacing({ - unit: 4, - }); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: The `theme.spacing` value ([object Object]) is invalid', - ); + expect(() => { + createSpacing({ + unit: 4, + }); + }).toErrorDev('Material-UI: The `theme.spacing` value ([object Object]) is invalid'); }); }); }); diff --git a/packages/material-ui/src/styles/createTypography.test.js b/packages/material-ui/src/styles/createTypography.test.js index 1d1b5526c0f826..06d100e8aac4aa 100644 --- a/packages/material-ui/src/styles/createTypography.test.js +++ b/packages/material-ui/src/styles/createTypography.test.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import createPalette from './createPalette'; import createTypography from './createTypography'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; describe('createTypography', () => { let palette; @@ -76,30 +75,16 @@ describe('createTypography', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('logs an error if `fontSize` is not of type number', () => { - createTypography({}, { fontSize: '1' }); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.match( - /Material-UI: `fontSize` is required to be a number./, - ); + expect(() => { + createTypography({}, { fontSize: '1' }); + }).toErrorDev('Material-UI: `fontSize` is required to be a number.'); }); it('logs an error if `htmlFontSize` is not of type number', () => { - createTypography({}, { htmlFontSize: '1' }); - - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.match( - /Material-UI: `htmlFontSize` is required to be a number./, - ); + expect(() => { + createTypography({}, { htmlFontSize: '1' }); + }).toErrorDev('Material-UI: `htmlFontSize` is required to be a number.'); }); }); }); diff --git a/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js b/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js index ba9977d1a25fd8..e5733f8264abd5 100644 --- a/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js +++ b/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js @@ -1,7 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { ThemeProvider } from '@material-ui/styles'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { act, createClientRender } from 'test/utils/createClientRender'; import createServerRender from 'test/utils/createServerRender'; import mediaQuery from 'css-mediaquery'; @@ -269,26 +268,19 @@ describe('useMediaQuery', () => { }); describe('warnings', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('warns on invalid `query` argument', () => { function MyComponent() { useMediaQuery(() => '(min-width:2000px)'); return null; } - render(); - // logs warning twice in StrictMode - expect(consoleErrorMock.callCount()).to.equal(2); // strict mode renders twice - expect(consoleErrorMock.messages()[0]).to.include( + expect(() => { + render(); + }).toErrorDev([ + 'Material-UI: The `query` argument provided is invalid', + // logs warning twice in StrictMode 'Material-UI: The `query` argument provided is invalid', - ); + ]); }); }); }); diff --git a/packages/material-ui/src/utils/deprecatedPropType.test.js b/packages/material-ui/src/utils/deprecatedPropType.test.js index 15e223b43083a6..0cd06c82730251 100644 --- a/packages/material-ui/src/utils/deprecatedPropType.test.js +++ b/packages/material-ui/src/utils/deprecatedPropType.test.js @@ -1,6 +1,5 @@ import { expect } from 'chai'; import PropTypes from 'prop-types'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import deprecatedPropType from './deprecatedPropType'; describe('deprecatedPropType', () => { @@ -8,14 +7,9 @@ describe('deprecatedPropType', () => { const location = 'prop'; beforeEach(() => { - consoleErrorMock.spy(); PropTypes.resetWarningCache(); }); - afterEach(() => { - consoleErrorMock.reset(); - }); - it('should not warn', () => { const propName = `children${new Date()}`; const props = {}; @@ -27,7 +21,7 @@ describe('deprecatedPropType', () => { location, componentName, ); - expect(consoleErrorMock.callCount()).to.equal(0); + expect(() => {}).not.toErrorDev(); }); it('should warn once', () => { @@ -35,24 +29,27 @@ describe('deprecatedPropType', () => { const props = { [propName]: 'yolo', }; - PropTypes.checkPropTypes( - { - [propName]: deprecatedPropType(PropTypes.string, 'give me a reason'), - }, - props, - location, - componentName, - ); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.match(/give me a reason/); - PropTypes.checkPropTypes( - { - [propName]: deprecatedPropType(PropTypes.string, 'give me a reason'), - }, - props, - location, - componentName, - ); - expect(consoleErrorMock.callCount()).to.equal(1); + + expect(() => { + PropTypes.checkPropTypes( + { + [propName]: deprecatedPropType(PropTypes.string, 'give me a reason'), + }, + props, + location, + componentName, + ); + }).toErrorDev('give me a reason'); + + expect(() => { + PropTypes.checkPropTypes( + { + [propName]: deprecatedPropType(PropTypes.string, 'give me a reason'), + }, + props, + location, + componentName, + ); + }).not.toErrorDev(); }); }); diff --git a/packages/material-ui/src/utils/index.test.js b/packages/material-ui/src/utils/index.test.js index 116da0ef0d7756..3f3ece8688aa4d 100644 --- a/packages/material-ui/src/utils/index.test.js +++ b/packages/material-ui/src/utils/index.test.js @@ -2,7 +2,6 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import PropTypes from 'prop-types'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import { mount } from 'enzyme'; import { isMuiElement, setRef, useForkRef } from '.'; import { Input, ListItemSecondaryAction, SvgIcon } from '..'; @@ -64,14 +63,6 @@ describe('utils/index.js', () => { }); describe('useForkRef', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('returns a single ref-setter function that forks the ref to its inputs', () => { function Component(props) { const { innerRef } = props; @@ -89,10 +80,11 @@ describe('utils/index.js', () => { }; const outerRef = React.createRef(); - mount(); + expect(() => { + mount(); + }).not.toErrorDev(); expect(outerRef.current.textContent).to.equal('has a ref'); - expect(consoleErrorMock.callCount()).to.equal(0); }); it('forks if only one of the branches requires a ref', () => { @@ -104,10 +96,11 @@ describe('utils/index.js', () => { return {String(hasRef)}; }); - const wrapper = mount(); - + let wrapper; + expect(() => { + wrapper = mount(); + }).not.toErrorDev(); expect(wrapper.containsMatchingElement(true)).to.equal(true); - expect(consoleErrorMock.callCount()).to.equal(0); }); it('does nothing if none of the forked branches requires a ref', () => { @@ -124,12 +117,13 @@ describe('utils/index.js', () => { return ; } - mount( - - - , - ); - expect(consoleErrorMock.callCount()).to.equal(0); + expect(() => { + mount( + + + , + ); + }).not.toErrorDev(); }); describe('changing refs', () => { @@ -148,25 +142,28 @@ describe('utils/index.js', () => { }; it('handles changing from no ref to some ref', () => { - const wrapper = mount(); + let wrapper; - expect(consoleErrorMock.callCount()).to.equal(0); + expect(() => { + wrapper = mount(); + }).not.toErrorDev(); const ref = React.createRef(); - wrapper.setProps({ leftRef: ref }); - + expect(() => { + wrapper.setProps({ leftRef: ref }); + }).not.toErrorDev(); expect(ref.current.id).to.equal('test'); - expect(consoleErrorMock.callCount()).to.equal(0); }); it('cleans up detached refs', () => { const firstLeftRef = React.createRef(); const firstRightRef = React.createRef(); const secondRightRef = React.createRef(); + let wrapper; - const wrapper = mount(); - - expect(consoleErrorMock.callCount()).to.equal(0); + expect(() => { + wrapper = mount(); + }).not.toErrorDev(); expect(firstLeftRef.current.id).to.equal('test'); expect(firstRightRef.current.id).to.equal('test'); expect(secondRightRef.current).to.equal(null); diff --git a/packages/material-ui/src/utils/useControlled.test.js b/packages/material-ui/src/utils/useControlled.test.js index 31155debec145c..bcc585f9a742b4 100644 --- a/packages/material-ui/src/utils/useControlled.test.js +++ b/packages/material-ui/src/utils/useControlled.test.js @@ -1,7 +1,6 @@ import * as React from 'react'; import { expect } from 'chai'; import { createClientRender } from 'test/utils/createClientRender'; -import consoleErrorMock from 'test/utils/consoleErrorMock'; import useControlled from './useControlled'; const TestComponent = ({ value: valueProp, defaultValue, children }) => { @@ -16,14 +15,6 @@ const TestComponent = ({ value: valueProp, defaultValue, children }) => { describe('useControlled', () => { const render = createClientRender(); - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - it('works correctly when is not controlled', () => { let valueState; let setValueState; @@ -55,43 +46,59 @@ describe('useControlled', () => { }); it('warns when switching from uncontrolled to controlled', () => { - const { setProps } = render({() => null}); - expect(consoleErrorMock.callCount()).to.equal(0); - setProps({ value: 'foobar' }); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + let setProps; + expect(() => { + ({ setProps } = render({() => null})); + }).not.toErrorDev(); + + expect(() => { + setProps({ value: 'foobar' }); + }).toErrorDev( 'Material-UI: A component is changing the uncontrolled value state of TestComponent to be controlled.', ); }); it('warns when switching from controlled to uncontrolled', () => { - const { setProps } = render({() => null}); - expect(consoleErrorMock.callCount()).to.equal(0); - setProps({ value: undefined }); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + let setProps; + + expect(() => { + ({ setProps } = render({() => null})); + }).not.toErrorDev(); + + expect(() => { + setProps({ value: undefined }); + }).toErrorDev( 'Material-UI: A component is changing the controlled value state of TestComponent to be uncontrolled.', ); }); it('warns when changing the defaultValue prop after initial rendering', () => { - const { setProps } = render({() => null}); - expect(consoleErrorMock.callCount()).to.equal(0); - setProps({ defaultValue: 1 }); - expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( + let setProps; + + expect(() => { + ({ setProps } = render({() => null})); + }).not.toErrorDev(); + + expect(() => { + setProps({ defaultValue: 1 }); + }).toErrorDev( 'Material-UI: A component is changing the default value state of an uncontrolled TestComponent after being initialized.', ); }); it('should not raise a warning if changing the defaultValue when controlled', () => { - const { setProps } = render( - - {() => null} - , - ); - expect(consoleErrorMock.callCount()).to.equal(0); - setProps({ defaultValue: 1 }); - expect(consoleErrorMock.callCount()).to.equal(0); + let setProps; + + expect(() => { + ({ setProps } = render( + + {() => null} + , + )); + }).not.toErrorDev(); + + expect(() => { + setProps({ defaultValue: 1 }); + }).not.toErrorDev(); }); }); diff --git a/scripts/react-next.diff b/scripts/react-next.diff index afe398e0683a75..6626e07cf0fc94 100644 --- a/scripts/react-next.diff +++ b/scripts/react-next.diff @@ -1,114 +1,197 @@ diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js -index 2f3ea31dc..4ad337e85 100644 +index 1fd4bad8b..20d22063b 100644 --- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js +++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js -@@ -1018,7 +1018,7 @@ describe('', () => { - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.equal('a'); -- expect(consoleErrorMock.callCount()).to.equal(4); // strict mode renders twice -+ expect(consoleErrorMock.callCount()).to.equal(3); - expect(consoleErrorMock.messages()[0]).to.include( +@@ -1038,8 +1038,6 @@ describe('', () => { + fireEvent.change(textbox, { target: { value: 'a' } }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + }).toErrorDev([ +- 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', +- // strict mode renders twice 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', - ); -@@ -1070,7 +1070,7 @@ describe('', () => { - />, - ); - -- expect(consoleWarnMock.callCount()).to.equal(4); // strict mode renders twice -+ expect(consoleWarnMock.callCount()).to.equal(2); - expect(consoleWarnMock.messages()[0]).to.include( + 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', + 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', +@@ -1094,9 +1092,6 @@ describe('', () => { + />, + ); + }).toWarnDev([ +- 'None of the options match with `"not a good value"`', +- // strict mode renders twice +- 'None of the options match with `"not a good value"`', 'None of the options match with `"not a good value"`', - ); -@@ -1099,7 +1099,7 @@ describe('', () => { - const options = getAllByRole('option').map((el) => el.textContent); + 'None of the options match with `"not a good value"`', + ]); +@@ -1122,11 +1117,7 @@ describe('', () => { + groupBy={(option) => option.group} + />, + ); +- }).toWarnDev([ +- // strict mode renders twice +- 'returns duplicated headers', +- 'returns duplicated headers', +- ]); ++ }).toWarnDev(['returns duplicated headers']); + const options = screen.getAllByRole('option').map((el) => el.textContent); expect(options).to.have.length(7); expect(options).to.deep.equal(['A', 'D', 'E', 'B', 'G', 'F', 'C']); -- expect(consoleWarnMock.callCount()).to.equal(2); // strict mode renders twice -+ expect(consoleWarnMock.callCount()).to.equal(1); - expect(consoleWarnMock.messages()[0]).to.include('returns duplicated headers'); - }); - }); +diff --git a/packages/material-ui-lab/src/TreeView/TreeView.test.js b/packages/material-ui-lab/src/TreeView/TreeView.test.js +index 056a9fcbc..21eeee2cf 100644 +--- a/packages/material-ui-lab/src/TreeView/TreeView.test.js ++++ b/packages/material-ui-lab/src/TreeView/TreeView.test.js +@@ -8,6 +8,7 @@ import { getClasses } from '@material-ui/core/test-utils'; + import createMount from 'test/utils/createMount'; + import TreeView from './TreeView'; + import TreeItem from '../TreeItem'; ++import { act } from 'react-test-renderer'; + + describe('', () => { + let classes; +@@ -58,7 +59,8 @@ describe('', () => { + + // should not throw eventually or with a better error message + // FIXME: https://github.com/mui-org/material-ui/issues/20832 +- it('crashes when unmounting with duplicate ids', () => { ++ // FIXME: unclear what is happening in react@next ++ it.skip('crashes when unmounting with duplicate ids', () => { + const CustomTreeItem = () => { + return ; + }; diff --git a/packages/material-ui-styles/src/ThemeProvider/ThemeProvider.test.js b/packages/material-ui-styles/src/ThemeProvider/ThemeProvider.test.js -index 9013d9095..7f0862466 100644 +index 46238825d..bcf275893 100644 --- a/packages/material-ui-styles/src/ThemeProvider/ThemeProvider.test.js +++ b/packages/material-ui-styles/src/ThemeProvider/ThemeProvider.test.js -@@ -135,7 +135,7 @@ describe('ThemeProvider', () => { - - , - ); -- expect(consoleErrorMock.callCount()).to.equal(2); // strict mode renders twice -+ expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include('However, no outer theme is present.'); +@@ -127,11 +127,7 @@ describe('ThemeProvider', () => { + + , + ); +- }).toErrorDev([ +- 'However, no outer theme is present.', +- // strict mode renders twice +- 'However, no outer theme is present.', +- ]); ++ }).toErrorDev(['However, no outer theme is present.']); }); -@@ -148,7 +148,7 @@ describe('ThemeProvider', () => { - , - , - ); -- expect(consoleErrorMock.callCount()).to.equal(2); // strict mode renders twice -+ expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: You should return an object from your theme function', - ); + it('should warn about wrong theme function', () => { +@@ -144,11 +140,7 @@ describe('ThemeProvider', () => { + , + , + ); +- }).toErrorDev([ +- 'Material-UI: You should return an object from your theme function', +- // strict mode renders twice +- 'Material-UI: You should return an object from your theme function', +- ]); ++ }).toErrorDev(['Material-UI: You should return an object from your theme function']); + }); + }); + }); +diff --git a/packages/material-ui-styles/src/makeStyles/makeStyles.test.js b/packages/material-ui-styles/src/makeStyles/makeStyles.test.js +index cce608f6b..19cab2d18 100644 +--- a/packages/material-ui-styles/src/makeStyles/makeStyles.test.js ++++ b/packages/material-ui-styles/src/makeStyles/makeStyles.test.js +@@ -213,7 +213,7 @@ describe('makeStyles', () => { + expect(sheetsRegistry.registry[0].classes).to.deep.equal({ root: 'makeStyles-root-2' }); + + wrapper.unmount(); +- expect(sheetsRegistry.registry.length).to.equal(0); ++ expect(sheetsRegistry.registry.length).to.equal(1); + }); + + it('should work when depending on a theme', () => { +diff --git a/packages/material-ui-styles/src/withStyles/withStyles.test.js b/packages/material-ui-styles/src/withStyles/withStyles.test.js +index d2a231d03..f247918fa 100644 +--- a/packages/material-ui-styles/src/withStyles/withStyles.test.js ++++ b/packages/material-ui-styles/src/withStyles/withStyles.test.js +@@ -110,7 +110,7 @@ describe('withStyles', () => { + expect(sheetsRegistry.registry[0].classes).to.deep.equal({ root: 'Empty-root-2' }); + + wrapper.unmount(); +- expect(sheetsRegistry.registry.length).to.equal(0); ++ expect(sheetsRegistry.registry.length).to.equal(1); + }); + + it('should supply correct props to jss callbacks', () => { diff --git a/packages/material-ui/src/Breadcrumbs/Breadcrumbs.test.js b/packages/material-ui/src/Breadcrumbs/Breadcrumbs.test.js -index ed0e37f21..49d8ea9b0 100644 +index f5c603b45..2f18cca5c 100644 --- a/packages/material-ui/src/Breadcrumbs/Breadcrumbs.test.js +++ b/packages/material-ui/src/Breadcrumbs/Breadcrumbs.test.js -@@ -102,7 +102,7 @@ describe('', () => { - ); - expect(getAllByRole('listitem', { hidden: false })).to.have.length(4); - expect(getByRole('list')).to.have.text('first/second/third/fourth'); -- expect(consoleErrorMock.callCount()).to.equal(2); // strict mode renders twice -+ expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: You have provided an invalid combination of props to the Breadcrumbs.\nitemsAfterCollapse={2} + itemsBeforeCollapse={2} >= maxItems={3}', +@@ -93,7 +93,6 @@ describe('', () => { ); + }).toErrorDev([ + 'Material-UI: You have provided an invalid combination of props to the Breadcrumbs.\nitemsAfterCollapse={2} + itemsBeforeCollapse={2} >= maxItems={3}', +- 'Material-UI: You have provided an invalid combination of props to the Breadcrumbs.\nitemsAfterCollapse={2} + itemsBeforeCollapse={2} >= maxItems={3}', + ]); + expect(screen.getAllByRole('listitem', { hidden: false })).to.have.length(4); + expect(screen.getByRole('list')).to.have.text('first/second/third/fourth'); +diff --git a/packages/material-ui/src/Tabs/Tabs.test.js b/packages/material-ui/src/Tabs/Tabs.test.js +index d5cd9ab15..d9affd078 100644 +--- a/packages/material-ui/src/Tabs/Tabs.test.js ++++ b/packages/material-ui/src/Tabs/Tabs.test.js +@@ -85,9 +85,6 @@ describe('', () => { + expect(() => { + render(); + }).toErrorDev([ +- 'Material-UI: You can not use the `centered={true}` and `variant="scrollable"`', +- // StrictMode renders twice +- 'Material-UI: You can not use the `centered={true}` and `variant="scrollable"`', + 'Material-UI: You can not use the `centered={true}` and `variant="scrollable"`', + 'Material-UI: You can not use the `centered={true}` and `variant="scrollable"`', + ]); diff --git a/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js b/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js -index 09daadd96..1eaf80628 100644 +index ef6533f4b..bd5633dc4 100644 --- a/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js +++ b/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js -@@ -261,7 +261,7 @@ describe('', () => { - }); - forceUpdate(); +@@ -253,12 +253,7 @@ describe('', () => { -- expect(consoleErrorMock.callCount()).to.equal(3); // strict mode renders twice -+ expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include('Material-UI: Too many re-renders.'); + expect(() => { + forceUpdate(); +- }).toErrorDev([ +- 'Material-UI: Too many re-renders.', +- // strict mode renders twice +- 'Material-UI: Too many re-renders.', +- 'Material-UI: Too many re-renders.', +- ]); ++ }).toErrorDev(['Material-UI: Too many re-renders.']); }); }); + }); diff --git a/packages/material-ui/src/internal/SwitchBase.test.js b/packages/material-ui/src/internal/SwitchBase.test.js -index 41a38bc07..c9397fd13 100644 +index 27b143abf..7c321b14a 100644 --- a/packages/material-ui/src/internal/SwitchBase.test.js +++ b/packages/material-ui/src/internal/SwitchBase.test.js -@@ -373,7 +373,7 @@ describe('', () => { - wrapper.setProps({ checked: true }); - expect(consoleErrorMock.callCount()).to.equal(2); - expect(consoleErrorMock.messages()[0]).to.include( -- 'Warning: A component is changing an uncontrolled input of type checkbox to be controlled.', -+ 'Warning: A component is changing an uncontrolled input to be controlled.', - ); - expect(consoleErrorMock.messages()[1]).to.include( - 'Material-UI: A component is changing the uncontrolled checked state of SwitchBase to be controlled.', -@@ -392,7 +392,7 @@ describe('', () => { +@@ -360,7 +360,7 @@ describe('', () => { + expect(() => { + setProps({ checked: true }); + }).toErrorDev([ +- 'Warning: A component is changing an uncontrolled input of type checkbox to be controlled.', ++ 'Warning: A component is changing an uncontrolled input to be controlled.', + 'Material-UI: A component is changing the uncontrolled checked state of SwitchBase to be controlled.', + ]); + }); +@@ -380,7 +380,7 @@ describe('', () => { + expect(() => { setProps({ checked: undefined }); - expect(consoleErrorMock.callCount()).to.equal(2); - expect(consoleErrorMock.messages()[0]).to.include( -- 'Warning: A component is changing a controlled input of type checkbox to be uncontrolled.', -+ 'Warning: A component is changing a controlled input to be uncontrolled.', - ); - expect(consoleErrorMock.messages()[1]).to.include( - 'Material-UI: A component is changing the controlled checked state of SwitchBase to be uncontrolled.', + }).toErrorDev([ +- 'Warning: A component is changing a controlled input of type checkbox to be uncontrolled.', ++ 'Warning: A component is changing a controlled input to be uncontrolled', + 'Material-UI: A component is changing the controlled checked state of SwitchBase to be uncontrolled.', + ]); + }); diff --git a/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js b/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js -index ba9977d1a..b5ca0ca4b 100644 +index e5733f826..61333a172 100644 --- a/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js +++ b/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js -@@ -285,7 +285,7 @@ describe('useMediaQuery', () => { +@@ -276,11 +276,7 @@ describe('useMediaQuery', () => { - render(); - // logs warning twice in StrictMode -- expect(consoleErrorMock.callCount()).to.equal(2); // strict mode renders twice -+ expect(consoleErrorMock.callCount()).to.equal(1); - expect(consoleErrorMock.messages()[0]).to.include( - 'Material-UI: The `query` argument provided is invalid', - ); + expect(() => { + render(); +- }).toErrorDev([ +- 'Material-UI: The `query` argument provided is invalid', +- // logs warning twice in StrictMode +- 'Material-UI: The `query` argument provided is invalid', +- ]); ++ }).toErrorDev(['Material-UI: The `query` argument provided is invalid']); + }); + }); + }); diff --git a/test/README.md b/test/README.md index 0b79bf8bbff9d7..ff520230a1b9a9 100644 --- a/test/README.md +++ b/test/README.md @@ -40,6 +40,65 @@ Deciding where to put a test is (like naming things) a hard problem: a lot of styles consider adding a component (that doesn't require any interaction) to `test/regressions/tests/` e.g. `test/regressions/tests/List/ListWithSomeStyleProp` +### Unexpected calls to `console.error` or `console.war` + +By default our test suite fails if any test recorded `console.error` or `console.warn` calls: +![unexpected console.error call](./unexpected-console-error-call.png) + +The failure message includes the name of the test. +The logged error is prefixed with the test file so that you can find the failing test faster (test file + test name). +The error includes the logged message as well as the stacktrace of that message. +Unfortunately the stacktrace is currently duplicated due to `chai`. + +However, in watchmode (`yarn test:unit --watch`) unexpected calls are ignored (see https://github.com/mochajs/mocha/issues/4347). +You can explicitly [expect no console calls](#writing-a-test-for-consoleerror-or-consolewarn) for when you're adding a regression test. +This makes the test more readable and properly fails the test in watchmode if the test had unexpected `console` calls. + +### Writing a test for `console.error` or `console.warn` + +If you add a new warning via `console.error` or `console.warn` you should add tests that expect this message. +For tests that expect a call you can use our custom `toWarnDev` or `toErrorDev` matchers. +The expected messages must be a subset of the actual messages and match the casing. +The order of these message must match as well. + +Example: + +```jsx +function SomeComponent({ variant }) { + if (process.env.NODE_ENV !== 'production') { + if (variant === 'unexpected') { + console.error("That variant doesn't make sense."); + } + if (variant !== undefined) { + console.error('`variant` is deprecated.'); + } + } + + return ; +} +expect(() => { + render(); +}).toErrorDev(["That variant doesn't make sense.", '`variant` is deprecated.']); +``` + +```js +function SomeComponent({ variant }) { + if (process.env.NODE_ENV !== 'production') { + if (variant === 'unexpected') { + console.error("That variant doesn't make sense."); + } + if (variant !== undefined) { + console.error('`variant` is deprecated.'); + } + } + + return ; +} +expect(() => { + render(); +}).not.toErrorDev(); +``` + #### Visual regression tests We try to use as many demos from the documentation as possible; diff --git a/test/unexpected-console-error-call.png b/test/unexpected-console-error-call.png new file mode 100644 index 00000000000000..dcb7c303bb0942 Binary files /dev/null and b/test/unexpected-console-error-call.png differ diff --git a/test/utils/components.js b/test/utils/components.js new file mode 100644 index 00000000000000..c4ac86bb5590cb --- /dev/null +++ b/test/utils/components.js @@ -0,0 +1,37 @@ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; + +/** + * A basic error boundary that can be used to assert thrown errors in render. + * @example ; + * expect(errorRef.current.errors).to.have.lenght(0); + */ +// enforce a single file for test related components +// eslint-disable-next-line import/prefer-default-export +export class ErrorBoundary extends React.Component { + static propTypes = { + children: PropTypes.node.isRequired, + }; + + state = { error: null }; + + /** + * @public + */ + errors = []; + + static getDerivedStateFromError(error) { + return { error }; + } + + componentDidCatch(error) { + this.errors.push(error); + } + + render() { + if (this.state.error) { + return null; + } + return this.props.children; + } +} diff --git a/test/utils/consoleError.js b/test/utils/consoleError.js deleted file mode 100644 index 1b5aa26ebf7622..00000000000000 --- a/test/utils/consoleError.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable no-console */ -// Makes sure the tests fails when a PropType validation fails. -function consoleError() { - console.error = (...args) => { - // Can't use log as karma is not displaying them. - console.info(...args); - throw new Error(...args); - }; - - console.warn = (...args) => { - // Can't use log as karma is not displaying them. - console.info(...args); - throw new Error(...args); - }; -} - -module.exports = consoleError; diff --git a/test/utils/consoleErrorMock.js b/test/utils/consoleErrorMock.js deleted file mode 100644 index fb3e92f287c056..00000000000000 --- a/test/utils/consoleErrorMock.js +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-disable no-console */ -import utilFormat from 'format-util'; -import { spy } from 'sinon'; - -/** - * One alternative to this module is to use something like: - * - * let warning; - * - * beforeEach(() => { - * warning = mock(console).expects('error'); - * }); - * - * afterEach(() => { - * warning.restore(); - * }); - */ -export class ConsoleMock { - consoleErrorContainer; - - constructor(methodName) { - this.methodName = methodName; - } - - spy = () => { - this.consoleErrorContainer = console[this.methodName]; - console[this.methodName] = spy(); - }; - - reset = () => { - console[this.methodName] = this.consoleErrorContainer; - delete this.consoleErrorContainer; - }; - - callCount = () => { - if (this.consoleErrorContainer) { - return console[this.methodName].callCount; - } - - throw new Error('Requested call count before spy() was called'); - }; - - args = () => { - throw new TypeError( - 'args() was removed in favor of messages(). ' + - 'Use messages() to match against the actual error message that will be displayed in the console.', - ); - }; - - /** - * returns the formatted message for each call - * - * you could call console[this.methodName]("type %s", "foo") which would log - * "type foo". If you want to assert on the actual message use messages() instead - */ - messages = () => { - if (!this.consoleErrorContainer) { - throw new Error('Requested call count before spy() was called'); - } - - /** - * @type {import('sinon').SinonSpy} - */ - const consoleSpy = console[this.methodName]; - return consoleSpy.args.map((loggerArgs) => { - return utilFormat(...loggerArgs); - }); - }; -} - -export const consoleWarnMock = new ConsoleMock('warn'); - -export default new ConsoleMock('error'); diff --git a/test/utils/consoleErrorMock.test.js b/test/utils/consoleErrorMock.test.js deleted file mode 100644 index 14de8e18f343ef..00000000000000 --- a/test/utils/consoleErrorMock.test.js +++ /dev/null @@ -1,62 +0,0 @@ -import { expect } from 'chai'; -import consoleErrorMock from './consoleErrorMock'; - -describe('consoleErrorMock()', () => { - describe('callCount()', () => { - it('should throw error when calling callCount() before spy()', () => { - expect(() => { - consoleErrorMock.callCount(); - }).to.throw('Requested call count before spy() was called'); - }); - }); - - describe('args', () => { - it('was removed but throws a descriptive error', () => { - expect(() => consoleErrorMock.args()).to.throw( - 'args() was removed in favor of messages(). Use messages() to match against the actual error message that will be displayed in the console.', - ); - }); - }); - - describe('messages()', () => { - describe('when not spying', () => { - it('should throw error', () => { - expect(() => { - consoleErrorMock.messages(); - }).to.throw('Requested call count before spy() was called'); - }); - }); - - describe('when spying', () => { - beforeEach(() => { - consoleErrorMock.spy(); - }); - - afterEach(() => { - consoleErrorMock.reset(); - }); - - it('returns the formatted output', () => { - console.error('expected %s but got %s', '1', 2); - - expect(consoleErrorMock.messages()[0]).to.equal('expected 1 but got 2'); - }); - }); - }); - - describe('spy()', () => { - it('should place a spy in console.error', () => { - consoleErrorMock.spy(); - expect(console.error.hasOwnProperty('isSinonProxy')).to.equal(true); - consoleErrorMock.reset(); - expect(console.error.hasOwnProperty('isSinonProxy')).to.equal(false); - }); - - it('should keep the call count', () => { - consoleErrorMock.spy(); - console.error(); - expect(consoleErrorMock.callCount()).to.equal(1); - consoleErrorMock.reset(); - }); - }); -}); diff --git a/test/utils/init.d.ts b/test/utils/init.d.ts index e9f646ef3f3f82..4246916c818e31 100644 --- a/test/utils/init.d.ts +++ b/test/utils/init.d.ts @@ -27,5 +27,21 @@ declare namespace Chai { * checks if the element is focused */ toHaveFocus(): void; + /** + * Matches calls to `console.warn` in the asserted callback. + * + * @example expect(() => render()).not.toWarnDev() + * @example expect(() => render()).toWarnDev('single message') + * @example expect(() => render()).toWarnDev(['first warning', 'then the second']) + */ + toWarnDev(messages?: string | string[]): void; + /** + * Matches calls to `console.error` in the asserted callback. + * + * @example expect(() => render()).not.toErrorDev() + * @example expect(() => render()).toErrorDev('single message') + * @example expect(() => render()).toErrorDev(['first warning', 'then the second']) + */ + toErrorDev(messages?: string | string[]): void; } } diff --git a/test/utils/init.js b/test/utils/init.js index e2925cb103309e..fee2ab944c79b2 100644 --- a/test/utils/init.js +++ b/test/utils/init.js @@ -1,7 +1,6 @@ import enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import * as testingLibrary from '@testing-library/react/pure'; -import consoleError from './consoleError'; import './initMatchers'; enzyme.configure({ adapter: new Adapter() }); @@ -14,5 +13,3 @@ const defaultHidden = !process.env.CI; // adds verbosity for something that might be confusing console.warn(`${defaultHidden ? 'including' : 'excluding'} inaccessible elements by default`); testingLibrary.configure({ defaultHidden }); - -consoleError(); diff --git a/test/utils/initMatchers.js b/test/utils/initMatchers.js index 60c00e9082f1e1..f7e1e848ab9115 100644 --- a/test/utils/initMatchers.js +++ b/test/utils/initMatchers.js @@ -3,6 +3,7 @@ import chaiDom from 'chai-dom'; import { isInaccessible } from '@testing-library/dom'; import { prettyDOM } from '@testing-library/react/pure'; import { computeAccessibleName } from 'dom-accessibility-api'; +import formatUtil from 'format-util'; // chai#utils.elToString that looks like stringified elements in testing-library function elementToString(element) { @@ -151,3 +152,137 @@ chai.use((chaiAPI, utils) => { new chai.Assertion(this._obj).to.be.visible; }); }); + +chai.use((chaiAPI, utils) => { + function addConsoleMatcher(matcherName, methodName) { + /** + * @param {string[]} expectedMessages + */ + function matcher(expectedMessages = []) { + // documented pattern to get the actual value of the assertion + // eslint-disable-next-line no-underscore-dangle + const callback = this._obj; + + if (process.env.NODE_ENV !== 'production') { + const remainingMessages = + typeof expectedMessages === 'string' ? [expectedMessages] : expectedMessages.slice(); + const unexpectedMessages = []; + let caughtError = null; + + this.assert( + remainingMessages.length > 0, + `Expected to call console.${methodName} but didn't provide messages. ` + + `If you don't expect any messages prefer \`expect().not.${matcherName}();\`.`, + `Expected no call to console.${methodName} while also expecting messages.` + + 'Expected no call to console.error but provided messages. ' + + "If you want to make sure a certain message isn't logged prefer the positive. " + + 'By expecting certain messages you automatically expect that no other messages are logged', + ); + + // eslint-disable-next-line no-console + const originalMethod = console[methodName]; + + const consoleMatcher = (format, ...args) => { + const actualMessage = formatUtil(format, ...args); + const expectedMessage = remainingMessages.shift(); + + let message = null; + if (expectedMessage === undefined) { + message = `Expected no more error messages but got:\n"${actualMessage}"`; + } else if (!actualMessage.includes(expectedMessage)) { + message = `Expected "${actualMessage}"\nto include\n"${expectedMessage}"`; + } + + if (message !== null) { + const error = new Error(message); + + const { stack: fullStack } = error; + const fullStacktrace = fullStack.replace(`Error: ${message}\n`, '').split('\n'); + + const usefulStacktrace = fullStacktrace + // + // first line points to this frame which is irrelevant for the tester + .slice(1); + const usefulStack = `${message}\n${usefulStacktrace.join('\n')}`; + + error.stack = usefulStack; + unexpectedMessages.push(error); + } + }; + // eslint-disable-next-line no-console + console[methodName] = consoleMatcher; + + try { + callback(); + } catch (error) { + caughtError = error; + } finally { + // eslint-disable-next-line no-console + console[methodName] = originalMethod; + + // unexpected thrown error takes precedence over unexpected console call + if (caughtError !== null) { + // not the same pattern as described in the block because we don't rethrow in the catch + // eslint-disable-next-line no-unsafe-finally + throw caughtError; + } + + const formatMessages = (messages) => { + const formattedMessages = messages.map((message) => { + if (typeof message === 'string') { + return `"${message}"`; + } + // full Error + return `${message.stack}`; + }); + return `\n\n - ${formattedMessages.join('\n\n- ')}`; + }; + + const shouldHaveWarned = utils.flag(this, 'negate') !== true; + + // unreachable from expect().not.toWarnDev(messages) + if (unexpectedMessages.length > 0) { + const unexpectedMessageRecordedMessage = `Recorded unexpected console.${methodName} calls: ${formatMessages( + unexpectedMessages, + )}`; + // chai will duplicate the stack frames from the unexpected calls in their assertion error + // it's not ideal but the test failure is located the second to last stack frame + // and the origin of the call is the second stackframe in the stack + this.assert( + // force chai to always trigger an assertion error + !shouldHaveWarned, + unexpectedMessageRecordedMessage, + unexpectedMessageRecordedMessage, + ); + } + + if (shouldHaveWarned) { + this.assert( + remainingMessages.length === 0, + `Could not match the following console.${methodName} calls. ` + + `Make sure previous actions didn't call console.${methodName} by wrapping them in expect(() => {}).not.${matcherName}(): ${formatMessages( + remainingMessages, + )}`, + `Impossible state reached in \`expect().${matcherName}()\`. ` + + `This is a bug in the matcher.`, + ); + } + } + } else { + // nothing to do in prod + // If there are still console calls than our test setup throws. + callback(); + } + } + + chai.Assertion.addMethod(matcherName, matcher); + /* eslint-enable no-console */ + } + + /** + * @example expect(() => render()).toWarnDev('single message') + * @example expect(() => render()).toWarnDev(['first warning', 'then the second']) + */ + addConsoleMatcher('toWarnDev', 'warn'); + addConsoleMatcher('toErrorDev', 'error'); +}); diff --git a/test/utils/initMatchers.test.js b/test/utils/initMatchers.test.js new file mode 100644 index 00000000000000..74cf9fd398674d --- /dev/null +++ b/test/utils/initMatchers.test.js @@ -0,0 +1,120 @@ +import { expect } from 'chai'; +import { createSandbox } from 'sinon'; + +describe('custom matchers', () => { + const consoleSandbox = createSandbox(); + + beforeEach(() => { + // otherwise our global setup throws on unexpected calls in afterEach + consoleSandbox.stub(console, 'warn'); + consoleSandbox.stub(console, 'error'); + }); + + afterEach(() => { + consoleSandbox.restore(); + }); + + describe('toErrorDev()', () => { + it('passes if the message is exactly the same', () => { + expect(() => console.error('expected message')).toErrorDev('expected message'); + }); + + it('passes if the message is a subset', () => { + expect(() => console.error('expected message')).toErrorDev('pected messa'); + }); + + it('passes if multiple messages are expected', () => { + expect(() => { + console.error('expected message'); + console.error('another message'); + }).toErrorDev(['expected message', 'another message']); + }); + + it('fails if an expected console.error call wasnt recorded with a useful stacktrace', () => { + let caughtError; + try { + console.error('expected message'); + expect(() => {}).toErrorDev('expected message'); + } catch (error) { + caughtError = error; + } + + expect(caughtError).to.have.property('stack'); + expect(caughtError.stack).to.include( + 'Could not match the following console.error calls. ' + + "Make sure previous actions didn't call console.error by wrapping them in expect(() => {}).not.toErrorDev(): \n\n" + + ' - "expected message"\n' + + ' at Context.', + ); + // check that the top stackframe points to this test + // if this test is moved to another file the next assertion fails + expect(caughtError.stack).to.match( + /- "expected message"\s+at Context\. \(.+\/initMatchers\.test\.js:\d+:\d+\)/, + ); + }); + + it('is case sensitive', () => { + let caughtError; + try { + expect(() => console.error('expected Message')).toErrorDev('expected message'); + } catch (error) { + caughtError = error; + } + + expect(caughtError).to.have.property('stack'); + expect(caughtError.stack).to.include( + 'Recorded unexpected console.error calls: \n\n' + + ' - Expected "expected Message"\n' + + 'to include\n' + + '"expected message"\n' + + ' at callback', + ); + // check that the top stackframe points to this test + // if this test is moved to another file the next assertion fails + expect(caughtError.stack).to.match( + /"expected message"\s+at callback \(.+\/initMatchers\.test\.js:\d+:\d+\)/, + ); + }); + + it('fails if the order of calls does not match', () => { + expect(() => { + expect(() => { + console.error('another message'); + console.error('expected message'); + }).toErrorDev(['expected message', 'another message']); + }).to.throw('Recorded unexpected console.error calls'); + }); + + it('fails if there are fewer messages than expected', () => { + expect(() => { + expect(() => { + console.error('expected message'); + }).toErrorDev(['expected message', 'another message']); + }).to.throw('Could not match the following console.error calls'); + }); + + it('passes if no messages were recored if expected', () => { + expect(() => {}).not.toErrorDev(); + expect(() => {}).not.toErrorDev([]); + }); + + it('fails if no arguments are used as a way of negating', () => { + expect(() => { + expect(() => {}).toErrorDev(); + }).to.throw( + "Expected to call console.error but didn't provide messages. " + + "If you don't expect any messages prefer `expect().not.toErrorDev();", + ); + }); + + it('fails if arguments are passed when negated', () => { + expect(() => { + expect(() => {}).not.toErrorDev('not unexpected?'); + }).to.throw( + 'Expected no call to console.error but provided messages. ' + + "If you want to make sure a certain message isn't logged prefer the positive. " + + 'By expecting certain messages you automatically expect that no other messages are logged', + ); + }); + }); +}); diff --git a/test/utils/setup.js b/test/utils/setup.js index bc077adf7c3023..f44328f9f978a7 100644 --- a/test/utils/setup.js +++ b/test/utils/setup.js @@ -1,6 +1,61 @@ +const formatUtil = require('format-util'); const createDOM = require('./createDOM'); process.browser = true; createDOM(); require('./init'); + +const mochaHooks = { + beforeEach: [], + afterEach: [], +}; + +function throwOnUnexpectedConsoleMessages(methodName, expectedMatcher) { + const unexpectedCalls = []; + + function logUnexpectedConsoleCalls(format, ...args) { + const message = formatUtil(format, ...args); + // Safe stack so that test dev can track where the unexpected console message was created. + const { stack } = new Error(); + + unexpectedCalls.push([ + // first line includes the (empty) error message + // i.e. Remove the `Error:` line + // second line is this frame + stack.split('\n').slice(2).join('\n'), + message, + ]); + } + // eslint-disable-next-line no-console + console[methodName] = logUnexpectedConsoleCalls; + + mochaHooks.beforeEach.push(function resetUnexpectedCalls() { + unexpectedCalls.length = 0; + }); + + mochaHooks.afterEach.push(function flushUnexpectedCalls() { + const hadUnexpectedCalls = unexpectedCalls.length > 0; + const formattedCalls = unexpectedCalls.map(([stack, message]) => `${message}\n${stack}`); + unexpectedCalls.length = 0; + + // eslint-disable-next-line no-console + if (console[methodName] !== logUnexpectedConsoleCalls) { + throw new Error(`Did not tear down spy or stub of console.${methodName} in your test.`); + } + if (hadUnexpectedCalls) { + const location = this.currentTest.file; + const message = + `Expected test not to call console.${methodName}()\n\n` + + 'If the warning is expected, test for it explicitly by ' + + `using the ${expectedMatcher}() matcher.`; + + throw new Error(`${location}: ${message}\n\n${formattedCalls.join('\n\n')}`); + } + }); +} + +throwOnUnexpectedConsoleMessages('warn', 'toWarnDev'); +throwOnUnexpectedConsoleMessages('error', 'toErrorDev'); + +module.exports = { mochaHooks };