From 7a94e938fe14b37d66996afe946ca744ae4bf49d Mon Sep 17 00:00:00 2001 From: Sergey Myssak Date: Wed, 14 Jun 2023 22:36:38 +0600 Subject: [PATCH] [CCI] Add `useDeprecatedPropWarning` and align with `deprecated` hoc (#762) * Add useDeprecatedPropWarning and align with deprecated hoc (#761) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak * Add multiple props to useDeprecatedPropWarning and pass getMessage to deprecatedComponentWarning (#761) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak * Use ExclusiveUnion in interfaces (#761) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak --------- Signed-off-by: Sergey Myssak Co-authored-by: Andrey Myssak --- .../loading/loading_elastic.test.tsx | 11 +- src/components/loading/loading_elastic.tsx | 14 +- .../loading/loading_kibana.test.tsx | 11 +- src/components/loading/loading_kibana.tsx | 12 +- .../page/page_header/page_header.test.tsx | 48 +++++- .../page/page_header/page_header.tsx | 14 +- .../__snapshots__/deprecated.test.tsx.snap | 6 +- src/utils/deprecated/deprecated.test.tsx | 163 ++++++++++++++++-- src/utils/deprecated/deprecated.tsx | 58 ++++++- 9 files changed, 286 insertions(+), 51 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..e15b9a18bb 100644 --- a/src/components/loading/loading_elastic.tsx +++ b/src/components/loading/loading_elastic.tsx @@ -32,7 +32,7 @@ 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'; const sizeToClassNameMap = { m: 'ouiLoadingElastic--medium', @@ -43,9 +43,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 +63,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({ + newComponentName: '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..c54eee9a10 100644 --- a/src/components/loading/loading_kibana.tsx +++ b/src/components/loading/loading_kibana.tsx @@ -32,7 +32,7 @@ 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'; const sizeToClassNameMap = { m: 'ouiLoadingKibana--medium', @@ -42,9 +42,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 +67,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({ + 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 10414b41da..afb02c7e84 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,48 @@ describe('OuiPageHeader', () => { }); }); }); + + describe('deprecation', () => { + it('should console 1 deprecation warning without repetition', () => { + console.warn = jest.fn(); + + const component = mount(); + component.setProps({ iconType: 'database' }); + + 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(); + + 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(); + + 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..97e2359c3e 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,10 @@ 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({ + props: { iconType, 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..69daed1c1b 100644 --- a/src/utils/deprecated/deprecated.test.tsx +++ b/src/utils/deprecated/deprecated.test.tsx @@ -3,37 +3,168 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { FC } 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 ExampleComponent = () =>
; + ExampleComponent.displayName = 'Example'; + + const Example = deprecatedComponentWarning({ + newComponentName: 'NewComponent', + version: '2.0.0', + })(ExampleComponent); + + const component = mount(); + component.setProps({ name: 'new' }); - 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 console custom warning', () => { console.warn = jest.fn(); - const Component = () =>
; - const DeprecatedComponent = deprecated(warning)(Component); + 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(); + + const ExampleComponent = () =>
; + ExampleComponent.displayName = 'Example'; + + const Example = deprecatedComponentWarning({ + newComponentName: 'New Component', + version: '2.0.0', + })(ExampleComponent); - const component = render(); + const component = render(); expect(component).toMatchSnapshot(); }); it('should properly name DeprecatedWrapper function', () => { - const Component = () =>
; - const DeprecatedComponent = deprecated(warning)(Component); + const ExampleComponent = () =>
; + ExampleComponent.displayName = 'Example'; + + const Example = deprecatedComponentWarning({ + newComponentName: 'New Component', + version: '2.0.0', + })(ExampleComponent); + + expect(Example.name).toEqual('Example'); + }); +}); + +describe('useDeprecatedPropWarning', () => { + interface IExampleComponent { + name?: string; + age?: number; + version?: string; + getMessage?: (propName: string) => string; + } + + const ExampleDefaultMessageComponent: FC = ({ + name, + age, + version, + }) => { + useDeprecatedPropWarning({ props: { name, age }, version }); + + return
; + }; + + 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(); + component.setProps({ name: 'new name' }); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + '[DEPRECATED] The `name` prop is deprecated and will be removed.' + ); + }); + + 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.', + '[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(); - 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..926a2aba49 100644 --- a/src/utils/deprecated/deprecated.tsx +++ b/src/utils/deprecated/deprecated.tsx @@ -3,25 +3,75 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; +import { ExclusiveUnion } from '../../components/common'; export const getDeprecatedMessage = (message: string): string => `[DEPRECATED] ${message}`; -export const deprecated = (message: string) => { +type DeprecatedComponentWarning = ExclusiveUnion< + { newComponentName: string; version?: string }, + { getMessage?: (deprecatedComponentName: string) => string } +>; + +export const deprecatedComponentWarning = ({ + newComponentName, + version, + getMessage, +}: DeprecatedComponentWarning) => { return >(Component: T): T => { + const deprecatedComponentName = Component.displayName || Component.name; + const DeprecatedWrapper = (props: React.ComponentProps) => { useEffect(() => { - console.warn(getDeprecatedMessage(message)); + 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); }, []); return ; }; Object.defineProperty(DeprecatedWrapper, 'name', { - value: Component.displayName || Component.name, + value: deprecatedComponentName, }); return DeprecatedWrapper as T; }; }; + +type _DeprecatedPropWarningExclusiveProps = ExclusiveUnion< + { version?: string }, + { getMessage?: (deprecatedComponentName: string) => string } +>; + +type DeprecatedPropWarning = { + props: Record; +} & _DeprecatedPropWarningExclusiveProps; + +export const useDeprecatedPropWarning = ({ + props, + version, + getMessage, +}: DeprecatedPropWarning): void => { + const warnedProps = useRef(new Set()).current; + + useEffect(() => { + Object.entries(props).forEach(([name, value]) => { + if (value !== undefined && !warnedProps.has(name)) { + 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, getMessage]); +};