From 5c007dfa31018c37480aeb17290a0b69fcf1c7e3 Mon Sep 17 00:00:00 2001 From: Sergey Myssak Date: Wed, 17 May 2023 17:51:50 +0600 Subject: [PATCH 1/3] Add useDeprecatedPropWarning and align with deprecated hoc (#761) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak --- .../loading/loading_elastic.test.tsx | 11 ++- src/components/loading/loading_elastic.tsx | 15 ++-- .../loading/loading_kibana.test.tsx | 11 ++- src/components/loading/loading_kibana.tsx | 13 +-- .../page/page_header/page_header.test.tsx | 28 ++++++- .../page/page_header/page_header.tsx | 21 +++-- .../__snapshots__/deprecated.test.tsx.snap | 6 +- src/utils/deprecated/deprecated.test.tsx | 84 +++++++++++++++---- src/utils/deprecated/deprecated.tsx | 39 ++++++++- 9 files changed, 180 insertions(+), 48 deletions(-) diff --git a/src/components/loading/loading_elastic.test.tsx b/src/components/loading/loading_elastic.test.tsx index 7b57964d4b..1038252515 100644 --- a/src/components/loading/loading_elastic.test.tsx +++ b/src/components/loading/loading_elastic.test.tsx @@ -6,8 +6,7 @@ import React from 'react'; import { render, mount } from 'enzyme'; import { requiredProps } from '../../test'; -import { getDeprecatedMessage } from '../../utils'; -import { OuiLoadingElastic, SIZES, WARNING } from './loading_elastic'; +import { OuiLoadingElastic, SIZES } from './loading_elastic'; describe('OuiLoadingElastic', () => { test('is rendered', () => { @@ -28,10 +27,14 @@ describe('OuiLoadingElastic', () => { }); }); - it('should console warning about a deprecated component', () => { + it('should console deprecation warning', () => { console.warn = jest.fn(); + mount(); - expect(console.warn).toHaveBeenCalledWith(getDeprecatedMessage(WARNING)); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] OuiLoadingElastic is deprecated in favor of OuiLoadingDashboards and will be removed in v2.0.0.' + ); }); }); diff --git a/src/components/loading/loading_elastic.tsx b/src/components/loading/loading_elastic.tsx index 19756c10c2..4d8665d487 100644 --- a/src/components/loading/loading_elastic.tsx +++ b/src/components/loading/loading_elastic.tsx @@ -32,7 +32,8 @@ import React, { HTMLAttributes, FunctionComponent } from 'react'; import classNames from 'classnames'; import { CommonProps, keysOf } from '../common'; import { OuiIcon } from '../icon'; -import { deprecated } from '../../utils'; +import { deprecatedComponentWarning } from '../../utils'; +import { OuiLoadingDashboards } from './loading_dashboards'; const sizeToClassNameMap = { m: 'ouiLoadingElastic--medium', @@ -43,9 +44,6 @@ const sizeToClassNameMap = { export const SIZES = keysOf(sizeToClassNameMap); -export const WARNING = - 'OuiLoadingElastic is deprecated in favor of OuiLoadingDashboards and will be removed in v2.0.0.'; - export interface OuiLoadingElasticProps { size?: keyof typeof sizeToClassNameMap; } @@ -66,9 +64,12 @@ const OuiLoadingElasticComponent: FunctionComponent< ); }; +OuiLoadingElasticComponent.displayName = 'OuiLoadingElastic'; + /** * @deprecated OuiLoadingElastic is deprecated in favor of OuiLoadingDashboards and will be removed in v2.0.0. */ -export const OuiLoadingElastic = deprecated(WARNING)( - OuiLoadingElasticComponent -); +export const OuiLoadingElastic = deprecatedComponentWarning({ + NewComponent: OuiLoadingDashboards, + version: '2.0.0', +})(OuiLoadingElasticComponent); diff --git a/src/components/loading/loading_kibana.test.tsx b/src/components/loading/loading_kibana.test.tsx index 8dfb53bc05..a0a417eef1 100644 --- a/src/components/loading/loading_kibana.test.tsx +++ b/src/components/loading/loading_kibana.test.tsx @@ -31,8 +31,7 @@ import React from 'react'; import { render, mount } from 'enzyme'; import { requiredProps } from '../../test'; -import { getDeprecatedMessage } from '../../utils'; -import { OuiLoadingKibana, SIZES, WARNING } from './loading_kibana'; +import { OuiLoadingKibana, SIZES } from './loading_kibana'; describe('OuiLoadingKibana', () => { test('is rendered', () => { @@ -53,10 +52,14 @@ describe('OuiLoadingKibana', () => { }); }); - it('should console warning about a deprecated component', () => { + it('should console deprecation warning', () => { console.warn = jest.fn(); + mount(); - expect(console.warn).toHaveBeenCalledWith(getDeprecatedMessage(WARNING)); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] OuiLoadingKibana is deprecated in favor of OuiLoadingLogo and will be removed in v2.0.0.' + ); }); }); diff --git a/src/components/loading/loading_kibana.tsx b/src/components/loading/loading_kibana.tsx index f315595f7d..55d5b0f7bd 100644 --- a/src/components/loading/loading_kibana.tsx +++ b/src/components/loading/loading_kibana.tsx @@ -32,7 +32,8 @@ import React, { HTMLAttributes, FunctionComponent } from 'react'; import classNames from 'classnames'; import { CommonProps, keysOf } from '../common'; import { OuiIcon } from '../icon'; -import { deprecated } from '../../utils'; +import { deprecatedComponentWarning } from '../../utils'; +import { OuiLoadingLogo } from './loading_logo'; const sizeToClassNameMap = { m: 'ouiLoadingKibana--medium', @@ -42,9 +43,6 @@ const sizeToClassNameMap = { export const SIZES = keysOf(sizeToClassNameMap); -export const WARNING = - 'OuiLoadingKibana is deprecated in favor of OuiLoadingLogo and will be removed in v2.0.0.'; - export type OuiLoadingKibanaProps = CommonProps & HTMLAttributes & { size?: keyof typeof sizeToClassNameMap; @@ -70,7 +68,12 @@ const OuiLoadingKibanaComponent: FunctionComponent = ({ ); }; +OuiLoadingKibanaComponent.displayName = 'OuiLoadingKibana'; + /** * @deprecated OuiLoadingKibana is deprecated in favor of OuiLoadingLogo and will be removed in v2.0.0. */ -export const OuiLoadingKibana = deprecated(WARNING)(OuiLoadingKibanaComponent); +export const OuiLoadingKibana = deprecatedComponentWarning({ + NewComponent: OuiLoadingLogo, + version: '2.0.0', +})(OuiLoadingKibanaComponent); diff --git a/src/components/page/page_header/page_header.test.tsx b/src/components/page/page_header/page_header.test.tsx index 10414b41da..dce7880597 100644 --- a/src/components/page/page_header/page_header.test.tsx +++ b/src/components/page/page_header/page_header.test.tsx @@ -29,8 +29,8 @@ */ import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../../test/required_props'; +import { mount, render } from 'enzyme'; +import { requiredProps } from '../../../test'; import { OuiPageHeader, OuiPageHeaderProps } from './page_header'; import { ALIGN_ITEMS } from './page_header_content'; @@ -124,4 +124,28 @@ describe('OuiPageHeader', () => { }); }); }); + + it('should console deprecation warning', () => { + console.warn = jest.fn(); + + mount( + + ); + + expect(console.warn).toHaveBeenCalledTimes(2); + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] The `iconType` prop is deprecated and will be removed in v2.0.0' + ); + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] The `iconProps` prop is deprecated and will be removed in v2.0.0' + ); + }); + + it('should not console deprecation warning', () => { + console.warn = jest.fn(); + + mount(); + + expect(console.warn).not.toHaveBeenCalled(); + }); }); diff --git a/src/components/page/page_header/page_header.tsx b/src/components/page/page_header/page_header.tsx index 50cf06ac87..79f06adb31 100644 --- a/src/components/page/page_header/page_header.tsx +++ b/src/components/page/page_header/page_header.tsx @@ -28,7 +28,7 @@ * under the License. */ -import React, { FunctionComponent, HTMLAttributes, useEffect } from 'react'; +import React, { FunctionComponent, HTMLAttributes } from 'react'; import classNames from 'classnames'; import { CommonProps, keysOf } from '../../common'; import { @@ -39,6 +39,7 @@ import { _OuiPageRestrictWidth, setPropsForRestrictedPageWidth, } from '../_restrict_width'; +import { useDeprecatedPropWarning } from '../../../utils'; const paddingSizeToClassNameMap = { none: null, @@ -92,13 +93,17 @@ export const OuiPageHeader: FunctionComponent = ({ style ); - useEffect(() => { - if (iconType || iconProps) { - console.warn( - 'WARNING: The `iconType` and `iconProps` properties in `OuiPageHeader` are deprecated and will be removed in the future. Please update your code accordingly.' - ); - } - }, [iconType, iconProps]); + useDeprecatedPropWarning({ + deprecatedProp: iconType, + deprecatedPropName: 'iconType', + version: '2.0.0', + }); + + useDeprecatedPropWarning({ + deprecatedProp: iconProps, + deprecatedPropName: 'iconProps', + version: '2.0.0', + }); const classes = classNames( 'ouiPageHeader', diff --git a/src/utils/deprecated/__snapshots__/deprecated.test.tsx.snap b/src/utils/deprecated/__snapshots__/deprecated.test.tsx.snap index f31fac9b05..16811bd308 100644 --- a/src/utils/deprecated/__snapshots__/deprecated.test.tsx.snap +++ b/src/utils/deprecated/__snapshots__/deprecated.test.tsx.snap @@ -1,3 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`deprecated should render component 1`] = `
`; +exports[`deprecatedComponentWarning should render wrapped component 1`] = ` +
+`; diff --git a/src/utils/deprecated/deprecated.test.tsx b/src/utils/deprecated/deprecated.test.tsx index 331428e39c..193c00f0fd 100644 --- a/src/utils/deprecated/deprecated.test.tsx +++ b/src/utils/deprecated/deprecated.test.tsx @@ -5,35 +5,89 @@ import React from 'react'; import { mount, render } from 'enzyme'; -import { deprecated, getDeprecatedMessage } from './deprecated'; - -describe('deprecated', () => { - const warning = 'This component is deprecated in favor of another.'; +import { + deprecatedComponentWarning, + useDeprecatedPropWarning, +} from './deprecated'; +describe('deprecatedComponentWarning', () => { it('should console warning', () => { console.warn = jest.fn(); - const Component = () =>
; - const DeprecatedComponent = deprecated(warning)(Component); - mount(); + const NewComponent = () =>
; + const ExampleComponent = () =>
; + ExampleComponent.displayName = 'Example'; + + const Example = deprecatedComponentWarning({ + NewComponent, + version: '2.0.0', + })(ExampleComponent); + + mount(); - expect(console.warn).toHaveBeenCalledWith(getDeprecatedMessage(warning)); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] Example is deprecated in favor of NewComponent and will be removed in v2.0.0.' + ); }); - it('should render component', () => { + it('should render wrapped component', () => { console.warn = jest.fn(); - const Component = () =>
; - const DeprecatedComponent = deprecated(warning)(Component); + const NewComponent = () =>
; + const ExampleComponent = () =>
; + ExampleComponent.displayName = 'Example'; - const component = render(); + const Example = deprecatedComponentWarning({ + NewComponent, + version: '2.0.0', + })(ExampleComponent); + + const component = render(); expect(component).toMatchSnapshot(); }); it('should properly name DeprecatedWrapper function', () => { - const Component = () =>
; - const DeprecatedComponent = deprecated(warning)(Component); + const NewComponent = () =>
; + const ExampleComponent = () =>
; + ExampleComponent.displayName = 'Example'; + + const Example = deprecatedComponentWarning({ + NewComponent, + version: '2.0.0', + })(ExampleComponent); + + expect(Example.name).toEqual('Example'); + }); +}); + +describe('useDeprecatedPropWarning', () => { + const ExampleComponent = ({ name }: { name?: string }) => { + useDeprecatedPropWarning({ + deprecatedProp: name, + deprecatedPropName: 'name', + version: '2.0.0', + }); + + return
{name}
; + }; + + it('should console warning', () => { + console.warn = jest.fn(); + + mount(); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] The `name` prop is deprecated and will be removed in v2.0.0' + ); + }); + + it('should not console warning', () => { + console.warn = jest.fn(); + + mount(); - expect(DeprecatedComponent.name).toEqual('Component'); + expect(console.warn).not.toHaveBeenCalled(); }); }); diff --git a/src/utils/deprecated/deprecated.tsx b/src/utils/deprecated/deprecated.tsx index 6d4c80b40f..3205ee49df 100644 --- a/src/utils/deprecated/deprecated.tsx +++ b/src/utils/deprecated/deprecated.tsx @@ -5,14 +5,34 @@ import React, { useEffect } from 'react'; +interface IDeprecatedComponentWarning { + NewComponent: React.ComponentType; + version: string; +} + +interface IDeprecatedPropWarning { + deprecatedProp: any; + deprecatedPropName: string; + version: string; +} + export const getDeprecatedMessage = (message: string): string => `[DEPRECATED] ${message}`; -export const deprecated = (message: string) => { +export const deprecatedComponentWarning = ({ + NewComponent, + version, +}: IDeprecatedComponentWarning) => { return >(Component: T): T => { + const deprecatedComponentName = Component.displayName || Component.name; + const newComponentName = NewComponent.displayName || NewComponent.name; + const DeprecatedWrapper = (props: React.ComponentProps) => { useEffect(() => { - console.warn(getDeprecatedMessage(message)); + const formattedMessage = `${deprecatedComponentName} is deprecated in favor of ${newComponentName} and will be removed in v${version}.`; + const deprecatedMessage = getDeprecatedMessage(formattedMessage); + + console.warn(deprecatedMessage); }, []); return ; @@ -25,3 +45,18 @@ export const deprecated = (message: string) => { return DeprecatedWrapper as T; }; }; + +export const useDeprecatedPropWarning = ({ + deprecatedProp, + deprecatedPropName, + version, +}: IDeprecatedPropWarning): void => { + useEffect(() => { + if (deprecatedProp !== undefined) { + const formattedMessage = `The \`${deprecatedPropName}\` prop is deprecated and will be removed in v${version}`; + const deprecatedMessage = getDeprecatedMessage(formattedMessage); + + console.warn(deprecatedMessage); + } + }, [deprecatedProp, deprecatedPropName, version]); +}; From 37b4bb6bef714e467baff1e6afa952afd64d83b0 Mon Sep 17 00:00:00 2001 From: Sergey Myssak Date: Fri, 19 May 2023 21:37:49 +0600 Subject: [PATCH 2/3] Add multiple props to useDeprecatedPropWarning and pass getMessage to deprecatedComponentWarning (#761) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak --- src/components/loading/loading_elastic.tsx | 3 +- src/components/loading/loading_kibana.tsx | 3 +- .../page/page_header/page_header.test.tsx | 54 ++++++++++++------ .../page/page_header/page_header.tsx | 9 +-- src/utils/deprecated/deprecated.test.tsx | 43 ++++++++++----- src/utils/deprecated/deprecated.tsx | 55 ++++++++++--------- 6 files changed, 100 insertions(+), 67 deletions(-) diff --git a/src/components/loading/loading_elastic.tsx b/src/components/loading/loading_elastic.tsx index 4d8665d487..e15b9a18bb 100644 --- a/src/components/loading/loading_elastic.tsx +++ b/src/components/loading/loading_elastic.tsx @@ -33,7 +33,6 @@ import classNames from 'classnames'; import { CommonProps, keysOf } from '../common'; import { OuiIcon } from '../icon'; import { deprecatedComponentWarning } from '../../utils'; -import { OuiLoadingDashboards } from './loading_dashboards'; const sizeToClassNameMap = { m: 'ouiLoadingElastic--medium', @@ -70,6 +69,6 @@ OuiLoadingElasticComponent.displayName = 'OuiLoadingElastic'; * @deprecated OuiLoadingElastic is deprecated in favor of OuiLoadingDashboards and will be removed in v2.0.0. */ export const OuiLoadingElastic = deprecatedComponentWarning({ - NewComponent: OuiLoadingDashboards, + newComponentName: 'OuiLoadingDashboards', version: '2.0.0', })(OuiLoadingElasticComponent); diff --git a/src/components/loading/loading_kibana.tsx b/src/components/loading/loading_kibana.tsx index 55d5b0f7bd..c54eee9a10 100644 --- a/src/components/loading/loading_kibana.tsx +++ b/src/components/loading/loading_kibana.tsx @@ -33,7 +33,6 @@ import classNames from 'classnames'; import { CommonProps, keysOf } from '../common'; import { OuiIcon } from '../icon'; import { deprecatedComponentWarning } from '../../utils'; -import { OuiLoadingLogo } from './loading_logo'; const sizeToClassNameMap = { m: 'ouiLoadingKibana--medium', @@ -74,6 +73,6 @@ OuiLoadingKibanaComponent.displayName = 'OuiLoadingKibana'; * @deprecated OuiLoadingKibana is deprecated in favor of OuiLoadingLogo and will be removed in v2.0.0. */ export const OuiLoadingKibana = deprecatedComponentWarning({ - NewComponent: OuiLoadingLogo, + newComponentName: 'OuiLoadingLogo', version: '2.0.0', })(OuiLoadingKibanaComponent); diff --git a/src/components/page/page_header/page_header.test.tsx b/src/components/page/page_header/page_header.test.tsx index dce7880597..02a57be00a 100644 --- a/src/components/page/page_header/page_header.test.tsx +++ b/src/components/page/page_header/page_header.test.tsx @@ -125,27 +125,47 @@ describe('OuiPageHeader', () => { }); }); - it('should console deprecation warning', () => { - console.warn = jest.fn(); + describe('deprecation', () => { + it('should console 1 deprecation warning without repetition', () => { + console.warn = jest.fn(); - mount( - - ); + const component = mount(); + component.setProps({ iconType: 'database' }); - expect(console.warn).toHaveBeenCalledTimes(2); - expect(console.warn).toHaveBeenCalledWith( - '[DEPRECATED] The `iconType` prop is deprecated and will be removed in v2.0.0' - ); - expect(console.warn).toHaveBeenCalledWith( - '[DEPRECATED] The `iconProps` prop is deprecated and will be removed in v2.0.0' - ); - }); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] The `iconType` prop is deprecated and will be removed in v2.0.0' + ); + }); + + it('should console 2 deprecation warning without repetition', () => { + console.warn = jest.fn(); - it('should not console deprecation warning', () => { - console.warn = jest.fn(); + const component = mount( + + ); + component.setProps({ + iconType: 'database', + iconProps: { color: 'blue' }, + }); + + const results = [ + '[DEPRECATED] The `iconType` prop is deprecated and will be removed in v2.0.0', + '[DEPRECATED] The `iconProps` prop is deprecated and will be removed in v2.0.0', + ]; + + expect(console.warn).toHaveBeenCalledTimes(2); + results.forEach((item) => + expect(console.warn).toHaveBeenCalledWith(item) + ); + }); + + it('should not console deprecation warning', () => { + console.warn = jest.fn(); - mount(); + mount(); - expect(console.warn).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/components/page/page_header/page_header.tsx b/src/components/page/page_header/page_header.tsx index 79f06adb31..97e2359c3e 100644 --- a/src/components/page/page_header/page_header.tsx +++ b/src/components/page/page_header/page_header.tsx @@ -94,14 +94,7 @@ export const OuiPageHeader: FunctionComponent = ({ ); useDeprecatedPropWarning({ - deprecatedProp: iconType, - deprecatedPropName: 'iconType', - version: '2.0.0', - }); - - useDeprecatedPropWarning({ - deprecatedProp: iconProps, - deprecatedPropName: 'iconProps', + props: { iconType, iconProps }, version: '2.0.0', }); diff --git a/src/utils/deprecated/deprecated.test.tsx b/src/utils/deprecated/deprecated.test.tsx index 193c00f0fd..595762a351 100644 --- a/src/utils/deprecated/deprecated.test.tsx +++ b/src/utils/deprecated/deprecated.test.tsx @@ -14,16 +14,16 @@ describe('deprecatedComponentWarning', () => { it('should console warning', () => { console.warn = jest.fn(); - const NewComponent = () =>
; const ExampleComponent = () =>
; ExampleComponent.displayName = 'Example'; const Example = deprecatedComponentWarning({ - NewComponent, + newComponentName: 'NewComponent', version: '2.0.0', })(ExampleComponent); - mount(); + const component = mount(); + component.setProps({ name: 'new' }); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( @@ -34,12 +34,11 @@ describe('deprecatedComponentWarning', () => { it('should render wrapped component', () => { console.warn = jest.fn(); - const NewComponent = () =>
; const ExampleComponent = () =>
; ExampleComponent.displayName = 'Example'; const Example = deprecatedComponentWarning({ - NewComponent, + newComponentName: 'New Component', version: '2.0.0', })(ExampleComponent); @@ -48,12 +47,11 @@ describe('deprecatedComponentWarning', () => { }); it('should properly name DeprecatedWrapper function', () => { - const NewComponent = () =>
; const ExampleComponent = () =>
; ExampleComponent.displayName = 'Example'; const Example = deprecatedComponentWarning({ - NewComponent, + newComponentName: 'New Component', version: '2.0.0', })(ExampleComponent); @@ -62,20 +60,24 @@ describe('deprecatedComponentWarning', () => { }); describe('useDeprecatedPropWarning', () => { - const ExampleComponent = ({ name }: { name?: string }) => { + const ExampleComponent = ({ name, age }: { name?: string; age?: number }) => { useDeprecatedPropWarning({ - deprecatedProp: name, - deprecatedPropName: 'name', + props: { name, age }, version: '2.0.0', }); - return
{name}
; + return ( +
+ {name} {age} +
+ ); }; - it('should console warning', () => { + it('should console 1 warning without repetition', () => { console.warn = jest.fn(); - mount(); + const component = mount(); + component.setProps({ name: 'new name' }); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( @@ -83,6 +85,21 @@ describe('useDeprecatedPropWarning', () => { ); }); + it('should console 2 warning without repetition', () => { + console.warn = jest.fn(); + + const component = mount(); + component.setProps({ name: 'new name', age: 22 }); + + const results = [ + '[DEPRECATED] The `name` prop is deprecated and will be removed in v2.0.0', + '[DEPRECATED] The `age` prop is deprecated and will be removed in v2.0.0', + ]; + + expect(console.warn).toHaveBeenCalledTimes(2); + results.forEach((item) => expect(console.warn).toHaveBeenCalledWith(item)); + }); + it('should not console warning', () => { console.warn = jest.fn(); diff --git a/src/utils/deprecated/deprecated.tsx b/src/utils/deprecated/deprecated.tsx index 3205ee49df..5603ba3a06 100644 --- a/src/utils/deprecated/deprecated.tsx +++ b/src/utils/deprecated/deprecated.tsx @@ -3,34 +3,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect } from 'react'; - -interface IDeprecatedComponentWarning { - NewComponent: React.ComponentType; - version: string; -} - -interface IDeprecatedPropWarning { - deprecatedProp: any; - deprecatedPropName: string; - version: string; -} +import React, { useEffect, useRef } from 'react'; export const getDeprecatedMessage = (message: string): string => `[DEPRECATED] ${message}`; +interface IDeprecatedComponentWarning { + newComponentName: string; + version?: string; + getMessage?: (deprecatedComponentName: string) => string; +} + export const deprecatedComponentWarning = ({ - NewComponent, + newComponentName, version, + getMessage, }: IDeprecatedComponentWarning) => { return >(Component: T): T => { const deprecatedComponentName = Component.displayName || Component.name; - const newComponentName = NewComponent.displayName || NewComponent.name; const DeprecatedWrapper = (props: React.ComponentProps) => { useEffect(() => { - const formattedMessage = `${deprecatedComponentName} is deprecated in favor of ${newComponentName} and will be removed in v${version}.`; - const deprecatedMessage = getDeprecatedMessage(formattedMessage); + const message = + getMessage?.(deprecatedComponentName) || + `${deprecatedComponentName} is deprecated in favor of ${newComponentName} and will be removed in v${version}.`; + const deprecatedMessage = getDeprecatedMessage(message); console.warn(deprecatedMessage); }, []); @@ -39,24 +36,32 @@ export const deprecatedComponentWarning = ({ }; Object.defineProperty(DeprecatedWrapper, 'name', { - value: Component.displayName || Component.name, + value: deprecatedComponentName, }); return DeprecatedWrapper as T; }; }; +interface IDeprecatedPropWarning { + props: Record; + version: string; +} export const useDeprecatedPropWarning = ({ - deprecatedProp, - deprecatedPropName, + props, version, }: IDeprecatedPropWarning): void => { + const warnedProps = useRef(new Set()).current; + useEffect(() => { - if (deprecatedProp !== undefined) { - const formattedMessage = `The \`${deprecatedPropName}\` prop is deprecated and will be removed in v${version}`; - const deprecatedMessage = getDeprecatedMessage(formattedMessage); + Object.entries(props).forEach(([name, value]) => { + if (value !== undefined && !warnedProps.has(name)) { + const message = `The \`${name}\` prop is deprecated and will be removed in v${version}`; + const deprecatedMessage = getDeprecatedMessage(message); - console.warn(deprecatedMessage); - } - }, [deprecatedProp, deprecatedPropName, version]); + warnedProps.add(name); + console.warn(deprecatedMessage); + } + }); + }, [warnedProps, props, version]); }; From ef50a6712a5f0aa931913fba9715e15663e3c37a Mon Sep 17 00:00:00 2001 From: Sergey Myssak Date: Thu, 25 May 2023 08:30:47 +0600 Subject: [PATCH 3/3] Use ExclusiveUnion in interfaces (#761) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak --- .../page/page_header/page_header.test.tsx | 6 +- src/utils/deprecated/deprecated.test.tsx | 94 +++++++++++++++---- src/utils/deprecated/deprecated.tsx | 42 +++++---- 3 files changed, 106 insertions(+), 36 deletions(-) diff --git a/src/components/page/page_header/page_header.test.tsx b/src/components/page/page_header/page_header.test.tsx index 02a57be00a..afb02c7e84 100644 --- a/src/components/page/page_header/page_header.test.tsx +++ b/src/components/page/page_header/page_header.test.tsx @@ -134,7 +134,7 @@ describe('OuiPageHeader', () => { expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( - '[DEPRECATED] The `iconType` prop is deprecated and will be removed in v2.0.0' + '[DEPRECATED] The `iconType` prop is deprecated and will be removed in v2.0.0.' ); }); @@ -150,8 +150,8 @@ describe('OuiPageHeader', () => { }); const results = [ - '[DEPRECATED] The `iconType` prop is deprecated and will be removed in v2.0.0', - '[DEPRECATED] The `iconProps` prop is deprecated and will be removed in v2.0.0', + '[DEPRECATED] The `iconType` prop is deprecated and will be removed in v2.0.0.', + '[DEPRECATED] The `iconProps` prop is deprecated and will be removed in v2.0.0.', ]; expect(console.warn).toHaveBeenCalledTimes(2); diff --git a/src/utils/deprecated/deprecated.test.tsx b/src/utils/deprecated/deprecated.test.tsx index 595762a351..69daed1c1b 100644 --- a/src/utils/deprecated/deprecated.test.tsx +++ b/src/utils/deprecated/deprecated.test.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { FC } from 'react'; import { mount, render } from 'enzyme'; import { deprecatedComponentWarning, @@ -31,6 +31,25 @@ describe('deprecatedComponentWarning', () => { ); }); + it('should console custom warning', () => { + console.warn = jest.fn(); + + const ExampleComponent = () =>
; + ExampleComponent.displayName = 'Example'; + + const Example = deprecatedComponentWarning({ + getMessage: (componentName) => `Custom message for \`${componentName}\`.`, + })(ExampleComponent); + + const component = mount(); + component.setProps({ name: 'new' }); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] Custom message for `Example`.' + ); + }); + it('should render wrapped component', () => { console.warn = jest.fn(); @@ -60,50 +79,91 @@ describe('deprecatedComponentWarning', () => { }); describe('useDeprecatedPropWarning', () => { - const ExampleComponent = ({ name, age }: { name?: string; age?: number }) => { - useDeprecatedPropWarning({ - props: { name, age }, - version: '2.0.0', - }); + interface IExampleComponent { + name?: string; + age?: number; + version?: string; + getMessage?: (propName: string) => string; + } + + const ExampleDefaultMessageComponent: FC = ({ + name, + age, + version, + }) => { + useDeprecatedPropWarning({ props: { name, age }, version }); + + return
; + }; - return ( -
- {name} {age} -
- ); + const ExampleCustomMessageComponent: FC = ({ + name, + age, + getMessage, + }) => { + useDeprecatedPropWarning({ props: { name, age }, getMessage }); + + return
; }; it('should console 1 warning without repetition', () => { console.warn = jest.fn(); - const component = mount(); + const component = mount(); component.setProps({ name: 'new name' }); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( - '[DEPRECATED] The `name` prop is deprecated and will be removed in v2.0.0' + '[DEPRECATED] The `name` prop is deprecated and will be removed.' ); }); it('should console 2 warning without repetition', () => { console.warn = jest.fn(); - const component = mount(); + const component = mount( + + ); component.setProps({ name: 'new name', age: 22 }); const results = [ - '[DEPRECATED] The `name` prop is deprecated and will be removed in v2.0.0', - '[DEPRECATED] The `age` prop is deprecated and will be removed in v2.0.0', + '[DEPRECATED] The `name` prop is deprecated and will be removed.', + '[DEPRECATED] The `age` prop is deprecated and will be removed.', ]; expect(console.warn).toHaveBeenCalledTimes(2); results.forEach((item) => expect(console.warn).toHaveBeenCalledWith(item)); }); + it('should console warning with version', () => { + console.warn = jest.fn(); + + mount(); + + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] The `name` prop is deprecated and will be removed in v2.0.0.' + ); + }); + + it('should console warning with custom message', () => { + console.warn = jest.fn(); + + mount( + `Custom message: \`${propName}\`.`} + /> + ); + + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] Custom message: `name`.' + ); + }); + it('should not console warning', () => { console.warn = jest.fn(); - mount(); + mount(); expect(console.warn).not.toHaveBeenCalled(); }); diff --git a/src/utils/deprecated/deprecated.tsx b/src/utils/deprecated/deprecated.tsx index 5603ba3a06..926a2aba49 100644 --- a/src/utils/deprecated/deprecated.tsx +++ b/src/utils/deprecated/deprecated.tsx @@ -4,29 +4,30 @@ */ import React, { useEffect, useRef } from 'react'; +import { ExclusiveUnion } from '../../components/common'; export const getDeprecatedMessage = (message: string): string => `[DEPRECATED] ${message}`; -interface IDeprecatedComponentWarning { - newComponentName: string; - version?: string; - getMessage?: (deprecatedComponentName: string) => string; -} +type DeprecatedComponentWarning = ExclusiveUnion< + { newComponentName: string; version?: string }, + { getMessage?: (deprecatedComponentName: string) => string } +>; export const deprecatedComponentWarning = ({ newComponentName, version, getMessage, -}: IDeprecatedComponentWarning) => { +}: DeprecatedComponentWarning) => { return >(Component: T): T => { const deprecatedComponentName = Component.displayName || Component.name; const DeprecatedWrapper = (props: React.ComponentProps) => { useEffect(() => { - const message = - getMessage?.(deprecatedComponentName) || - `${deprecatedComponentName} is deprecated in favor of ${newComponentName} and will be removed in v${version}.`; + const defaultMessage = version + ? `${deprecatedComponentName} is deprecated in favor of ${newComponentName} and will be removed in v${version}.` + : `${deprecatedComponentName} is deprecated in favor of ${newComponentName} and will be removed.`; + const message = getMessage?.(deprecatedComponentName) || defaultMessage; const deprecatedMessage = getDeprecatedMessage(message); console.warn(deprecatedMessage); @@ -43,25 +44,34 @@ export const deprecatedComponentWarning = ({ }; }; -interface IDeprecatedPropWarning { - props: Record; - version: string; -} +type _DeprecatedPropWarningExclusiveProps = ExclusiveUnion< + { version?: string }, + { getMessage?: (deprecatedComponentName: string) => string } +>; + +type DeprecatedPropWarning = { + props: Record; +} & _DeprecatedPropWarningExclusiveProps; + export const useDeprecatedPropWarning = ({ props, version, -}: IDeprecatedPropWarning): void => { + getMessage, +}: DeprecatedPropWarning): void => { const warnedProps = useRef(new Set()).current; useEffect(() => { Object.entries(props).forEach(([name, value]) => { if (value !== undefined && !warnedProps.has(name)) { - const message = `The \`${name}\` prop is deprecated and will be removed in v${version}`; + const defaultMessage = version + ? `The \`${name}\` prop is deprecated and will be removed in v${version}.` + : `The \`${name}\` prop is deprecated and will be removed.`; + const message = getMessage?.(name) || defaultMessage; const deprecatedMessage = getDeprecatedMessage(message); warnedProps.add(name); console.warn(deprecatedMessage); } }); - }, [warnedProps, props, version]); + }, [warnedProps, props, version, getMessage]); };