Skip to content

Commit

Permalink
feat: warn when there are multiple configs (#11922)
Browse files Browse the repository at this point in the history
  • Loading branch information
jankaifer committed Oct 6, 2021
1 parent 24f9bc8 commit cdc64c6
Show file tree
Hide file tree
Showing 19 changed files with 321 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

### Features

- `[jest-config]` Warn when multiple Jest configs are located ([#11922](https://github.com/facebook/jest/pull/11922))

### Fixes

- `[expect]` Pass matcher context to asymmetric matchers ([#11926](https://github.com/facebook/jest/pull/11926) & [#11930](https://github.com/facebook/jest/pull/11930))
Expand Down
9 changes: 6 additions & 3 deletions e2e/Utils.ts
Expand Up @@ -176,20 +176,23 @@ interface JestPackageJson extends PackageJson {
}

const DEFAULT_PACKAGE_JSON: JestPackageJson = {
description: 'THIS IS AN AUTOGENERATED FILE AND SHOULD NOT BE ADDED TO GIT',
jest: {
testEnvironment: 'node',
},
};

export const createEmptyPackage = (
directory: Config.Path,
packageJson = DEFAULT_PACKAGE_JSON,
packageJson: PackageJson = DEFAULT_PACKAGE_JSON,
) => {
const packageJsonWithDefaults = {
...packageJson,
description: 'THIS IS AN AUTOGENERATED FILE AND SHOULD NOT BE ADDED TO GIT',
};
fs.mkdirSync(directory, {recursive: true});
fs.writeFileSync(
path.resolve(directory, 'package.json'),
JSON.stringify(packageJson, null, 2),
JSON.stringify(packageJsonWithDefaults, null, 2),
);
};

Expand Down
25 changes: 25 additions & 0 deletions e2e/__tests__/__snapshots__/multipleConfigs.ts.snap
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`multiple configs will warn 1`] = `
● Multiple configurations found:
* <rootDir>/e2e/multiple-configs/jest.config.js
* <rootDir>/e2e/multiple-configs/jest.config.json
* \`jest\` key in <rootDir>/e2e/multiple-configs/package.json
Implicit config resolution does not allow multiple configuration files.
Either remove unused config files or select one explicitly with \`--config\`.
Configuration Documentation:
https://jestjs.io/docs/configuration.html
PASS Config from js file __tests__/test.js
✓ dummy test
`;
exports[`multiple configs will warn 2`] = `
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites.
`;
24 changes: 24 additions & 0 deletions e2e/__tests__/configOverride.test.ts
@@ -0,0 +1,24 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {getConfig} from '../runJest';

test('reads config from cjs file', () => {
const {configs} = getConfig(
'config-override',
['--config', 'different-config.json'],
{
skipPkgJsonCheck: true,
},
);

expect(configs).toHaveLength(1);
expect(configs[0].displayName).toEqual({
color: 'white',
name: 'Config from different-config.json file',
});
});
2 changes: 1 addition & 1 deletion e2e/__tests__/dependencyClash.test.ts
Expand Up @@ -18,7 +18,7 @@ const hasteImplModulePath = path

beforeEach(() => {
cleanup(tempDir);
createEmptyPackage(tempDir);
createEmptyPackage(tempDir, {});
});

// This test case is checking that when having both
Expand Down
43 changes: 43 additions & 0 deletions e2e/__tests__/multipleConfigs.ts
@@ -0,0 +1,43 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import * as path from 'path';
import {wrap} from 'jest-snapshot-serializer-raw';
import slash = require('slash');
import {extractSummary} from '../Utils';
import runJest from '../runJest';

const MULTIPLE_CONFIGS_WARNING_TEXT = 'Multiple configurations found';

test('multiple configs will warn', () => {
const rootDir = slash(path.resolve(__dirname, '../..'));
const {exitCode, stderr} = runJest('multiple-configs', [], {
skipPkgJsonCheck: true,
});

expect(exitCode).toBe(0);
expect(stderr).toContain(MULTIPLE_CONFIGS_WARNING_TEXT);

const cleanStdErr = stderr.replace(new RegExp(rootDir, 'g'), '<rootDir>');
const {rest, summary} = extractSummary(cleanStdErr);

expect(wrap(rest)).toMatchSnapshot();
expect(wrap(summary)).toMatchSnapshot();
});

test('multiple configs warning can be suppressed by using --config', () => {
const {exitCode, stderr} = runJest(
'multiple-configs',
['--config', 'jest.config.json'],
{
skipPkgJsonCheck: true,
},
);

expect(exitCode).toBe(0);
expect(stderr).not.toContain(MULTIPLE_CONFIGS_WARNING_TEXT);
});
10 changes: 10 additions & 0 deletions e2e/config-override/__tests__/test.js
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

test('dummy test', () => {
expect(1).toBe(1);
});
3 changes: 3 additions & 0 deletions e2e/config-override/different-config.json
@@ -0,0 +1,3 @@
{
"displayName": "Config from different-config.json file"
}
3 changes: 3 additions & 0 deletions e2e/config-override/jest.config.json
@@ -0,0 +1,3 @@
{
"displayName": "Config from json file"
}
3 changes: 3 additions & 0 deletions e2e/config-override/package.json
@@ -0,0 +1,3 @@
{
"name": "config-override"
}
2 changes: 1 addition & 1 deletion e2e/esm-config/cjs/package.json
@@ -1,3 +1,3 @@
{
"jest": {}
"name": "cjs-config"
}
2 changes: 1 addition & 1 deletion e2e/esm-config/mjs/package.json
@@ -1,3 +1,3 @@
{
"jest": {}
"name": "mjs-config"
}
10 changes: 10 additions & 0 deletions e2e/multiple-configs/__tests__/test.js
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

test('dummy test', () => {
expect(1).toBe(1);
});
10 changes: 10 additions & 0 deletions e2e/multiple-configs/jest.config.js
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

module.exports = {
displayName: 'Config from js file',
};
3 changes: 3 additions & 0 deletions e2e/multiple-configs/jest.config.json
@@ -0,0 +1,3 @@
{
"displayName": "Config from json file"
}
5 changes: 5 additions & 0 deletions e2e/multiple-configs/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"displayName": "Config from package.json file"
}
}
88 changes: 86 additions & 2 deletions packages/jest-config/src/__tests__/resolveConfigPath.test.ts
Expand Up @@ -14,6 +14,17 @@ import resolveConfigPath from '../resolveConfigPath';
const DIR = path.resolve(tmpdir(), 'resolve_config_path_test');
const ERROR_PATTERN = /Could not find a config file based on provided values/;
const NO_ROOT_DIR_ERROR_PATTERN = /Can't find a root directory/;
const MULTIPLE_CONFIGS_ERROR_PATTERN = /Multiple configurations found/;

const mockConsoleWarn = () => {
jest.spyOn(console, 'warn');
const mockedConsoleWarn = console.warn as jest.Mock<void, Array<any>>;

// We will mock console.warn because it would produce a lot of noise in the tests
mockedConsoleWarn.mockImplementation(() => {});

return mockedConsoleWarn;
};

beforeEach(() => cleanup(DIR));
afterEach(() => cleanup(DIR));
Expand Down Expand Up @@ -45,6 +56,8 @@ describe.each(JEST_CONFIG_EXT_ORDER.slice(0))(
});

test(`directory path with "${extension}"`, () => {
const mockedConsoleWarn = mockConsoleWarn();

const relativePackageJsonPath = 'a/b/c/package.json';
const absolutePackageJsonPath = path.resolve(
DIR,
Expand All @@ -53,9 +66,9 @@ describe.each(JEST_CONFIG_EXT_ORDER.slice(0))(
const relativeJestConfigPath = `a/b/c/jest.config${extension}`;
const absoluteJestConfigPath = path.resolve(DIR, relativeJestConfigPath);

// no configs yet. should throw
writeFiles(DIR, {[`a/b/c/some_random_file${extension}`]: ''});

// no configs yet. should throw
expect(() =>
// absolute
resolveConfigPath(path.dirname(absoluteJestConfigPath), DIR),
Expand All @@ -68,6 +81,7 @@ describe.each(JEST_CONFIG_EXT_ORDER.slice(0))(

writeFiles(DIR, {[relativePackageJsonPath]: ''});

mockedConsoleWarn.mockClear();
// absolute
expect(
resolveConfigPath(path.dirname(absolutePackageJsonPath), DIR),
Expand All @@ -77,20 +91,45 @@ describe.each(JEST_CONFIG_EXT_ORDER.slice(0))(
expect(
resolveConfigPath(path.dirname(relativePackageJsonPath), DIR),
).toBe(absolutePackageJsonPath);
expect(mockedConsoleWarn).not.toBeCalled();

// jest.config.js takes precedence
writeFiles(DIR, {[relativeJestConfigPath]: ''});

// jest.config.js takes precedence
mockedConsoleWarn.mockClear();
// absolute
expect(
resolveConfigPath(path.dirname(absolutePackageJsonPath), DIR),
).toBe(absoluteJestConfigPath);

// relative
expect(
resolveConfigPath(path.dirname(relativePackageJsonPath), DIR),
).toBe(absoluteJestConfigPath);
expect(mockedConsoleWarn).not.toBeCalled();

// jest.config.js and package.json with 'jest' cannot be used together
writeFiles(DIR, {[relativePackageJsonPath]: JSON.stringify({jest: {}})});

// absolute
mockedConsoleWarn.mockClear();
expect(
resolveConfigPath(path.dirname(absolutePackageJsonPath), DIR),
).toBe(absoluteJestConfigPath);
expect(mockedConsoleWarn).toBeCalledTimes(1);
expect(mockedConsoleWarn.mock.calls[0].join()).toMatch(
MULTIPLE_CONFIGS_ERROR_PATTERN,
);

// relative
mockedConsoleWarn.mockClear();
expect(
resolveConfigPath(path.dirname(relativePackageJsonPath), DIR),
).toBe(absoluteJestConfigPath);
expect(mockedConsoleWarn).toBeCalledTimes(1);
expect(mockedConsoleWarn.mock.calls[0].join()).toMatch(
MULTIPLE_CONFIGS_ERROR_PATTERN,
);

expect(() => {
resolveConfigPath(
Expand All @@ -101,3 +140,48 @@ describe.each(JEST_CONFIG_EXT_ORDER.slice(0))(
});
},
);

const pickPairsWithSameOrder = <T>(array: ReadonlyArray<T>) =>
array
.map((value1, idx, arr) =>
arr.slice(idx + 1).map(value2 => [value1, value2]),
)
// TODO: use .flat() when we drop Node 10
.reduce((acc, val) => acc.concat(val), []);

test('pickPairsWithSameOrder', () => {
expect(pickPairsWithSameOrder([1, 2, 3])).toStrictEqual([
[1, 2],
[1, 3],
[2, 3],
]);
});

describe.each(pickPairsWithSameOrder(JEST_CONFIG_EXT_ORDER))(
'Using multiple configs shows warning',
(extension1, extension2) => {
test(`Using jest.config${extension1} and jest.config${extension2} shows warning`, () => {
const mockedConsoleWarn = mockConsoleWarn();

const relativeJestConfigPaths = [
`a/b/c/jest.config${extension1}`,
`a/b/c/jest.config${extension2}`,
];

writeFiles(DIR, {
[relativeJestConfigPaths[0]]: '',
[relativeJestConfigPaths[1]]: '',
});

// multiple configs here, should print warning
mockedConsoleWarn.mockClear();
expect(
resolveConfigPath(path.dirname(relativeJestConfigPaths[0]), DIR),
).toBe(path.resolve(DIR, relativeJestConfigPaths[0]));
expect(mockedConsoleWarn).toBeCalledTimes(1);
expect(mockedConsoleWarn.mock.calls[0].join()).toMatch(
MULTIPLE_CONFIGS_ERROR_PATTERN,
);
});
},
);
15 changes: 13 additions & 2 deletions packages/jest-config/src/index.ts
Expand Up @@ -44,6 +44,7 @@ export async function readConfig(
skipArgvConfigOption?: boolean,
parentConfigDirname?: Config.Path | null,
projectIndex: number = Infinity,
skipMultipleConfigWarning = false,
): Promise<ReadConfig> {
let rawOptions: Config.InitialOptions;
let configPath = null;
Expand Down Expand Up @@ -77,11 +78,19 @@ export async function readConfig(
// A string passed to `--config`, which is either a direct path to the config
// or a path to directory containing `package.json`, `jest.config.js` or `jest.config.ts`
} else if (!skipArgvConfigOption && typeof argv.config == 'string') {
configPath = resolveConfigPath(argv.config, process.cwd());
configPath = resolveConfigPath(
argv.config,
process.cwd(),
skipMultipleConfigWarning,
);
rawOptions = await readConfigFileAndSetRootDir(configPath);
} else {
// Otherwise just try to find config in the current rootDir.
configPath = resolveConfigPath(packageRootOrConfig, process.cwd());
configPath = resolveConfigPath(
packageRootOrConfig,
process.cwd(),
skipMultipleConfigWarning,
);
rawOptions = await readConfigFileAndSetRootDir(configPath);
}

Expand Down Expand Up @@ -332,6 +341,8 @@ export async function readConfigs(
skipArgvConfigOption,
configPath ? path.dirname(configPath) : cwd,
projectIndex,
// we wanna skip the warning if this is the "main" project
projectIsCwd,
);
}),
);
Expand Down

0 comments on commit cdc64c6

Please sign in to comment.