Skip to content

Commit

Permalink
Jest support (#351)
Browse files Browse the repository at this point in the history
* Adds WIP core and tests for jest support

* Adds functional way to generalize all test cases and file names

* Fixes eslint errors and adds support for 'transform' config objects

* Adds support for option values types 'string' and 'array'

* Adds support for removing 'node_modules' from [moduleName, options] type

* Reworks 'testJest()' to ignore array order of dependencies

* Adds more tests for modules with options or referenced files

* Removes unnecessary conditionals for technically invalid jest configs

* Adds test to get code coverage to 100%

* Adds entry for jest to 'Special' section in README

* Fixes linting error in test/special/jest

* 0.9.0

* Revert "0.9.0"

This reverts commit 849a844.
  • Loading branch information
GarrettGeorge authored and rumpl committed May 25, 2019
1 parent eb69af9 commit 13bd6e8
Show file tree
Hide file tree
Showing 3 changed files with 360 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ The *special* component is used to recognize the dependencies that are not gener
- [Grunt](https://www.npmjs.com/package/grunt) plugins
- `feross-standard` - [Feross standard](https://www.npmjs.com/package/standard) format parser
- `mocha` - [Mocha](https://www.npmjs.com/package/mocha) explicit required dependencies
- `jest` - [Jest](https://www.npmjs.com/package/jest) properties in [Jest Configuration](https://jestjs.io/docs/en/configuration)
- `commitizen` - [Commitizen](https://www.npmjs.com/package/commitizen) configuration adaptor
- `gulp-load-plugins` - [Gulp-load-plugins](https://www.npmjs.com/package/gulp-load-plugins) lazy loaded plugins
- `gatsby` - [Gatsby](https://www.npmjs.com/package/gatsby) configuration parser
Expand Down
116 changes: 116 additions & 0 deletions src/special/jest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import path from 'path';
import lodash from 'lodash';

const _ = lodash;

const jestConfigRegex = /jest.conf(ig|).js(on|)$/;
const supportedProperties = [
'dependencyExtractor',
'preset',
'prettierPath',
'reporters',
'runner',
'setupFiles',
'setupFilesAfterEnv',
'snapshotResolver',
'snapshotSerializers',
'testEnvironment',
'testResultsProcessor',
'testRunner',
'transform',
'watchPlugins',
];

function parse(content) {
try {
return JSON.parse(content);
} catch (error) {
return {}; // ignore parse error silently
}
}

function contain(array, dep, prefix) {
if (!array) {
return false;
}

if (typeof array === 'string') {
return contain([array], dep, prefix);
}

// extract name if wrapping with options
const names = array.map(item => (lodash.isString(item) ? item : item[0]));
if (names.indexOf(dep) !== -1) {
return true;
}

if (prefix && dep.indexOf(prefix) === 0) {
return contain(array, dep.substring(prefix.length), false);
}

return false;
}

function removeNodeModuleRelativePaths(filepath) {
if (Array.isArray(filepath)) {
return removeNodeModuleRelativePaths(filepath[0]);
}
return filepath.replace(/^.*node_modules\//, '').replace(/\/.*/, '');
}

function filter(deps, options) {
const runner = deps.filter(dep => (
contain(options.runner, dep, 'jest-runner-')
));

const watchPlugins = deps.filter(dep => (
contain(options.watchPlugins, dep, 'jest-watch-')
));

const otherProps = lodash(options)
.entries()
.map(([prop, value]) => {
if (prop === 'transform') {
return _.values(value).map(removeNodeModuleRelativePaths);
}
if (Array.isArray(value)) {
return value.map(removeNodeModuleRelativePaths);
}
return removeNodeModuleRelativePaths(value);
})
.flatten()
.intersection(deps)
.value();

return _.uniq(runner.concat(watchPlugins).concat(otherProps));
}

function checkOptions(deps, options = {}) {
const pickedOptions = lodash(options)
.pick(supportedProperties)
.value();
return filter(deps, pickedOptions);
}

export default function parseJest(content, filePath, deps, rootDir) {
const filename = path.basename(filePath);
if (jestConfigRegex.test(filename)) {
try {
// eslint-disable-next-line global-require
const options = require(filePath) || {};
return checkOptions(deps, options);
} catch (error) {
return [];
}
}

const packageJsonPath = path.resolve(rootDir, 'package.json');
const resolvedFilePath = path.resolve(rootDir, filename);

if (resolvedFilePath === packageJsonPath) {
const metadata = parse(content);
return checkOptions(deps, metadata.jest);
}

return [];
}
243 changes: 243 additions & 0 deletions test/special/jest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/* global describe, it */

import 'should';
import path from 'path';
import fse from 'fs-extra';
import jestSpecialParser from '../../src/special/jest';

const configFileNames = [
'jest.config.js',
'jest.conf.js',
'jest.config.json',
'jest.conf.json',
];

const testCases = [
{
name: 'ignore when the config is the empty object',
deps: [],
content: {},
},
{
name: 'recognize single short-name jest-runner',
deps: ['jest-runner-mocha'],
content: { runner: 'mocha' },
},
{
name: 'recognize single long-name jest-runner',
deps: ['jest-runner-mocha'],
content: { runner: 'jest-runner-mocha' },
},
{
name: 'recognize single short-name jest-watch plugin',
deps: ['jest-watch-master'],
content: { watchPlugins: ['master'] },
},
{
name: 'recognize single long-name jest-watch plugin',
deps: ['jest-watch-master'],
content: { watchPlugins: ['jest-watch-master'] },
},
{
name: 'recognize multiple short-name jest-watch plugin',
deps: ['jest-watch-master', 'jest-watch-select-projects'],
content: { watchPlugins: ['master', 'select-projects'] },
},
{
name: 'recognize module with options',
deps: ['jest-watch-master'],
content: {
watchPlugins: [
[
'master',
{
key: 'k',
prompt: 'show a custom prompt',
},
],
],
},
},
{
name: 'recognize transform path with node_modules',
deps: ['babel-jest'],
content: {
transform: {
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
},
},
},
{
name: 'recognize duplicated transformer',
deps: ['babel-jest'],
content: {
transform: {
'^.+\\.js?$': 'babel-jest',
'^.+\\.jsx?$': 'babel-jest',
},
},
},
{
name: 'recognize module when preset is referenced',
deps: ['foo-bar'],
content: {
preset: './node_modules/foo-bar/jest-preset.js',
},
},
{
name: 'recognize reporter when defined with options',
deps: ['jest-custom-reporter', 'jest-reporter'],
content: {
reporters: [
[
'jest-custom-reporter',
{ foo: 'bar' },
],
[
'<rootDir>/node_modules/jest-reporter',
{ jest: 'reporter' },
],
],
},
},
{
name: 'recognize array of strings of modules',
deps: ['foo', 'bar', 'jest', 'babel-jest'],
content: {
setupFiles: [
'<rootDir>/node_modules/foo',
'../node_modules/bar',
'jest',
'./node_modules/babel-jest/custom-setup.js',
],
},
},
{
name: 'recognize multiple options',
deps: ['babel-jest', 'vue-jest', 'jest-serializer-vue'],
content: {
transform: {
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
'^.+\\.vue$': 'vue-jest',
},
snapshotSerializers: ['jest-serializer-vue'],
},
},
];

function random() {
return Math.random().toString().substring(2);
}

async function getTempPath(filename, content) {
const tempFolder = path.resolve(__dirname, `tmp-${random()}`);
const tempPath = path.resolve(tempFolder, filename);
await fse.ensureDir(tempFolder);
await fse.outputFile(tempPath, content);
return tempPath;
}

async function removeTempFile(filepath) {
const fileFolder = path.dirname(filepath);
await fse.remove(filepath);
await fse.remove(fileFolder);
}

async function testJest(content, deps, expectedDeps, filename) {
const tempPath = await getTempPath(
(filename || configFileNames[0]),
content,
);
try {
const result = jestSpecialParser(content, tempPath, deps, __dirname);
// sort() allows us to ignore order
Array.from(result).sort().should.deepEqual(expectedDeps.sort());
} finally {
await removeTempFile(tempPath);
}
}

describe('jest special parser', () => {
it('should ignore when filename is not supported', () => {
const result = jestSpecialParser('content', 'jest.js', [], __dirname);
result.should.deepEqual([]);
});

it('should handle JSON parse error', () => {
const content = '{ this is an invalid JSON string';
return testJest(content, [], []);
});

it('should handle parse error for valid JS but invalid JSON', () => {
const content = 'module.exports = function() {}';
return testJest(content, [], []);
});

it('should ignore unsupported config properties', () => {
const content = `module.exports = ${{ unsupported: 'npm-module' }}`;
return testJest(content, [], []);
});

it('should recognize unused dependencies in jest config', () => {
const config = JSON.stringify(testCases[1].content);
const content = `module.exports = ${config}`;
const deps = testCases[1].deps.concat(['unused-module']);
return testJest(content, deps, testCases[1].deps);
});

it('should handle require call to other modules', () => {
const config = JSON.stringify(testCases[1].content);
const content = `const fs = require('fs');
module.exports = ${config}`;
return testJest(content, testCases[1].deps, testCases[1].deps);
});

it('should handle options which are not supported', () => {
const result = jestSpecialParser(
'module.exports = { automock: true }',
'jest.config.js',
[],
__dirname,
);
result.should.deepEqual([]);
});

it('should handle JSON parse error when using package.json', () => {
const content = '{ this is an invalid JSON string';
const result = jestSpecialParser(
content,
path.resolve(__dirname, 'package.json'),
[],
__dirname,
);
result.should.deepEqual([]);
});

it('should handle package.json config', () => {
const result = jestSpecialParser(
JSON.stringify({ jest: [...testCases].pop().content }),
path.resolve(__dirname, 'package.json'),
[...testCases].pop().deps,
__dirname,
);
result.sort().should.deepEqual([...testCases].pop().deps.sort());
});

it('should handle if module.exports evaluates to undefined', () => {
const content = 'module.exports = undefined';
return testJest(content, [], []);
});

configFileNames.forEach(fileName => (
testCases.forEach(testCase => (
it(`should ${testCase.name} in config file ${fileName}`, () => {
const config = JSON.stringify(testCase.content);
let content = config;
if (fileName.split('.').pop() === 'js') {
content = `module.exports = ${config}`;
}
return testJest(content, testCase.deps, testCase.deps, fileName);
})
))
));
});

0 comments on commit 13bd6e8

Please sign in to comment.