From 0dbbf482a5be8321f0e2309a1242ae02a6c54b74 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 3 Jun 2025 12:15:37 +0200 Subject: [PATCH] [Lint] Officially deprecate Enzyme (#221779) ## Summary Please see for [context](https://docs.google.com/document/d/1J9mGfmGukgFS-6jqD1zopH8wIRvsqNhDhQbjQZ9hjEY/edit?tab=t.hzzf0a72f9gd) This PR tries to discourage usage of enzyme: - Mark shared test utils that use enzyme as `@deprecated` - Add eslint warning for imports of `enzyme` - Add ci stat counter of imports of `enzyme` --- .eslintrc.js | 14 +++++++++ .../kbn-docs-utils/src/build_api_docs_cli.ts | 12 +++++++- .../count_enzyme_imports.test.ts | 25 ++++++++++++++++ .../count_enzyme_imports.ts | 30 +++++++++++++++++++ .../src/count_enzyme_imports/index.ts | 10 +++++++ .../test_enzyme_import.test.ts | 18 +++++++++++ packages/kbn-eslint-plugin-eslint/index.js | 1 + .../rules/no_deprecated_imports.js | 19 ++++++++++++ .../src/enzyme_helpers.tsx | 19 +++++++++++- .../src/find_test_subject.ts | 2 ++ .../src/testbed/mount_component.tsx | 6 ++++ .../src/testbed/testbed.ts | 2 ++ .../src/testing_library_react_helpers.tsx | 24 ++++++++++----- .../auto_follow_pattern_list.test.js | 2 +- .../follower_indices_list.test.js | 2 +- 15 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 packages/kbn-docs-utils/src/count_enzyme_imports/count_enzyme_imports.test.ts create mode 100644 packages/kbn-docs-utils/src/count_enzyme_imports/count_enzyme_imports.ts create mode 100644 packages/kbn-docs-utils/src/count_enzyme_imports/index.ts create mode 100644 packages/kbn-docs-utils/src/count_enzyme_imports/test_enzyme_import.test.ts create mode 100644 packages/kbn-eslint-plugin-eslint/rules/no_deprecated_imports.js diff --git a/.eslintrc.js b/.eslintrc.js index a5fd6d85be2f..154ff3aae1d7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -391,6 +391,19 @@ const RESTRICTED_IMPORTS = [ }, ]; +/** + * Imports that are deprecated and should be phased out + * They are not restricted until fully removed, + * but will log a warning + **/ +const DEPRECATED_IMPORTS = [ + { + name: 'enzyme', + message: + 'Enzyme is deprecated and no longer maintained. Please use @testing-library/react instead.', + }, +]; + module.exports = { root: true, @@ -918,6 +931,7 @@ module.exports = { files: ['**/*.{js,mjs,ts,tsx}'], rules: { 'no-restricted-imports': ['error', ...RESTRICTED_IMPORTS], + '@kbn/eslint/no_deprecated_imports': ['warn', ...DEPRECATED_IMPORTS], 'no-restricted-modules': [ 'error', { diff --git a/packages/kbn-docs-utils/src/build_api_docs_cli.ts b/packages/kbn-docs-utils/src/build_api_docs_cli.ts index 55cfe901402f..e4deb6fc6e0e 100644 --- a/packages/kbn-docs-utils/src/build_api_docs_cli.ts +++ b/packages/kbn-docs-utils/src/build_api_docs_cli.ts @@ -34,6 +34,7 @@ import { writeDeprecationDueByTeam } from './mdx/write_deprecations_due_by_team' import { trimDeletedDocsFromNav } from './trim_deleted_docs_from_nav'; import { getAllDocFileIds } from './mdx/get_all_doc_file_ids'; import { getPathsByPackage } from './get_paths_by_package'; +import { countEnzymeImports, EnzymeImportCounts } from './count_enzyme_imports'; function isStringArray(arr: unknown | string[]): arr is string[] { return Array.isArray(arr) && arr.every((p) => typeof p === 'string'); @@ -144,7 +145,9 @@ export function runBuildApiDocsCli() { const reporter = CiStatsReporter.fromEnv(log); - const allPluginStats: { [key: string]: PluginMetaInfo & ApiStats & EslintDisableCounts } = {}; + const allPluginStats: { + [key: string]: PluginMetaInfo & ApiStats & EslintDisableCounts & EnzymeImportCounts; + } = {}; for (const plugin of plugins) { const id = plugin.id; @@ -162,6 +165,7 @@ export function runBuildApiDocsCli() { allPluginStats[id] = { ...(await countEslintDisableLines(paths)), + ...(await countEnzymeImports(paths)), ...collectApiStatsForPlugin( pluginApi, missingApiItems, @@ -283,6 +287,12 @@ export function runBuildApiDocsCli() { group: 'Total ESLint disabled count', value: pluginStats.eslintDisableFileCount + pluginStats.eslintDisableLineCount, }, + { + id, + meta: { pluginTeam }, + group: 'Enzyme imports', + value: pluginStats.enzymeImportCount, + }, ]); const getLink = (d: ApiDeclaration) => diff --git a/packages/kbn-docs-utils/src/count_enzyme_imports/count_enzyme_imports.test.ts b/packages/kbn-docs-utils/src/count_enzyme_imports/count_enzyme_imports.test.ts new file mode 100644 index 000000000000..988a59bd2027 --- /dev/null +++ b/packages/kbn-docs-utils/src/count_enzyme_imports/count_enzyme_imports.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Path from 'path'; +import { getRepoFiles } from '@kbn/get-repo-files'; +import { countEnzymeImports } from './count_enzyme_imports'; + +describe('count', () => { + test('number of "enzyme" imports in this file', async () => { + const { enzymeImportCount } = await countEnzymeImports([Path.resolve(__dirname, __filename)]); + expect(enzymeImportCount).toBe(0); + }); + + test('number of "enzyme" imports in this directory', async () => { + const allFiles = await getRepoFiles([__dirname]); + const { enzymeImportCount } = await countEnzymeImports(Array.from(allFiles, (f) => f.abs)); + expect(enzymeImportCount).toBe(1); + }); +}); diff --git a/packages/kbn-docs-utils/src/count_enzyme_imports/count_enzyme_imports.ts b/packages/kbn-docs-utils/src/count_enzyme_imports/count_enzyme_imports.ts new file mode 100644 index 000000000000..f87d13664a75 --- /dev/null +++ b/packages/kbn-docs-utils/src/count_enzyme_imports/count_enzyme_imports.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { asyncForEachWithLimit } from '@kbn/std'; +import Fs from 'fs'; + +export interface EnzymeImportCounts { + enzymeImportCount: number; +} + +const count = (s: string, r: RegExp) => Array.from(s.matchAll(r)).length; + +export async function countEnzymeImports(paths: string[]): Promise { + let enzymeImportCount = 0; + + await asyncForEachWithLimit(paths, 100, async (path) => { + const content = await Fs.promises.readFile(path, 'utf8'); + enzymeImportCount += + count(content, /import\s+[^;]*?from\s+['"]enzyme['"]/g) + + count(content, /require\(['"]enzyme['"]\)/g); + }); + + return { enzymeImportCount }; +} diff --git a/packages/kbn-docs-utils/src/count_enzyme_imports/index.ts b/packages/kbn-docs-utils/src/count_enzyme_imports/index.ts new file mode 100644 index 000000000000..ad47d481a6ff --- /dev/null +++ b/packages/kbn-docs-utils/src/count_enzyme_imports/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { countEnzymeImports, type EnzymeImportCounts } from './count_enzyme_imports'; diff --git a/packages/kbn-docs-utils/src/count_enzyme_imports/test_enzyme_import.test.ts b/packages/kbn-docs-utils/src/count_enzyme_imports/test_enzyme_import.test.ts new file mode 100644 index 000000000000..28913dff46e8 --- /dev/null +++ b/packages/kbn-docs-utils/src/count_enzyme_imports/test_enzyme_import.test.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +describe('test_enzyme_import', () => { + it('renders a div', () => { + const wrapper = shallow(React.createElement('div', null, 'Hello World')); + expect(wrapper.is('div')).toBe(true); + }); +}); diff --git a/packages/kbn-eslint-plugin-eslint/index.js b/packages/kbn-eslint-plugin-eslint/index.js index e022fee797c0..cd85187d561d 100644 --- a/packages/kbn-eslint-plugin-eslint/index.js +++ b/packages/kbn-eslint-plugin-eslint/index.js @@ -21,5 +21,6 @@ module.exports = { no_unsafe_console: require('./rules/no_unsafe_console'), no_unsafe_hash: require('./rules/no_unsafe_hash'), require_kibana_feature_privileges_naming: require('./rules/require_kibana_feature_privileges_naming'), + no_deprecated_imports: require('./rules/no_deprecated_imports'), }, }; diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_imports.js b/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_imports.js new file mode 100644 index 000000000000..12e03fd17749 --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_imports.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +const Linter = require('eslint').Linter; + +const coreRule = new Linter().getRules().get('no-restricted-imports'); + +/** + * This rule is used to prevent the use of deprecated imports in Kibana code. + * It is a wrapper around the core ESLint rule `no-restricted-imports` with + * a different id to avoid conflicts with the core rule. + */ +module.exports = coreRule; diff --git a/src/platform/packages/shared/kbn-test-jest-helpers/src/enzyme_helpers.tsx b/src/platform/packages/shared/kbn-test-jest-helpers/src/enzyme_helpers.tsx index f1843d6d9c8c..4f6a262ba241 100644 --- a/src/platform/packages/shared/kbn-test-jest-helpers/src/enzyme_helpers.tsx +++ b/src/platform/packages/shared/kbn-test-jest-helpers/src/enzyme_helpers.tsx @@ -53,6 +53,8 @@ function getOptions(context = {}, props = {}) { } /** + * @deprecated - use `renderWithKibanaRenderContext` or `renderWithI18n` with mocked nested components instead to switch from Enzyme to Testing Library + * * Creates the wrapper instance using shallow with provided intl object into context * * @param node The React element or cheerio wrapper @@ -74,6 +76,8 @@ export function shallowWithIntl( } /** + * @deprecated - use `renderWithKibanaRenderContext` or `renderWithI18n` instead to switch from Enzyme to Testing Library + * * Creates the wrapper instance using mount with provided intl object into context * * @param node The React element or cheerio wrapper @@ -92,7 +96,9 @@ export function mountWithIntl(node: React.ReactElement, options?: MountRendererP } /** - * Creates the wrapper instance using render with provided intl object into context + * @deprecated - use `renderWithKibanaRenderContext` or `renderWithI18n` instead to switch from Enzyme to Testing Library + * + * Creates the wrapper instance using render with provided intl object into context * * @param node The React element or cheerio wrapper * @param options properties to pass into render wrapper @@ -129,6 +135,8 @@ interface ReactHookWrapper { } /** + * @deprecated - use `renderHook` from testing-library/react instead to switch from Enzyme to Testing Library + * * Allows for execution of hooks inside of a test component which records the * returned values. * @@ -186,6 +194,9 @@ export const mountHook = ( }; }; +/** + * @deprecated - use `renderWithKibanaRenderContext` or `renderWithI18n` instead to switch from Enzyme to Testing Library + */ export function shallowWithI18nProvider( child: ReactElement, options?: Omit & { @@ -196,6 +207,9 @@ export function shallowWithI18nProvider( return wrapped.children().dive(); } +/** + * @deprecated - use `renderWithKibanaRenderContext` or `renderWithI18n` instead to switch from Enzyme to Testing Library + */ export function mountWithI18nProvider( child: ReactElement, options?: Omit & { @@ -206,6 +220,9 @@ export function mountWithI18nProvider( return wrapped.children().childAt(0); } +/** + * @deprecated - use `renderWithKibanaRenderContext` or `renderWithI18n` instead to switch from Enzyme to Testing Library + */ export function renderWithI18nProvider( child: ReactElement, options?: Omit & { diff --git a/src/platform/packages/shared/kbn-test-jest-helpers/src/find_test_subject.ts b/src/platform/packages/shared/kbn-test-jest-helpers/src/find_test_subject.ts index c4f3d91f3594..bd808c368610 100644 --- a/src/platform/packages/shared/kbn-test-jest-helpers/src/find_test_subject.ts +++ b/src/platform/packages/shared/kbn-test-jest-helpers/src/find_test_subject.ts @@ -21,6 +21,8 @@ const MATCHERS: Matcher[] = [ ]; /** + * @deprecated - use '@testing-library/react' and byId query instead. + * * Find node which matches a specific test subject selector. Returns ReactWrappers around DOM element, * https://github.com/airbnb/enzyme/tree/master/docs/api/ReactWrapper. * Common use cases include calling simulate or getDOMNode on the returned ReactWrapper. diff --git a/src/platform/packages/shared/kbn-test-jest-helpers/src/testbed/mount_component.tsx b/src/platform/packages/shared/kbn-test-jest-helpers/src/testbed/mount_component.tsx index da481469cfe3..1f1a0dcd59b7 100644 --- a/src/platform/packages/shared/kbn-test-jest-helpers/src/testbed/mount_component.tsx +++ b/src/platform/packages/shared/kbn-test-jest-helpers/src/testbed/mount_component.tsx @@ -48,6 +48,9 @@ function getCompFromConfig>({ return Comp; } +/** + * @deprecated - use @testing-library/react instead + */ export function mountComponentSync>( config: Config ): ReactWrapper { @@ -55,6 +58,9 @@ export function mountComponentSync>( return mountWithIntl(); } +/** + * @deprecated - use @testing-library/react instead + */ export async function mountComponentAsync>( config: Config ): Promise { diff --git a/src/platform/packages/shared/kbn-test-jest-helpers/src/testbed/testbed.ts b/src/platform/packages/shared/kbn-test-jest-helpers/src/testbed/testbed.ts index 106cb67cdb8c..9972e49001c7 100644 --- a/src/platform/packages/shared/kbn-test-jest-helpers/src/testbed/testbed.ts +++ b/src/platform/packages/shared/kbn-test-jest-helpers/src/testbed/testbed.ts @@ -35,6 +35,8 @@ const defaultConfig: TestBedConfig = { }; /** + * @deprecated - use @testing-library/react instead + * * Register a new Testbed to test a React Component. * * @param Component The component under test diff --git a/src/platform/packages/shared/kbn-test-jest-helpers/src/testing_library_react_helpers.tsx b/src/platform/packages/shared/kbn-test-jest-helpers/src/testing_library_react_helpers.tsx index 30dc649f5006..f44ebebdcb6a 100644 --- a/src/platform/packages/shared/kbn-test-jest-helpers/src/testing_library_react_helpers.tsx +++ b/src/platform/packages/shared/kbn-test-jest-helpers/src/testing_library_react_helpers.tsx @@ -7,19 +7,29 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -/** - * Components using the @kbn/i18n module require access to the intl context. - * This is not available when mounting single components in Enzyme. - * These helper functions aim to address that and wrap a valid, - * intl context around them. - */ - import React from 'react'; import { render } from '@testing-library/react'; import { I18nProvider } from '@kbn/i18n-react'; +import { EuiThemeProvider } from '@elastic/eui'; + +export const renderWithKibanaRenderContext = (...args: Parameters) => { + const [ui, ...remainingRenderArgs] = args; + return render( + + {ui} + , + ...remainingRenderArgs + ); +}; export const renderWithI18n = (...args: Parameters) => { const [ui, ...remainingRenderArgs] = args; // Avoid using { wrapper: I18nProvider } in case the caller adds a custom wrapper. return render({ui}, ...remainingRenderArgs); }; + +export const renderWithEuiTheme = (...args: Parameters) => { + const [ui, ...remainingRenderArgs] = args; + // Avoid using { wrapper: EuiThemeProvider } in case the caller adds a custom wrapper. + return render({ui}, ...remainingRenderArgs); +}; diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js index e2de5b4dbc09..ae2b3946f839 100644 --- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -5,8 +5,8 @@ * 2.0. */ -import { getAutoFollowPatternMock } from './fixtures/auto_follow_pattern'; import './mocks'; +import { getAutoFollowPatternMock } from './fixtures/auto_follow_pattern'; import { setupEnvironment, pageHelpers, nextTick, delay, getRandomString } from './helpers'; const { setup } = pageHelpers.autoFollowPatternList; diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js index 839aa48464bb..ec65ca95225b 100644 --- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js @@ -7,8 +7,8 @@ import { act } from 'react-dom/test-utils'; -import { getFollowerIndexMock } from './fixtures/follower_index'; import './mocks'; +import { getFollowerIndexMock } from './fixtures/follower_index'; import { setupEnvironment, pageHelpers, getRandomString } from './helpers'; const { setup } = pageHelpers.followerIndexList;