From 1c46799a522c40bcf77605876564ea10eec2bb29 Mon Sep 17 00:00:00 2001 From: MariaAga Date: Tue, 9 Jul 2024 15:02:21 +0100 Subject: [PATCH] Fixes #37637 - Test plugins from foreman core --- .eslintrc | 2 + babel.config.js | 4 + package.json | 22 ++- script/npm_test_plugin.js | 135 ++++++++++++++++++ .../TemplateGeneratorActions.test.js | 12 +- webpack/{test_setup.js => core_test_setup.js} | 7 + webpack/global_test_setup.js | 19 +++ webpack/jest.config.js | 71 +++++++++ webpack/theforeman-test.js | 36 +++++ 9 files changed, 300 insertions(+), 8 deletions(-) create mode 100644 babel.config.js create mode 100755 script/npm_test_plugin.js rename webpack/{test_setup.js => core_test_setup.js} (73%) create mode 100644 webpack/global_test_setup.js create mode 100644 webpack/jest.config.js create mode 100644 webpack/theforeman-test.js diff --git a/.eslintrc b/.eslintrc index 0de7f9bd00b..23fa34db5e2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -107,6 +107,7 @@ "noopener", "noreferrer", "nowrap", + "npx", "num", "numpad", "operatingsystem", @@ -159,6 +160,7 @@ "textarea", "textfield", "tfm", + "theforeman", "timepicker", "timerdelay", "timeseries", diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000000..e55bdc5f95a --- /dev/null +++ b/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: [require.resolve('@theforeman/builder/babel')], + plugins: [require.resolve('babel-plugin-dynamic-import-node')], +}; diff --git a/package.json b/package.json index 98a59097288..d3ee8a9a6e3 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,10 @@ "lint:custom": "eslint ./webpack", "foreman-js:link": "./script/npm_link_foreman_js.sh", "postlint": "./script/npm_lint_plugins.js", - "test": "tfm-test", + "test": "npx jest --setupFilesAfterEnv ./global_test_setup.js ./core_test_setup.js --testPathIgnorePatterns '/node_modules/' '/.+fixtures.+' --config ./webpack/jest.config.js ", "test:watch": "tfm-test --watchAll", "test:current": "tfm-test --watch", + "test:plugins": "./script/npm_test_plugin.js", "publish-coverage": "tfm-publish-coverage", "postinstall": "./script/npm_install_plugins.js", "analyze": "./script/webpack-analyze" @@ -31,26 +32,42 @@ "devDependencies": { "@babel/core": "^7.7.0", "@theforeman/builder": "^13.1.0", + "@theforeman/vendor-core": "^13.1.0", "@theforeman/eslint-plugin-foreman": "^13.1.0", "@theforeman/eslint-plugin-rules": "^13.1.0", - "@theforeman/test": "^13.1.0", "@theforeman/vendor-dev": "^13.1.0", + "@testing-library/jest-dom": "^5.3.0", + "@testing-library/react": "^10.0.2", + "@testing-library/react-hooks": "^3.4.2", "@types/jest": "<27.0.0", + "axios-mock-adapter": "^1.1.7", + "babel-jest": "^26.3.0", "argv-parse": "^1.0.1", "babel-eslint": "^10.0.0", "babel-loader": "^8.0.0", "buffer": "^5.7.1", "compression-webpack-plugin": "^10.0.0", "cross-env": "^5.2.0", + "cheerio": "1.0.0-rc.10", "css-loader": "^6.8.1", "dotenv": "^5.0.0", "eslint": "^6.7.2", "eslint-plugin-spellcheck": "0.0.17", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.2", + "enzyme-to-json": "^3.4.3", + "identity-obj-proxy": "^3.0.0", + "jest": "^26.4.0", + "jest-svg-transformer": "^1.0.0", + "jest-prop-type-error": "^1.1.0", "graphql": "^15.5.0", + "jest-transform-graphql": "^2.1.0", "path-browserify": "^1.0.1", "prettier": "^1.19.1", "pretty-format": "26.6.2", "react-dnd-test-backend": "^9.4.0", + "react-redux-test-utils": "^0.2.0", + "react-test-renderer": "^17.0.1", "redux-mock-store": "^1.2.2", "sass": "~1.60.0", "sass-loader": "^13.3.2", @@ -60,6 +77,7 @@ "tabbable": "~5.2.0", "victory-core": "~36.8.6", "victory-pie": "~36.8.6", + "victory-legend": "~36.8.6", "webpack": "^5.75.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^5.0.1", diff --git a/script/npm_test_plugin.js b/script/npm_test_plugin.js new file mode 100755 index 00000000000..ffe14f56735 --- /dev/null +++ b/script/npm_test_plugin.js @@ -0,0 +1,135 @@ +#!/usr/bin/env node +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable no-console */ +/* eslint-disable no-var */ + +/* This script is used to run tests for all plugins that have a `lint` script defined in their package.json + To run tests for an individual plugin, pass the plugin name as the first argument to the script + For example, to run tests for the `foreman-tasks` plugin, run: `npm run test-plugin foreman-tasks` + To pass arguments to jest, pass them after the plugin name like so: `npm run test-plugin foreman-tasks -- --debug` +*/ + +var fs = require('fs'); +var path = require('path'); +var lodash = require('lodash'); +var childProcess = require('child_process'); +var { packageJsonDirs } = require('./plugin_webpack_directories'); + +const passedArgs = process.argv.slice(2); +const coreConfigPath = path.resolve(__dirname, '../webpack/jest.config.js'); +const coreConfig = require(coreConfigPath); + +function runChildProcess(args, pluginPath) { + return new Promise((resolve, reject) => { + const child = childProcess.spawn('npx', args, { + shell: true, + }); + // this is needed to make sure the output is not cut + let stdoutBuffer = ''; + child.stdout.on('data', data => { + stdoutBuffer += data.toString(); + const lines = stdoutBuffer.split('\n'); + stdoutBuffer = lines.pop(); + }); + + let stderrBuffer = `${pluginPath}: \n`; + child.stderr.on('data', data => { + stderrBuffer += data.toString(); + const lines = stderrBuffer.split('\n'); + stderrBuffer = lines.pop(); + lines.forEach(line => console.error(line)); + }); + child.on('close', code => { + if (stdoutBuffer) console.log(stdoutBuffer); + if (stderrBuffer) console.error(stderrBuffer); + if (code === 0) { + resolve(); + } else { + reject(new Error(`Child process exited with code ${code}`)); + } + }); + }); +} +const runTests = async () => { + function pluginDefinesLint(pluginPath) { + var packageHasNodeModules = fs.existsSync(`${pluginPath}/node_modules`); // skip gems + var packageData = JSON.parse(fs.readFileSync(`${pluginPath}/package.json`)); + + return ( + packageHasNodeModules && packageData.scripts && packageData.scripts.lint + ); + } + var dirs = packageJsonDirs(); + if (passedArgs[0] && passedArgs[0][0] !== '-') { + dirs = dirs.filter(dir => dir.endsWith(passedArgs[0])); + passedArgs.shift(); + } + for (const pluginPath of dirs) { + if (pluginDefinesLint(pluginPath)) { + const testSetupFiles = [ + path.resolve(__dirname, '../webpack/global_test_setup.js'), + ]; + const testSetupPath = path.join(pluginPath, 'webpack', 'test_setup.js'); + if (fs.existsSync(testSetupPath)) { + testSetupFiles.unshift(testSetupPath); + } + const pluginConfigPath = path.join(pluginPath, 'jest.config.js'); + const combinedConfigPath = path.join( + pluginPath, + 'combined.jest.config.js' + ); + + if (fs.existsSync(pluginConfigPath)) { + // eslint-disable-next-line global-require + const pluginConfig = require(pluginConfigPath); + function customizer(objValue, srcValue) { + if (lodash.isArray(objValue)) { + return lodash.uniq(objValue.concat(srcValue)); + } + } + + const combinedConfig = lodash.mergeWith( + pluginConfig, + { + ...coreConfig, + setupFilesAfterEnv: [ + path.resolve(__dirname, '../webpack/global_test_setup.js'), + ], + }, + customizer + ); + combinedConfig.snapshotSerializers = coreConfig.snapshotSerializers; + fs.writeFileSync( + combinedConfigPath, + `module.exports = ${JSON.stringify(combinedConfig, null, 2)};`, + 'utf8' + ); + } + const pluginConfigOverride = fs.existsSync(pluginConfigPath); + const configPath = pluginConfigOverride + ? combinedConfigPath + : coreConfigPath; + const corePath = path.resolve(__dirname, '../'); + const args = [ + 'jest', + `${pluginPath}/webpack`, + '--roots', + pluginPath, + corePath, + `--config=${configPath}`, + pluginConfigOverride + ? '' + : `--setupFilesAfterEnv ${testSetupFiles.join(' ')}`, + '--color', + ...passedArgs, + ]; + + await runChildProcess(args, pluginPath); // Run every plugin test in a separate process + if(fs.existsSync(combinedConfigPath)) { + fs.unlinkSync(combinedConfigPath); + } + } + } +}; + +runTests(); diff --git a/webpack/assets/javascripts/react_app/components/TemplateGenerator/__tests__/TemplateGeneratorActions.test.js b/webpack/assets/javascripts/react_app/components/TemplateGenerator/__tests__/TemplateGeneratorActions.test.js index a3554f42fdb..05e8fba8f40 100644 --- a/webpack/assets/javascripts/react_app/components/TemplateGenerator/__tests__/TemplateGeneratorActions.test.js +++ b/webpack/assets/javascripts/react_app/components/TemplateGenerator/__tests__/TemplateGeneratorActions.test.js @@ -21,15 +21,16 @@ import * as actions from '../TemplateGeneratorActions'; jest.mock('file-saver'); jest.mock('../../../redux/API'); -beforeEach(() => { - API.post.mockImplementation(async () => scheduleResponse); - API.get.mockImplementation(async () => noContentResponse); -}); - describe('TemplateGeneratorActions', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); beforeEach(() => { API.post.mockClear(); API.get.mockClear(); + + API.post.mockImplementation(async () => scheduleResponse); + API.get.mockImplementation(async () => noContentResponse); }); describe('generateTemplate', () => { @@ -55,7 +56,6 @@ describe('TemplateGeneratorActions', () => { API.get .mockImplementationOnce(async () => noContentResponse) .mockImplementationOnce(async () => generatedReportResponse); - runActionInDepth(() => actions.generateTemplate(), 3).then(callTree => { const successAction = callTree[1][1][1]; expect(successAction).toHaveProperty('type', TEMPLATE_GENERATE_SUCCESS); diff --git a/webpack/test_setup.js b/webpack/core_test_setup.js similarity index 73% rename from webpack/test_setup.js rename to webpack/core_test_setup.js index c7158276891..d22b70ab381 100644 --- a/webpack/test_setup.js +++ b/webpack/core_test_setup.js @@ -6,13 +6,20 @@ ace.config.set('themePath', ''); jest.mock('jed'); jest.mock('./assets/javascripts/react_app/Root/Context/ForemanContext', () => ({ + getForemanContext: () => ({ + context: { metadata: { version: 'mocked_version' } }, + }), + useForemanContext: () => ({ metadata: { version: 'mocked_version' } }), + useForemanSetContext: () => {}, useForemanVersion: () => 'mocked_version', useForemanSettings: () => ({ perPage: 5 }), useForemanDocUrl: () => '/url', useForemanLocation: () => ({ title: 'location' }), useForemanOrganization: () => ({ title: 'organization' }), + useForemanUser: () => ({ login: 'user' }), getHostsPageUrl: displayNewHostsPage => displayNewHostsPage ? '/new/hosts' : '/hosts', + useForemanHostsPageUrl: () => '/hosts', })); jest.mock('./assets/javascripts/react_app/common/I18n'); jest.mock('./assets/javascripts/foreman_tools', () => ({ diff --git a/webpack/global_test_setup.js b/webpack/global_test_setup.js new file mode 100644 index 00000000000..f7fdd9bf82a --- /dev/null +++ b/webpack/global_test_setup.js @@ -0,0 +1,19 @@ +// eslint-disable-next-line import/no-unresolved, import/extensions +import 'core-js/shim'; +// eslint-disable-next-line import/no-extraneous-dependencies +import 'regenerator-runtime/runtime'; + +const { configure } = require('./theforeman-test'); +const Adapter = require('enzyme-adapter-react-16'); + +configure({ adapter: new Adapter() }); + +// https://github.com/facebook/jest/issues/6121 +// eslint-disable-next-line no-console +const { error } = console; +// eslint-disable-next-line no-console +console.error = (message, ...args) => { + error.apply(console, args); // keep default behaviour + const err = message instanceof Error ? message : new Error(message); + throw err; +}; diff --git a/webpack/jest.config.js b/webpack/jest.config.js new file mode 100644 index 00000000000..67f8d059fe2 --- /dev/null +++ b/webpack/jest.config.js @@ -0,0 +1,71 @@ +/* eslint-disable spellcheck/spell-checker */ +const fs = require('fs'); +const path = require('path'); + +const nodeModules = path.resolve(__dirname, '..', 'node_modules'); +const packageJsonPath = path.resolve(__dirname, '..', 'package.json'); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); +const vendorCorePackageJsonPath = path.resolve(nodeModules, '@theforeman/vendor-core', 'package.json'); +const vendorCorePackageJson = JSON.parse(fs.readFileSync(vendorCorePackageJsonPath, 'utf8')); +const dependencies = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + ...vendorCorePackageJson.dependencies, + '@apollo/client/testing': '@apollo/client/testing', +}; // Use shared dependencies from foreman node_modules and not plugin node_modules to avoid jest errors due to multiple instances of same package + +const moduleNameMapper = {}; +Object.keys(dependencies).forEach(dep => { + moduleNameMapper[`^${dep}$`] = path.resolve(nodeModules, dep); +}); + +const foremanReactFull = path.resolve( + __dirname, + 'assets/javascripts/react_app' +); +const foremanTest = path.resolve(__dirname, 'theforeman-test.js'); + +module.exports = { + verbose: true, + logHeapUsage: true, + maxWorkers: 2, + collectCoverage: true, + coverageReporters: ['lcov'], + coverageDirectory: `../coverage`, + setupFiles: [require.resolve('jest-prop-type-error')], + testPathIgnorePatterns: [ + '/node_modules/', + '/foreman/', + '/.+fixtures.+', + 'foreman/webpack', // dont test foreman core in plugins + ], + testMatch: ['**/*.test.js'], + moduleDirectories: [ + `node_modules`, + `/node_modules/@theforeman/vendor-core/node_modules`, + `node_modules/@theforeman/vendor-core/node_modules`, + '/node_modules', + ], + transform: { + '^.+\\.js?$': 'babel-jest', + '\\.(gql|graphql)$': require.resolve('jest-transform-graphql'), // for graphql-tag + }, + snapshotSerializers: [require.resolve('enzyme-to-json/serializer')], + moduleNameMapper: { + '^.+\\.(png|gif|css|scss)$': 'identity-obj-proxy', + ...moduleNameMapper, + '^dnd-core$': `${nodeModules}/dnd-core/dist/cjs`, + '^react-dnd$': `${nodeModules}/react-dnd/dist/cjs`, + '^react-dnd-html5-backend$': `${nodeModules}/react-dnd-html5-backend/dist/cjs`, + '^react-dnd-touch-backend$': `${nodeModules}/react-dnd-touch-backend/dist/cjs`, + '^react-dnd-test-backend$': `${nodeModules}/react-dnd-test-backend/dist/cjs`, + '^react-dnd-test-utils$': `${nodeModules}/react-dnd-test-utils/dist/cjs`, + '^foremanReact(.*)$': `${foremanReactFull}/$1`, + '^@theforeman/test$': foremanTest, + '^victory(.*)$': `${nodeModules}/victory$1`, + }, + globals: { + __testing__: true, + URL_PREFIX: '', + }, +}; diff --git a/webpack/theforeman-test.js b/webpack/theforeman-test.js new file mode 100644 index 00000000000..eb747effd53 --- /dev/null +++ b/webpack/theforeman-test.js @@ -0,0 +1,36 @@ +// replaces @theforeman/test.js +import { shallow, mount, render, configure } from 'enzyme'; +import MockAdapter from 'axios-mock-adapter'; + +import { + mockWindowLocation, + classFunctionUnitTest, + shallowRenderComponentWithFixtures, + testComponentSnapshotsWithFixtures, + runActionInDepth, + testActionSnapshot, + testActionSnapshotWithFixtures, + testReducerSnapshotWithFixtures, + testSelectorsSnapshotWithFixtures, + initMockStore, +} from './assets/javascripts/react_app/common/testHelpers'; +import IntegrationTestHelper from './assets/javascripts/react_app/common/IntegrationTestHelper'; + +export { + mockWindowLocation, + classFunctionUnitTest, + shallowRenderComponentWithFixtures, + testComponentSnapshotsWithFixtures, + runActionInDepth, + testActionSnapshot, + testActionSnapshotWithFixtures, + testReducerSnapshotWithFixtures, + testSelectorsSnapshotWithFixtures, + initMockStore, + IntegrationTestHelper, + shallow, + mount, + render, + configure, + MockAdapter, +};