From 98a0fbe9ee26255ea66e00cb107b70f1c9bcc4c6 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Fri, 29 May 2026 12:12:09 +0300 Subject: [PATCH 1/5] [progress] Show runtime errors only once --- .../src/CircularProgress/CircularProgress.js | 27 ++- .../CircularProgress/CircularProgress.test.js | 63 +++---- .../src/LinearProgress/LinearProgress.js | 64 +++---- .../src/LinearProgress/LinearProgress.test.js | 175 ++++++++---------- .../mui-utils/src/errorOnce/errorOnce.test.ts | 80 ++++++++ packages/mui-utils/src/errorOnce/errorOnce.ts | 37 ++++ packages/mui-utils/src/errorOnce/index.ts | 2 + packages/mui-utils/src/index.ts | 2 + 8 files changed, 270 insertions(+), 180 deletions(-) create mode 100644 packages/mui-utils/src/errorOnce/errorOnce.test.ts create mode 100644 packages/mui-utils/src/errorOnce/errorOnce.ts create mode 100644 packages/mui-utils/src/errorOnce/index.ts diff --git a/packages/mui-material/src/CircularProgress/CircularProgress.js b/packages/mui-material/src/CircularProgress/CircularProgress.js index 8101edb0f7c2d9..da27653092d002 100644 --- a/packages/mui-material/src/CircularProgress/CircularProgress.js +++ b/packages/mui-material/src/CircularProgress/CircularProgress.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import chainPropTypes from '@mui/utils/chainPropTypes'; import composeClasses from '@mui/utils/composeClasses'; +import errorOnce from '@mui/utils/errorOnce'; import { keyframes, css, styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; @@ -197,13 +198,12 @@ const CircularProgress = React.forwardRef(function CircularProgress(inProps, ref ...other } = props; - if (process.env.NODE_ENV !== 'production') { - if (variant === 'indeterminate' && (minProp !== undefined || maxProp !== undefined)) { - console.warn( - `MUI: You have provided the \`min\` or \`max\` props with an 'indeterminate' variant. These props will have no effect.`, - ); - } - } + errorOnce( + variant === 'indeterminate' && (minProp !== undefined || maxProp !== undefined), + `MUI: You have provided the \`min\` or \`max\` props with an 'indeterminate' variant. These props will have no effect.`, + 'warn', + 'circular-progress-min-max-without-variant', + ); const min = minProp ?? 0; const max = maxProp ?? 100; @@ -228,13 +228,12 @@ const CircularProgress = React.forwardRef(function CircularProgress(inProps, ref if (variant === 'determinate') { const circumference = 2 * Math.PI * ((SIZE - thickness) / 2); - if (process.env.NODE_ENV !== 'production') { - if (value < min || value > max || min >= max) { - console.error( - `MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, - ); - } - } + errorOnce( + value < min || value > max || min >= max, + `MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, + 'error', + 'circular-progress-invalid-min-max-value', + ); const range = max - min; circleStyle.strokeDasharray = circumference.toFixed(3); diff --git a/packages/mui-material/src/CircularProgress/CircularProgress.test.js b/packages/mui-material/src/CircularProgress/CircularProgress.test.js index 094aa6a454a0b0..93e207e7cf3df3 100644 --- a/packages/mui-material/src/CircularProgress/CircularProgress.test.js +++ b/packages/mui-material/src/CircularProgress/CircularProgress.test.js @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import { reset } from '@mui/utils/errorOnce'; import { createRenderer, strictModeDoubleLoggingSuppressed, @@ -224,45 +225,31 @@ describe('', () => { errorSpy.mockRestore(); }); - it('should error if min, max, and value props are invalid', () => { - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=0, value=5.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=0, value=5.', - ]); - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=10, value=15.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=10, value=15.', - ]); - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=20, value=5.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=20, value=5.', - ]); - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=20, value=25.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=20, value=25.', - ]); - }); - - it('should warn if min and max props are provided with an indeterminate variant', () => { - expect(() => { - render(); - }).toWarnDev([ - "MUI: You have provided the `min` or `max` props with an 'indeterminate' variant. These props will have no effect.", - !strictModeDoubleLoggingSuppressed && + describe('warnings and errors', () => { + beforeEach(() => { + reset(); + }); + + it.each([ + { value: 5, min: 10, max: 0 }, + { value: 15, min: 10, max: 10 }, + { value: 5, min: 10, max: 20 }, + { value: 25, min: 10, max: 20 }, + ])('should error if min=$min, max=$max, value=$value are invalid', ({ value, min, max }) => { + expect(() => { + render(); + }).toErrorDev([ + `MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, + ]); + }); + + it('should warn if min and max props are provided with an indeterminate variant', () => { + expect(() => { + render(); + }).toWarnDev([ "MUI: You have provided the `min` or `max` props with an 'indeterminate' variant. These props will have no effect.", - ]); + ]); + }); }); }); }); diff --git a/packages/mui-material/src/LinearProgress/LinearProgress.js b/packages/mui-material/src/LinearProgress/LinearProgress.js index 666dc0599cea19..f737d1b73303c9 100644 --- a/packages/mui-material/src/LinearProgress/LinearProgress.js +++ b/packages/mui-material/src/LinearProgress/LinearProgress.js @@ -3,6 +3,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; +import errorOnce from '@mui/utils/errorOnce'; import { useRtl } from '@mui/system/RtlProvider'; import { keyframes, css, styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; @@ -370,16 +371,13 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { variant, }; - if (process.env.NODE_ENV !== 'production') { - if ( - ['indeterminate', 'query'].includes(variant) && - (minProp !== undefined || maxProp !== undefined) - ) { - console.warn( - `MUI: You have provided the \`min\` or \`max\` props with an 'indeterminate' or 'query' variant. These props will have no effect.`, - ); - } - } + errorOnce( + ['indeterminate', 'query'].includes(variant) && + (minProp !== undefined || maxProp !== undefined), + `MUI: You have provided the \`min\` or \`max\` props with an 'indeterminate' or 'query' variant. These props will have no effect.`, + 'warn', + 'linear-progress-min-max-without-variant', + ); const min = minProp ?? 0; const max = maxProp ?? 100; @@ -392,13 +390,12 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { if (variant === 'determinate' || variant === 'buffer') { if (value !== undefined) { - if (process.env.NODE_ENV !== 'production') { - if (value < min || value > max || min >= max) { - console.error( - `MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, - ); - } - } + errorOnce( + value < min || value > max || min >= max, + `MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, + 'error', + 'linear-progress-invalid-min-max-value', + ); const range = max - min; let transform = ((value - min) / range) * 100 - 100; @@ -410,22 +407,23 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { rootProps['aria-valuenow'] = value; rootProps['aria-valuemin'] = min; rootProps['aria-valuemax'] = max; - } else if (process.env.NODE_ENV !== 'production') { - console.error( - 'MUI: You need to provide a value prop ' + - 'when using the determinate or buffer variant of LinearProgress.', + } else { + errorOnce( + true, + 'MUI: You need to provide a value prop when using the determinate or buffer variant of LinearProgress.', + 'error', + 'linear-progress-value-required-for-determinate-buffer', ); } } if (variant === 'buffer') { if (valueBuffer !== undefined) { - if (process.env.NODE_ENV !== 'production') { - if (valueBuffer < min || valueBuffer > max || valueBuffer < value || min >= max) { - console.error( - `MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=${min}, max=${max}, value=${value}, valueBuffer=${valueBuffer}.`, - ); - } - } + errorOnce( + valueBuffer < min || valueBuffer > max || valueBuffer < value || min >= max, + `MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=${min}, max=${max}, value=${value}, valueBuffer=${valueBuffer}.`, + 'error', + 'linear-progress-invalid-min-max-value-buffer', + ); const range = max - min; let transform = ((valueBuffer - min) / range) * 100 - 100; @@ -433,10 +431,12 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { transform = -transform; } inlineStyles.bar2.transform = range > 0 ? `translateX(${transform}%)` : 'translateX(-100%)'; // empty-state fallback when range is invalid - } else if (process.env.NODE_ENV !== 'production') { - console.error( - 'MUI: You need to provide a valueBuffer prop ' + - 'when using the buffer variant of LinearProgress.', + } else { + errorOnce( + true, + 'MUI: You need to provide a valueBuffer prop when using the buffer variant of LinearProgress.', + 'error', + 'linear-progress-value-buffer-required-for-buffer', ); } } diff --git a/packages/mui-material/src/LinearProgress/LinearProgress.test.js b/packages/mui-material/src/LinearProgress/LinearProgress.test.js index bc9936b2149e92..4aa7bb5512c6dc 100644 --- a/packages/mui-material/src/LinearProgress/LinearProgress.test.js +++ b/packages/mui-material/src/LinearProgress/LinearProgress.test.js @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import { reset } from '@mui/utils/errorOnce'; import { createRenderer, screen, @@ -161,28 +162,6 @@ describe('', () => { expect(progressbar).to.have.attribute('aria-valuemax', '100'); }); - describe('prop: value', () => { - it('should warn when not used as expected', () => { - let rerender; - - expect(() => { - ({ rerender } = render()); - }).toErrorDev([ - 'MUI: You need to provide a value prop', - !strictModeDoubleLoggingSuppressed && 'MUI: You need to provide a value prop', - ]); - - expect(() => { - rerender(); - }).toErrorDev([ - 'MUI: You need to provide a value prop', - 'MUI: You need to provide a valueBuffer prop', - !strictModeDoubleLoggingSuppressed && 'MUI: You need to provide a value prop', - !strictModeDoubleLoggingSuppressed && 'MUI: You need to provide a valueBuffer prop', - ]); - }); - }); - describe('prop: min & max', () => { it('should be able to use custom min and max values', () => { render(); @@ -249,83 +228,87 @@ describe('', () => { errorSpy.mockRestore(); }); - it('should warn if the value is out of range', () => { - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=0, max=10, value=-1.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=0, max=10, value=-1.', - ]); - - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=0, max=10, value=11.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=0, max=10, value=11.', - ]); - }); + describe('warnings and errors', () => { + beforeEach(() => { + reset(); + }); + + it.each([ + { variant: 'determinate', value: -1, min: 0, max: 10 }, + { variant: 'determinate', value: 11, min: 0, max: 10 }, + ])( + 'should warn if value=$value is out of range (min=$min, max=$max)', + ({ variant, value, min, max }) => { + expect(() => { + render(); + }).toErrorDev([ + `MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, + ]); + }, + ); - it('should error if the valueBuffer is out of range or less than the value prop', () => { - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=4.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=4.', - ]); - - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=11.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=11.', - ]); - - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=-1.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=-1.', - ]); - }); + it.each([ + { value: 5, valueBuffer: 4, min: 0, max: 10 }, + { value: 5, valueBuffer: 11, min: 0, max: 10 }, + { value: 5, valueBuffer: -1, min: 0, max: 10 }, + ])( + 'should error if valueBuffer=$valueBuffer is out of range or less than value=$value', + ({ value, valueBuffer, min, max }) => { + expect(() => { + render( + , + ); + }).toErrorDev([ + `MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=${min}, max=${max}, value=${value}, valueBuffer=${valueBuffer}.`, + ]); + }, + ); - it('should error if min is equal or greater than max', () => { - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=10, max=0, value=5.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=10, max=0, value=5.', - ]); - expect(() => { - render(); - }).toErrorDev([ - 'MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=10, max=10, value=5.', - !strictModeDoubleLoggingSuppressed && - 'MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=10, max=10, value=5.', - ]); - }); + it.each([ + { value: 5, min: 10, max: 0 }, + { value: 5, min: 10, max: 10 }, + ])('should error if min=$min is equal or greater than max=$max', ({ value, min, max }) => { + expect(() => { + render(); + }).toErrorDev([ + `MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, + ]); + }); + + it.each([ + { variant: 'indeterminate', min: 0, max: undefined }, + { variant: 'query', min: undefined, max: 100 }, + ])( + 'should warn if variant=$variant and min/max props are provided', + ({ variant, min, max }) => { + expect(() => { + render(); + }).toWarnDev([ + "MUI: You have provided the `min` or `max` props with an 'indeterminate' or 'query' variant. These props will have no effect.", + ]); + }, + ); - it('should warn if variant is indeterminate or query and min or max props are provided', () => { - expect(() => { - render(); - }).toWarnDev([ - "MUI: You have provided the `min` or `max` props with an 'indeterminate' or 'query' variant. These props will have no effect.", - !strictModeDoubleLoggingSuppressed && - "MUI: You have provided the `min` or `max` props with an 'indeterminate' or 'query' variant. These props will have no effect.", - ]); - - expect(() => { - render(); - }).toWarnDev([ - "MUI: You have provided the `min` or `max` props with an 'indeterminate' or 'query' variant. These props will have no effect.", - !strictModeDoubleLoggingSuppressed && - "MUI: You have provided the `min` or `max` props with an 'indeterminate' or 'query' variant. These props will have no effect.", - ]); + it('should error if value prop is not provided for the determinate variant', () => { + expect(() => { + render(); + }).toErrorDev(['MUI: You need to provide a value prop']); + }); + + it('should error if value and valueBuffer props are not provided for the buffer variant', () => { + expect(() => { + render(); + }).toErrorDev([ + 'MUI: You need to provide a value prop', + 'MUI: You need to provide a valueBuffer prop', + ]); + }); }); }); }); diff --git a/packages/mui-utils/src/errorOnce/errorOnce.test.ts b/packages/mui-utils/src/errorOnce/errorOnce.test.ts new file mode 100644 index 00000000000000..c79033b16efabd --- /dev/null +++ b/packages/mui-utils/src/errorOnce/errorOnce.test.ts @@ -0,0 +1,80 @@ +import errorOnce, { reset } from './errorOnce'; + +describe('errorOnce', () => { + it('should log an error only once', () => { + const consoleError = console.error; + const errorMock = vi.fn(); + console.error = errorMock; + + errorOnce(true, 'Test error', 'error'); + errorOnce(true, 'Test error', 'error'); + errorOnce(true, 'Test error', 'error'); + + expect(errorMock).toHaveBeenCalledTimes(1); + expect(errorMock).toHaveBeenCalledWith('Test error'); + + console.error = consoleError; + }); + + it('should log a warning only once', () => { + const consoleWarn = console.warn; + const warnMock = vi.fn(); + console.warn = warnMock; + + errorOnce(true, 'Test warning', 'warn'); + errorOnce(true, 'Test warning', 'warn'); + errorOnce(true, 'Test warning', 'warn'); + + expect(warnMock).toHaveBeenCalledTimes(1); + expect(warnMock).toHaveBeenCalledWith('Test warning'); + + console.warn = consoleWarn; + }); + + it('should not log if condition is false', () => { + const consoleError = console.error; + const errorMock = vi.fn(); + console.error = errorMock; + + errorOnce(false, 'This should not log', 'error'); + errorOnce(false, 'This should not log', 'error'); + + expect(errorMock).toHaveBeenCalledTimes(0); + + console.error = consoleError; + }); + + it('should reset the cache with reset function', () => { + const consoleError = console.error; + const errorMock = vi.fn(); + console.error = errorMock; + + errorOnce(true, 'Reset test error', 'error'); + expect(errorMock).toHaveBeenCalledTimes(1); + expect(errorMock).toHaveBeenCalledWith('Reset test error'); + + reset(); + + errorOnce(true, 'Reset test error', 'error'); + expect(errorMock).toHaveBeenCalledTimes(2); + expect(errorMock).toHaveBeenLastCalledWith('Reset test error'); + + console.error = consoleError; + }); + + it('should use key to identify unique messages', () => { + const consoleError = console.error; + const errorMock = vi.fn(); + console.error = errorMock; + + errorOnce(true, 'Message 1', 'error', 'key1'); + errorOnce(true, 'Message 2', 'error', 'key2'); + errorOnce(true, 'Message 1 again', 'error', 'key1'); // Should not log + + expect(errorMock).toHaveBeenCalledTimes(2); + expect(errorMock).toHaveBeenNthCalledWith(1, 'Message 1'); + expect(errorMock).toHaveBeenNthCalledWith(2, 'Message 2'); + + console.error = consoleError; + }); +}); diff --git a/packages/mui-utils/src/errorOnce/errorOnce.ts b/packages/mui-utils/src/errorOnce/errorOnce.ts new file mode 100644 index 00000000000000..c08c8c6cb572e4 --- /dev/null +++ b/packages/mui-utils/src/errorOnce/errorOnce.ts @@ -0,0 +1,37 @@ +const warnedMessages = new Set(); + +/** + * Logs an error or warning message only once. + * @param condition - The condition to check. + * @param message - The message to log. + * @param level - The level of the message ('error' or 'warn'). + * @param key - An optional key to identify the message. If not provided, the message itself is used as the key. + */ +function errorOnce(condition: boolean, message: string, level: 'error' | 'warn', key?: string) { + if (process.env.NODE_ENV !== 'production') { + if (!condition) { + return; + } + + const identifier = key || message; + + if (warnedMessages.has(identifier)) { + return; + } + + warnedMessages.add(identifier); + + if (level === 'error') { + console.error(message); + } else { + console.warn(message); + } + } +} + +function reset() { + warnedMessages.clear(); +} + +export default errorOnce; +export { reset }; diff --git a/packages/mui-utils/src/errorOnce/index.ts b/packages/mui-utils/src/errorOnce/index.ts new file mode 100644 index 00000000000000..d2f74c5e1ea7c4 --- /dev/null +++ b/packages/mui-utils/src/errorOnce/index.ts @@ -0,0 +1,2 @@ +export { default } from './errorOnce'; +export * from './errorOnce'; diff --git a/packages/mui-utils/src/index.ts b/packages/mui-utils/src/index.ts index ffc2b8b1db1ca5..ce2f4e6d8b0c3a 100644 --- a/packages/mui-utils/src/index.ts +++ b/packages/mui-utils/src/index.ts @@ -57,4 +57,6 @@ export { default as unstable_extractEventHandlers } from './extractEventHandlers export { default as unstable_getReactNodeRef } from './getReactNodeRef'; export { default as unstable_getReactElementRef } from './getReactElementRef'; export { default as isEventHandler } from './isEventHandler'; +export { default as errorOnce } from './errorOnce'; +export { reset as errorOnceReset } from './errorOnce'; export * from './types'; From c099c1f2e698168c4c5d2de0da8f5719da569029 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Fri, 29 May 2026 14:11:09 +0300 Subject: [PATCH 2/5] improve tests --- .../mui-utils/src/errorOnce/errorOnce.test.ts | 68 +++++++------------ 1 file changed, 26 insertions(+), 42 deletions(-) diff --git a/packages/mui-utils/src/errorOnce/errorOnce.test.ts b/packages/mui-utils/src/errorOnce/errorOnce.test.ts index c79033b16efabd..2bc2ea3e12ba06 100644 --- a/packages/mui-utils/src/errorOnce/errorOnce.test.ts +++ b/packages/mui-utils/src/errorOnce/errorOnce.test.ts @@ -1,80 +1,64 @@ import errorOnce, { reset } from './errorOnce'; describe('errorOnce', () => { - it('should log an error only once', () => { - const consoleError = console.error; - const errorMock = vi.fn(); - console.error = errorMock; + let errorSpy: ReturnType; + let warnSpy: ReturnType; + + beforeEach(() => { + reset(); + errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + afterEach(() => { + errorSpy.mockRestore(); + warnSpy.mockRestore(); + }); + + it('should log an error only once', () => { errorOnce(true, 'Test error', 'error'); errorOnce(true, 'Test error', 'error'); errorOnce(true, 'Test error', 'error'); - expect(errorMock).toHaveBeenCalledTimes(1); - expect(errorMock).toHaveBeenCalledWith('Test error'); - - console.error = consoleError; + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith('Test error'); }); it('should log a warning only once', () => { - const consoleWarn = console.warn; - const warnMock = vi.fn(); - console.warn = warnMock; - errorOnce(true, 'Test warning', 'warn'); errorOnce(true, 'Test warning', 'warn'); errorOnce(true, 'Test warning', 'warn'); - expect(warnMock).toHaveBeenCalledTimes(1); - expect(warnMock).toHaveBeenCalledWith('Test warning'); - - console.warn = consoleWarn; + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith('Test warning'); }); it('should not log if condition is false', () => { - const consoleError = console.error; - const errorMock = vi.fn(); - console.error = errorMock; - errorOnce(false, 'This should not log', 'error'); errorOnce(false, 'This should not log', 'error'); - expect(errorMock).toHaveBeenCalledTimes(0); - - console.error = consoleError; + expect(errorSpy).toHaveBeenCalledTimes(0); }); it('should reset the cache with reset function', () => { - const consoleError = console.error; - const errorMock = vi.fn(); - console.error = errorMock; - errorOnce(true, 'Reset test error', 'error'); - expect(errorMock).toHaveBeenCalledTimes(1); - expect(errorMock).toHaveBeenCalledWith('Reset test error'); + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith('Reset test error'); reset(); errorOnce(true, 'Reset test error', 'error'); - expect(errorMock).toHaveBeenCalledTimes(2); - expect(errorMock).toHaveBeenLastCalledWith('Reset test error'); - - console.error = consoleError; + expect(errorSpy).toHaveBeenCalledTimes(2); + expect(errorSpy).toHaveBeenLastCalledWith('Reset test error'); }); it('should use key to identify unique messages', () => { - const consoleError = console.error; - const errorMock = vi.fn(); - console.error = errorMock; - errorOnce(true, 'Message 1', 'error', 'key1'); errorOnce(true, 'Message 2', 'error', 'key2'); errorOnce(true, 'Message 1 again', 'error', 'key1'); // Should not log - expect(errorMock).toHaveBeenCalledTimes(2); - expect(errorMock).toHaveBeenNthCalledWith(1, 'Message 1'); - expect(errorMock).toHaveBeenNthCalledWith(2, 'Message 2'); - - console.error = consoleError; + expect(errorSpy).toHaveBeenCalledTimes(2); + expect(errorSpy).toHaveBeenNthCalledWith(1, 'Message 1'); + expect(errorSpy).toHaveBeenNthCalledWith(2, 'Message 2'); }); }); From f0b423f24fd53a9416a1b1c404ff99f6adb33715 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Fri, 29 May 2026 14:11:35 +0300 Subject: [PATCH 3/5] remove unneded import --- .../src/CircularProgress/CircularProgress.test.js | 6 +----- .../mui-material/src/LinearProgress/LinearProgress.test.js | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/mui-material/src/CircularProgress/CircularProgress.test.js b/packages/mui-material/src/CircularProgress/CircularProgress.test.js index 93e207e7cf3df3..68ca73789b975c 100644 --- a/packages/mui-material/src/CircularProgress/CircularProgress.test.js +++ b/packages/mui-material/src/CircularProgress/CircularProgress.test.js @@ -1,10 +1,6 @@ import { expect } from 'chai'; import { reset } from '@mui/utils/errorOnce'; -import { - createRenderer, - strictModeDoubleLoggingSuppressed, - screen, -} from '@mui/internal-test-utils'; +import { createRenderer, screen } from '@mui/internal-test-utils'; import CircularProgress, { circularProgressClasses as classes, } from '@mui/material/CircularProgress'; diff --git a/packages/mui-material/src/LinearProgress/LinearProgress.test.js b/packages/mui-material/src/LinearProgress/LinearProgress.test.js index 4aa7bb5512c6dc..5d20aa60eb8abc 100644 --- a/packages/mui-material/src/LinearProgress/LinearProgress.test.js +++ b/packages/mui-material/src/LinearProgress/LinearProgress.test.js @@ -1,10 +1,6 @@ import { expect } from 'chai'; import { reset } from '@mui/utils/errorOnce'; -import { - createRenderer, - screen, - strictModeDoubleLoggingSuppressed, -} from '@mui/internal-test-utils'; +import { createRenderer, screen } from '@mui/internal-test-utils'; import RtlProvider from '@mui/system/RtlProvider'; import LinearProgress, { linearProgressClasses as classes } from '@mui/material/LinearProgress'; import describeConformance from '../../test/describeConformance'; From 34b2b2bcb9ad769816abfd00a5b5093e6413c861 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Fri, 29 May 2026 15:37:19 +0300 Subject: [PATCH 4/5] add local checks --- .../src/CircularProgress/CircularProgress.js | 37 +++++--- .../CircularProgress/CircularProgress.test.js | 8 +- .../src/LinearProgress/LinearProgress.js | 93 ++++++++++++------- .../src/LinearProgress/LinearProgress.test.js | 4 +- .../mui-utils/src/errorOnce/errorOnce.test.ts | 64 ------------- packages/mui-utils/src/errorOnce/errorOnce.ts | 37 -------- packages/mui-utils/src/errorOnce/index.ts | 2 - packages/mui-utils/src/index.ts | 3 +- 8 files changed, 89 insertions(+), 159 deletions(-) delete mode 100644 packages/mui-utils/src/errorOnce/errorOnce.test.ts delete mode 100644 packages/mui-utils/src/errorOnce/errorOnce.ts delete mode 100644 packages/mui-utils/src/errorOnce/index.ts diff --git a/packages/mui-material/src/CircularProgress/CircularProgress.js b/packages/mui-material/src/CircularProgress/CircularProgress.js index da27653092d002..ffc8fe6c886c84 100644 --- a/packages/mui-material/src/CircularProgress/CircularProgress.js +++ b/packages/mui-material/src/CircularProgress/CircularProgress.js @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import chainPropTypes from '@mui/utils/chainPropTypes'; import composeClasses from '@mui/utils/composeClasses'; -import errorOnce from '@mui/utils/errorOnce'; import { keyframes, css, styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; @@ -14,6 +13,14 @@ import { getCircularProgressUtilityClass } from './circularProgressClasses'; const SIZE = 44; +let warnedMinMaxWithoutVariant = false; +let warnedInvalidMinMaxValue = false; + +export function resetWarningFlags() { + warnedMinMaxWithoutVariant = false; + warnedInvalidMinMaxValue = false; +} + const circularRotateKeyframe = keyframes` 0% { transform: rotate(0deg); @@ -198,12 +205,14 @@ const CircularProgress = React.forwardRef(function CircularProgress(inProps, ref ...other } = props; - errorOnce( - variant === 'indeterminate' && (minProp !== undefined || maxProp !== undefined), - `MUI: You have provided the \`min\` or \`max\` props with an 'indeterminate' variant. These props will have no effect.`, - 'warn', - 'circular-progress-min-max-without-variant', - ); + if (process.env.NODE_ENV !== 'production') { + if (!warnedMinMaxWithoutVariant && variant === 'indeterminate' && (minProp !== undefined || maxProp !== undefined)) { + console.warn( + `MUI: You have provided the \`min\` or \`max\` props with an 'indeterminate' variant. These props will have no effect.`, + ); + warnedMinMaxWithoutVariant = true; + } + } const min = minProp ?? 0; const max = maxProp ?? 100; @@ -228,12 +237,14 @@ const CircularProgress = React.forwardRef(function CircularProgress(inProps, ref if (variant === 'determinate') { const circumference = 2 * Math.PI * ((SIZE - thickness) / 2); - errorOnce( - value < min || value > max || min >= max, - `MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, - 'error', - 'circular-progress-invalid-min-max-value', - ); + if (process.env.NODE_ENV !== 'production') { + if (!warnedInvalidMinMaxValue && (value < min || value > max || min >= max)) { + console.error( + `MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, + ); + warnedInvalidMinMaxValue = true; + } + } const range = max - min; circleStyle.strokeDasharray = circumference.toFixed(3); diff --git a/packages/mui-material/src/CircularProgress/CircularProgress.test.js b/packages/mui-material/src/CircularProgress/CircularProgress.test.js index 68ca73789b975c..18453b57053e41 100644 --- a/packages/mui-material/src/CircularProgress/CircularProgress.test.js +++ b/packages/mui-material/src/CircularProgress/CircularProgress.test.js @@ -1,9 +1,7 @@ import { expect } from 'chai'; -import { reset } from '@mui/utils/errorOnce'; import { createRenderer, screen } from '@mui/internal-test-utils'; -import CircularProgress, { - circularProgressClasses as classes, -} from '@mui/material/CircularProgress'; +import CircularProgress, { circularProgressClasses as classes } from '@mui/material/CircularProgress'; +import { resetWarningFlags } from './CircularProgress'; import describeConformance from '../../test/describeConformance'; describe('', () => { @@ -223,7 +221,7 @@ describe('', () => { describe('warnings and errors', () => { beforeEach(() => { - reset(); + resetWarningFlags(); }); it.each([ diff --git a/packages/mui-material/src/LinearProgress/LinearProgress.js b/packages/mui-material/src/LinearProgress/LinearProgress.js index f737d1b73303c9..d36b523dab185f 100644 --- a/packages/mui-material/src/LinearProgress/LinearProgress.js +++ b/packages/mui-material/src/LinearProgress/LinearProgress.js @@ -3,7 +3,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; -import errorOnce from '@mui/utils/errorOnce'; import { useRtl } from '@mui/system/RtlProvider'; import { keyframes, css, styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; @@ -13,6 +12,20 @@ import capitalize from '../utils/capitalize'; import { getLinearProgressUtilityClass } from './linearProgressClasses'; const TRANSITION_DURATION = 4; // seconds + +let warnedMinMaxWithoutVariant = false; +let warnedInvalidMinMaxValue = false; +let warnedValueRequired = false; +let warnedInvalidMinMaxValueBuffer = false; +let warnedValueBufferRequired = false; + +export function resetWarningFlags() { + warnedMinMaxWithoutVariant = false; + warnedInvalidMinMaxValue = false; + warnedValueRequired = false; + warnedInvalidMinMaxValueBuffer = false; + warnedValueBufferRequired = false; +} const indeterminate1Keyframe = keyframes` 0% { left: -35%; @@ -371,13 +384,18 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { variant, }; - errorOnce( - ['indeterminate', 'query'].includes(variant) && - (minProp !== undefined || maxProp !== undefined), - `MUI: You have provided the \`min\` or \`max\` props with an 'indeterminate' or 'query' variant. These props will have no effect.`, - 'warn', - 'linear-progress-min-max-without-variant', - ); + if (process.env.NODE_ENV !== 'production') { + if ( + !warnedMinMaxWithoutVariant && + ['indeterminate', 'query'].includes(variant) && + (minProp !== undefined || maxProp !== undefined) + ) { + console.warn( + `MUI: You have provided the \`min\` or \`max\` props with an 'indeterminate' or 'query' variant. These props will have no effect.`, + ); + warnedMinMaxWithoutVariant = true; + } + } const min = minProp ?? 0; const max = maxProp ?? 100; @@ -390,12 +408,14 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { if (variant === 'determinate' || variant === 'buffer') { if (value !== undefined) { - errorOnce( - value < min || value > max || min >= max, - `MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, - 'error', - 'linear-progress-invalid-min-max-value', - ); + if (process.env.NODE_ENV !== 'production') { + if (!warnedInvalidMinMaxValue && (value < min || value > max || min >= max)) { + console.error( + `MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, + ); + warnedInvalidMinMaxValue = true; + } + } const range = max - min; let transform = ((value - min) / range) * 100 - 100; @@ -407,23 +427,28 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { rootProps['aria-valuenow'] = value; rootProps['aria-valuemin'] = min; rootProps['aria-valuemax'] = max; - } else { - errorOnce( - true, - 'MUI: You need to provide a value prop when using the determinate or buffer variant of LinearProgress.', - 'error', - 'linear-progress-value-required-for-determinate-buffer', - ); + } else if (process.env.NODE_ENV !== 'production') { + if (!warnedValueRequired) { + console.error( + 'MUI: You need to provide a value prop when using the determinate or buffer variant of LinearProgress.', + ); + warnedValueRequired = true; + } } } if (variant === 'buffer') { if (valueBuffer !== undefined) { - errorOnce( - valueBuffer < min || valueBuffer > max || valueBuffer < value || min >= max, - `MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=${min}, max=${max}, value=${value}, valueBuffer=${valueBuffer}.`, - 'error', - 'linear-progress-invalid-min-max-value-buffer', - ); + if (process.env.NODE_ENV !== 'production') { + if ( + !warnedInvalidMinMaxValueBuffer && + (valueBuffer < min || valueBuffer > max || valueBuffer < value || min >= max) + ) { + console.error( + `MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=${min}, max=${max}, value=${value}, valueBuffer=${valueBuffer}.`, + ); + warnedInvalidMinMaxValueBuffer = true; + } + } const range = max - min; let transform = ((valueBuffer - min) / range) * 100 - 100; @@ -431,13 +456,13 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { transform = -transform; } inlineStyles.bar2.transform = range > 0 ? `translateX(${transform}%)` : 'translateX(-100%)'; // empty-state fallback when range is invalid - } else { - errorOnce( - true, - 'MUI: You need to provide a valueBuffer prop when using the buffer variant of LinearProgress.', - 'error', - 'linear-progress-value-buffer-required-for-buffer', - ); + } else if (process.env.NODE_ENV !== 'production') { + if (!warnedValueBufferRequired) { + console.error( + 'MUI: You need to provide a valueBuffer prop when using the buffer variant of LinearProgress.', + ); + warnedValueBufferRequired = true; + } } } diff --git a/packages/mui-material/src/LinearProgress/LinearProgress.test.js b/packages/mui-material/src/LinearProgress/LinearProgress.test.js index 5d20aa60eb8abc..4a06df880d1496 100644 --- a/packages/mui-material/src/LinearProgress/LinearProgress.test.js +++ b/packages/mui-material/src/LinearProgress/LinearProgress.test.js @@ -1,8 +1,8 @@ import { expect } from 'chai'; -import { reset } from '@mui/utils/errorOnce'; import { createRenderer, screen } from '@mui/internal-test-utils'; import RtlProvider from '@mui/system/RtlProvider'; import LinearProgress, { linearProgressClasses as classes } from '@mui/material/LinearProgress'; +import { resetWarningFlags } from './LinearProgress'; import describeConformance from '../../test/describeConformance'; describe('', () => { @@ -226,7 +226,7 @@ describe('', () => { describe('warnings and errors', () => { beforeEach(() => { - reset(); + resetWarningFlags(); }); it.each([ diff --git a/packages/mui-utils/src/errorOnce/errorOnce.test.ts b/packages/mui-utils/src/errorOnce/errorOnce.test.ts deleted file mode 100644 index 2bc2ea3e12ba06..00000000000000 --- a/packages/mui-utils/src/errorOnce/errorOnce.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import errorOnce, { reset } from './errorOnce'; - -describe('errorOnce', () => { - let errorSpy: ReturnType; - let warnSpy: ReturnType; - - beforeEach(() => { - reset(); - errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - }); - - afterEach(() => { - errorSpy.mockRestore(); - warnSpy.mockRestore(); - }); - - it('should log an error only once', () => { - errorOnce(true, 'Test error', 'error'); - errorOnce(true, 'Test error', 'error'); - errorOnce(true, 'Test error', 'error'); - - expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy).toHaveBeenCalledWith('Test error'); - }); - - it('should log a warning only once', () => { - errorOnce(true, 'Test warning', 'warn'); - errorOnce(true, 'Test warning', 'warn'); - errorOnce(true, 'Test warning', 'warn'); - - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy).toHaveBeenCalledWith('Test warning'); - }); - - it('should not log if condition is false', () => { - errorOnce(false, 'This should not log', 'error'); - errorOnce(false, 'This should not log', 'error'); - - expect(errorSpy).toHaveBeenCalledTimes(0); - }); - - it('should reset the cache with reset function', () => { - errorOnce(true, 'Reset test error', 'error'); - expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy).toHaveBeenCalledWith('Reset test error'); - - reset(); - - errorOnce(true, 'Reset test error', 'error'); - expect(errorSpy).toHaveBeenCalledTimes(2); - expect(errorSpy).toHaveBeenLastCalledWith('Reset test error'); - }); - - it('should use key to identify unique messages', () => { - errorOnce(true, 'Message 1', 'error', 'key1'); - errorOnce(true, 'Message 2', 'error', 'key2'); - errorOnce(true, 'Message 1 again', 'error', 'key1'); // Should not log - - expect(errorSpy).toHaveBeenCalledTimes(2); - expect(errorSpy).toHaveBeenNthCalledWith(1, 'Message 1'); - expect(errorSpy).toHaveBeenNthCalledWith(2, 'Message 2'); - }); -}); diff --git a/packages/mui-utils/src/errorOnce/errorOnce.ts b/packages/mui-utils/src/errorOnce/errorOnce.ts deleted file mode 100644 index c08c8c6cb572e4..00000000000000 --- a/packages/mui-utils/src/errorOnce/errorOnce.ts +++ /dev/null @@ -1,37 +0,0 @@ -const warnedMessages = new Set(); - -/** - * Logs an error or warning message only once. - * @param condition - The condition to check. - * @param message - The message to log. - * @param level - The level of the message ('error' or 'warn'). - * @param key - An optional key to identify the message. If not provided, the message itself is used as the key. - */ -function errorOnce(condition: boolean, message: string, level: 'error' | 'warn', key?: string) { - if (process.env.NODE_ENV !== 'production') { - if (!condition) { - return; - } - - const identifier = key || message; - - if (warnedMessages.has(identifier)) { - return; - } - - warnedMessages.add(identifier); - - if (level === 'error') { - console.error(message); - } else { - console.warn(message); - } - } -} - -function reset() { - warnedMessages.clear(); -} - -export default errorOnce; -export { reset }; diff --git a/packages/mui-utils/src/errorOnce/index.ts b/packages/mui-utils/src/errorOnce/index.ts deleted file mode 100644 index d2f74c5e1ea7c4..00000000000000 --- a/packages/mui-utils/src/errorOnce/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './errorOnce'; -export * from './errorOnce'; diff --git a/packages/mui-utils/src/index.ts b/packages/mui-utils/src/index.ts index ce2f4e6d8b0c3a..af9371279a8bcc 100644 --- a/packages/mui-utils/src/index.ts +++ b/packages/mui-utils/src/index.ts @@ -57,6 +57,5 @@ export { default as unstable_extractEventHandlers } from './extractEventHandlers export { default as unstable_getReactNodeRef } from './getReactNodeRef'; export { default as unstable_getReactElementRef } from './getReactElementRef'; export { default as isEventHandler } from './isEventHandler'; -export { default as errorOnce } from './errorOnce'; -export { reset as errorOnceReset } from './errorOnce'; + export * from './types'; From 9ece45e2ed407ff5b37c07cf999e9daeacb41003 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Fri, 29 May 2026 15:37:42 +0300 Subject: [PATCH 5/5] prettier --- .../mui-material/src/CircularProgress/CircularProgress.js | 6 +++++- .../src/CircularProgress/CircularProgress.test.js | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/mui-material/src/CircularProgress/CircularProgress.js b/packages/mui-material/src/CircularProgress/CircularProgress.js index ffc8fe6c886c84..d7337118478e37 100644 --- a/packages/mui-material/src/CircularProgress/CircularProgress.js +++ b/packages/mui-material/src/CircularProgress/CircularProgress.js @@ -206,7 +206,11 @@ const CircularProgress = React.forwardRef(function CircularProgress(inProps, ref } = props; if (process.env.NODE_ENV !== 'production') { - if (!warnedMinMaxWithoutVariant && variant === 'indeterminate' && (minProp !== undefined || maxProp !== undefined)) { + if ( + !warnedMinMaxWithoutVariant && + variant === 'indeterminate' && + (minProp !== undefined || maxProp !== undefined) + ) { console.warn( `MUI: You have provided the \`min\` or \`max\` props with an 'indeterminate' variant. These props will have no effect.`, ); diff --git a/packages/mui-material/src/CircularProgress/CircularProgress.test.js b/packages/mui-material/src/CircularProgress/CircularProgress.test.js index 18453b57053e41..ce420cce68de83 100644 --- a/packages/mui-material/src/CircularProgress/CircularProgress.test.js +++ b/packages/mui-material/src/CircularProgress/CircularProgress.test.js @@ -1,6 +1,8 @@ import { expect } from 'chai'; import { createRenderer, screen } from '@mui/internal-test-utils'; -import CircularProgress, { circularProgressClasses as classes } from '@mui/material/CircularProgress'; +import CircularProgress, { + circularProgressClasses as classes, +} from '@mui/material/CircularProgress'; import { resetWarningFlags } from './CircularProgress'; import describeConformance from '../../test/describeConformance';