Skip to content

Commit

Permalink
feat: Add support for the jest.config.ts configuration file (#10564)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gamote committed Oct 12, 2020
1 parent d63f18a commit 591d612
Show file tree
Hide file tree
Showing 20 changed files with 420 additions and 31 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

### Features

- `[jest-cli, jest-config]` Add support for the `jest.config.ts` configuration file ([#10564](https://github.com/facebook/jest/pull/10564))

### Fixes

### Chore & Maintenance
Expand Down
26 changes: 23 additions & 3 deletions docs/Configuration.md
Expand Up @@ -3,7 +3,7 @@ id: configuration
title: Configuring Jest
---

Jest's configuration can be defined in the `package.json` file of your project, or through a `jest.config.js` file or through the `--config <path/to/file.js|cjs|mjs|json>` option. If you'd like to use your `package.json` to store Jest's config, the `"jest"` key should be used on the top level so Jest will know how to find your settings:
Jest's configuration can be defined in the `package.json` file of your project, or through a `jest.config.js`, or `jest.config.ts` file or through the `--config <path/to/file.js|ts|cjs|mjs|json>` option. If you'd like to use your `package.json` to store Jest's config, the `"jest"` key should be used on the top level so Jest will know how to find your settings:

```json
{
Expand All @@ -18,19 +18,39 @@ Or through JavaScript:

```js
// jest.config.js
//Sync object
// Sync object
module.exports = {
verbose: true,
};

//Or async function
// Or async function
module.exports = async () => {
return {
verbose: true,
};
};
```

Or through TypeScript (if `ts-node` is installed):

```ts
// jest.config.ts
import type {Config} from '@jest/types';

// Sync object
const config: Config.InitialOptions = {
verbose: true,
};
export default config;

// Or async function
export default async (): Promise<Config.InitialOptions> => {
return {
verbose: true,
};
};
```

Please keep in mind that the resulting configuration must be JSON-serializable.

When using the `--config` option, the JSON file must not contain a "jest" key:
Expand Down
36 changes: 36 additions & 0 deletions e2e/__tests__/__snapshots__/jest.config.ts.test.ts.snap
@@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`traverses directory tree up until it finds jest.config 1`] = `
console.log
<<REPLACED>>/jest.config.ts/some/nested/directory
at Object.log (__tests__/a-giraffe.js:3:27)
`;
exports[`traverses directory tree up until it finds jest.config 2`] = `
PASS ../../../__tests__/a-giraffe.js
✓ giraffe
✓ abc
`;
exports[`traverses directory tree up until it finds jest.config 3`] = `
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites.
`;
exports[`works with jest.config.ts 1`] = `
PASS __tests__/a-giraffe.js
✓ giraffe
`;
exports[`works with jest.config.ts 2`] = `
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites.
`;
83 changes: 83 additions & 0 deletions e2e/__tests__/jest.config.ts.test.ts
@@ -0,0 +1,83 @@
/**
* 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 runJest from '../runJest';
import {cleanup, extractSummary, writeFiles} from '../Utils';

const DIR = path.resolve(__dirname, '../jest.config.ts');

beforeEach(() => cleanup(DIR));
afterAll(() => cleanup(DIR));

test('works with jest.config.ts', () => {
writeFiles(DIR, {
'__tests__/a-giraffe.js': `test('giraffe', () => expect(1).toBe(1));`,
'jest.config.ts': `export default {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js'};`,
'package.json': '{}',
});

const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']);
const {rest, summary} = extractSummary(stderr);
expect(exitCode).toBe(0);
expect(wrap(rest)).toMatchSnapshot();
expect(wrap(summary)).toMatchSnapshot();
});

test('traverses directory tree up until it finds jest.config', () => {
writeFiles(DIR, {
'__tests__/a-giraffe.js': `
const slash = require('slash');
test('giraffe', () => expect(1).toBe(1));
test('abc', () => console.log(slash(process.cwd())));
`,
'jest.config.ts': `export default {testEnvironment: 'jest-environment-node', testRegex: '.*-giraffe.js'};`,
'package.json': '{}',
'some/nested/directory/file.js': '// nothing special',
});

const {stderr, exitCode, stdout} = runJest(
path.join(DIR, 'some', 'nested', 'directory'),
['-w=1', '--ci=false'],
{skipPkgJsonCheck: true},
);

// Snapshot the console.loged `process.cwd()` and make sure it stays the same
expect(
wrap(stdout.replace(/^\W+(.*)e2e/gm, '<<REPLACED>>')),
).toMatchSnapshot();

const {rest, summary} = extractSummary(stderr);
expect(exitCode).toBe(0);
expect(wrap(rest)).toMatchSnapshot();
expect(wrap(summary)).toMatchSnapshot();
});

test('it does type check the config', () => {
writeFiles(DIR, {
'__tests__/a-giraffe.js': `test('giraffe', () => expect(1).toBe(1));`,
'jest.config.ts': `export default { testTimeout: "10000" }`,
'package.json': '{}',
});

const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']);
expect(stderr).toMatch('must be of type');
expect(exitCode).toBe(1);
});

test('invalid JS in jest.config.ts', () => {
writeFiles(DIR, {
'__tests__/a-giraffe.js': `test('giraffe', () => expect(1).toBe(1));`,
'jest.config.ts': `export default i'll break this file yo`,
'package.json': '{}',
});

const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']);
expect(stderr).toMatch('TSError: ⨯ Unable to compile TypeScript:');
expect(exitCode).toBe(1);
});
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -77,6 +77,7 @@
"strip-ansi": "^6.0.0",
"tempy": "^0.6.0",
"throat": "^5.0.0",
"ts-node": "^9.0.0",
"type-fest": "^0.16.0",
"typescript": "^4.0.2",
"which": "^2.0.1"
Expand Down
4 changes: 2 additions & 2 deletions packages/jest-cli/src/__tests__/cli/args.test.ts
Expand Up @@ -82,13 +82,13 @@ describe('check', () => {
it('raises an exception if config is not a valid JSON string', () => {
const argv = {config: 'x:1'} as Config.Argv;
expect(() => check(argv)).toThrow(
'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .mjs, .cjs, .json',
'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .json',
);
});

it('raises an exception if config is not a supported file type', () => {
const message =
'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .mjs, .cjs, .json';
'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .json';

expect(() => check({config: 'jest.configjs'} as Config.Argv)).toThrow(
message,
Expand Down
Expand Up @@ -45,9 +45,81 @@ Object {
}
`;

exports[`init has-jest-config-file-ts ask the user whether to override config or not user answered with "Yes" 1`] = `
Object {
"initial": true,
"message": "It seems that you already have a jest configuration, do you want to override it?",
"name": "continue",
"type": "confirm",
}
`;

exports[`init project using jest.config.ts ask the user whether he wants to use Typescript or not user answered with "Yes" 1`] = `
Array [
Object {
"initial": true,
"message": "Would you like to use Jest when running \\"test\\" script in \\"package.json\\"?",
"name": "scripts",
"type": "confirm",
},
Object {
"initial": false,
"message": "Would you like to use Typescript for the configuration file?",
"name": "useTypescript",
"type": "confirm",
},
Object {
"choices": Array [
Object {
"title": "node",
"value": "node",
},
Object {
"title": "jsdom (browser-like)",
"value": "jsdom",
},
],
"initial": 0,
"message": "Choose the test environment that will be used for testing",
"name": "environment",
"type": "select",
},
Object {
"initial": false,
"message": "Do you want Jest to add coverage reports?",
"name": "coverage",
"type": "confirm",
},
Object {
"choices": Array [
Object {
"title": "v8",
"value": "v8",
},
Object {
"title": "babel",
"value": "babel",
},
],
"initial": 0,
"message": "Which provider should be used to instrument code for coverage?",
"name": "coverageProvider",
"type": "select",
},
Object {
"initial": false,
"message": "Automatically clear mock calls and instances between every test?",
"name": "clearMocks",
"type": "confirm",
},
]
`;

exports[`init project with package.json and no jest config all questions answered with answer: "No" should return the default configuration (an empty config) 1`] = `
"// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
"/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/en/configuration.html
*/
module.exports = {
// All imported modules in your tests should be mocked automatically
Expand Down
@@ -0,0 +1,8 @@
/**
* 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.
*/

export default {};
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
30 changes: 29 additions & 1 deletion packages/jest-cli/src/init/__tests__/init.test.js
Expand Up @@ -66,7 +66,7 @@ describe('init', () => {
expect(writtenJestConfigFilename.endsWith('.mjs')).toBe(true);

expect(typeof writtenJestConfig).toBe('string');
expect(writtenJestConfig.split('\n')[3]).toBe('export default {');
expect(writtenJestConfig.split('\n')[5]).toBe('export default {');
});
});

Expand Down Expand Up @@ -193,6 +193,34 @@ describe('init', () => {
},
);

describe('project using jest.config.ts', () => {
describe('ask the user whether he wants to use Typescript or not', () => {
it('user answered with "Yes"', async () => {
prompts.mockReturnValueOnce({useTypescript: true});

await init(resolveFromFixture('test_generated_jest_config_ts'));

expect(prompts.mock.calls[0][0]).toMatchSnapshot();

const jestConfigFileName = fs.writeFileSync.mock.calls[0][0];
const writtenJestConfig = fs.writeFileSync.mock.calls[0][1];

expect(jestConfigFileName).toContain('/jest.config.ts');
expect(writtenJestConfig.split('\n')[5]).toBe('export default {');
});

it('user answered with "No"', async () => {
prompts.mockReturnValueOnce({useTypescript: false});

await init(resolveFromFixture('test_generated_jest_config_ts'));

const jestConfigFileName = fs.writeFileSync.mock.calls[0][0];

expect(jestConfigFileName).not.toContain('jest.config.ts');
});
});
});

describe('has jest config in package.json', () => {
it('should ask the user whether to override config or not', async () => {
prompts.mockReturnValueOnce({continue: true}).mockReturnValueOnce({});
Expand Down
24 changes: 20 additions & 4 deletions packages/jest-cli/src/init/generate_config_file.ts
Expand Up @@ -35,7 +35,13 @@ const generateConfigFile = (
results: Record<string, unknown>,
generateEsm = false,
): string => {
const {coverage, coverageProvider, clearMocks, environment} = results;
const {
useTypescript,
coverage,
coverageProvider,
clearMocks,
environment,
} = results;

const overrides: Record<string, unknown> = {};

Expand Down Expand Up @@ -81,10 +87,20 @@ const generateConfigFile = (
}
}

const configHeaderMessage = `/*
* For a detailed explanation regarding each configuration property${
useTypescript ? ' and type check' : ''
}, visit:
* https://jestjs.io/docs/en/configuration.html
*/
`;

return (
'// For a detailed explanation regarding each configuration property, visit:\n' +
'// https://jestjs.io/docs/en/configuration.html\n\n' +
(generateEsm ? 'export default {\n' : 'module.exports = {\n') +
configHeaderMessage +
(useTypescript || generateEsm
? 'export default {\n'
: 'module.exports = {\n') +
properties.join('\n') +
'};\n'
);
Expand Down

0 comments on commit 591d612

Please sign in to comment.