diff --git a/README.md b/README.md index a6134f71f1..eeefdb3cd9 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,16 @@ If you want to run all tests and exit, type: yarn test-once ``` +### Eslint + +As you run tests you will see a report of Eslint errors at the end of the test output: + + yarn test + +If you would like to run tests without Eslint checks, set an environment variable: + + NO_ESLINT=1 yarn test + ### Flow There is limited support for using [Flow](https://flowtype.org/) to validate the intention of our program. diff --git a/jest.config.js b/jest.config.js index eae3f8769e..4375603fe0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,6 +21,7 @@ module.exports = { '/tests/jest-reporters/fingers-crossed.js', '/tests/jest-reporters/summary.js', '/tests/jest-reporters/flow-check.js', + '/tests/jest-reporters/eslint-check.js', ], setupTestFrameworkScriptFile: '/tests/setup.js', testPathIgnorePatterns: [ diff --git a/package.json b/package.json index dda128a7da..663b2a8314 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,8 @@ "NODE_ICU_DATA": "./node_modules/full-icu", "NODE_PATH": "./:./src", "NODE_ENV": "test", - "NO_FLOW": "1" + "NO_FLOW": "1", + "NO_ESLINT": "1" } }, "webpack-dev-server": { diff --git a/tests/jest-reporters/eslint-check.js b/tests/jest-reporters/eslint-check.js new file mode 100644 index 0000000000..c2947980ff --- /dev/null +++ b/tests/jest-reporters/eslint-check.js @@ -0,0 +1,58 @@ +/* eslint-disable no-console */ +const { CLIEngine } = require('eslint'); + +const { getChangedFiles } = require('./utils'); + +const NO_ESLINT_ENV_VAR = 'NO_ESLINT'; + +class EslintCheckReporter { + constructor() { + this.eslint = new CLIEngine(); + this.eslintOutput = null; + } + + isDisabled() { + return process.env[NO_ESLINT_ENV_VAR] === '1'; + } + + async onRunStart() { + if (this.isDisabled()) { + return; + } + + const files = await getChangedFiles(); + + if (!files) { + throw new Error(`Failed to retrieve files in the eslint check reporter.`); + } + + const report = this.eslint.executeOnFiles(files); + + if (report.errorCount === 0 && report.warningCount === 0) { + // All good. + this.eslintOutput = null; + } else { + this.eslintOutput = CLIEngine.getFormatter()(report.results); + } + } + + getLastError() { + if (this.isDisabled()) { + return undefined; + } + + console.log(''); + if (this.eslintOutput) { + console.log(this.eslintOutput); + console.log( + `Set ${NO_ESLINT_ENV_VAR}=1 in the environment to disable eslint checks`, + ); + return new Error('eslint errors'); + } + console.log('Eslint: no errors 💄 ✨'); + + return undefined; + } +} + +module.exports = EslintCheckReporter; diff --git a/tests/jest-reporters/utils.js b/tests/jest-reporters/utils.js new file mode 100644 index 0000000000..587385b3c6 --- /dev/null +++ b/tests/jest-reporters/utils.js @@ -0,0 +1,45 @@ +const util = require('util'); +const exec = util.promisify(require('child_process').exec); + +const filterFileNamesFromGitStatusOutput = (output) => { + const files = output + .split('\n') + .map((line) => line.trim()) + // Make sure we ignore deleted files. + .filter((line) => !line.startsWith('D')) + .map((line) => { + // Return the new name of a renamed file. + if (line.startsWith('RM')) { + return line.substring(line.indexOf('->')); + } + + return line; + }) + .map((line) => line.split(' ')) + // .flatMap() + .reduce((chunks, chunk) => chunks.concat(chunk), []) + // We assume no filename can be smaller than 3 to filter the short format + // statuses: https://git-scm.com/docs/git-status#_short_format. + .filter((chunk) => chunk.length > 3) + // Allow directories OR JS/JSX files + .filter((filename) => /(\/|\.jsx?)$/.test(filename)); + + return files; +}; + +const getChangedFiles = async () => { + // We use the Porcelain Format Version 1, + // See: https://git-scm.com/docs/git-status#_porcelain_format_version_1 + const { stdout, stderr } = await exec('git status --porcelain=1'); + + if (stderr) { + return null; + } + + return filterFileNamesFromGitStatusOutput(stdout); +}; + +module.exports = { + filterFileNamesFromGitStatusOutput, + getChangedFiles, +}; diff --git a/tests/unit/jest-reporters/test_utils.js b/tests/unit/jest-reporters/test_utils.js new file mode 100644 index 0000000000..95bed50be3 --- /dev/null +++ b/tests/unit/jest-reporters/test_utils.js @@ -0,0 +1,56 @@ +import { filterFileNamesFromGitStatusOutput } from 'tests/jest-reporters/utils'; + +describe(__filename, () => { + describe('_filterFileNamesFromGitStatusOutput', () => { + const _filterFileNamesFromGitStatusOutput = (lines = []) => { + return filterFileNamesFromGitStatusOutput(lines.join('\n')); + }; + + it('does not return deleted files', () => { + const files = _filterFileNamesFromGitStatusOutput([ + 'D src/core/browserWindow.js', + ' D src/core/browserWindow.js', + ]); + + expect(files).toHaveLength(0); + }); + + it('only returned the new filename of a renamed file', () => { + const files = _filterFileNamesFromGitStatusOutput([ + 'RM src/amo/sagas/categories.js -> src/core/sagas/categories.js', + ]); + + expect(files).toEqual(['src/core/sagas/categories.js']); + }); + + it('returns the filenames without the git statuses', () => { + const files = _filterFileNamesFromGitStatusOutput([ + 'M jest.config.js', + 'A tests/jest-reporters/eslint-check.js', + 'AM tests/jest-reporters/utils.js', + '?? tests/unit/jest-reporters/', + ]); + + expect(files).toEqual([ + 'jest.config.js', + 'tests/jest-reporters/eslint-check.js', + 'tests/jest-reporters/utils.js', + 'tests/unit/jest-reporters/', + ]); + }); + + it('only returns JavaScript files', () => { + const files = _filterFileNamesFromGitStatusOutput([ + 'M README.md', + 'M package.json', + 'M src/test.jsx', + ' M tests/unit/jest-reporters/test_utils.js', + ]); + + expect(files).toEqual([ + 'src/test.jsx', + 'tests/unit/jest-reporters/test_utils.js', + ]); + }); + }); +});