Skip to content

feat(Programmatic API): Add programmatic API #14062

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
908c73d
feat(Programmatic API): Add programmatic API
nicojs Apr 9, 2023
e2c65f8
Implement review comments
nicojs Apr 10, 2023
6a75acf
Fix lint: add copyright header
nicojs Apr 10, 2023
7d70600
Import performance from `perf_hooks`
nicojs Apr 11, 2023
8155081
Fix type error
nicojs Apr 12, 2023
79dbfd5
Remove pnpapi from types
nicojs Apr 20, 2023
d0fd9a9
Add e2e test for runCore
nicojs Apr 21, 2023
b9e9278
Update e2e test for programmatic run
nicojs Apr 21, 2023
d8f6e26
Fix lint e2e
nicojs Apr 21, 2023
68c924f
fix prettier
nicojs Apr 21, 2023
68ac3ef
Add unit tests
nicojs Apr 21, 2023
a375b0d
Add copyright header
nicojs Apr 21, 2023
0f8f6f2
Add docs for programmatic api
nicojs Apr 22, 2023
1935947
Rename `result` -> `results` to align with `runCLI` api
nicojs Apr 22, 2023
77723b3
Reorder exports
nicojs Apr 22, 2023
a749243
Format
nicojs Apr 22, 2023
48a5a94
Merge branch 'main' into feat/programmatic-api
nicojs Apr 30, 2023
ac2b879
Implement object oriented API proposal
nicojs May 2, 2023
e5c254f
Merge branch 'main' into feat/programmatic-api
nicojs May 2, 2023
59cfa1a
Fix lint
nicojs May 2, 2023
d570bc8
Undo unwanted changes
nicojs May 2, 2023
80d3c23
Undo export of `readConfigs` and `readInitialOptions`
nicojs May 2, 2023
0b6f93d
Merge branch 'main' into feat/programmatic-api
SimenB Oct 3, 2023
6877134
Merge branch 'main' into feat/programmatic-api
SimenB Oct 3, 2023
26e87e1
remove unused types field in tsconfig
SimenB Oct 3, 2023
f3e41ef
Merge branch 'main' into feat/programmatic-api
SimenB Oct 3, 2023
c3b95f8
fix(expect, jest-snapshot): Pass `test.failing` tests when containing…
KhaledElmorsy Oct 3, 2023
2677e1d
chore: update ESLint dependencies (#14595)
SimenB Oct 3, 2023
8ec975a
chore(deps): update yarn to v3.6.4 (#14600)
renovate[bot] Oct 4, 2023
ccc7e4e
feat: add `jest.advanceTimersToFrame()` (#14598)
alexreardon Oct 5, 2023
f04118d
fix: coverage badge is broken at readme (#14611)
nolddor Oct 10, 2023
f8e0259
docs: provide README for `jest-haste-map` package (#14613)
Mutesa-Cedric Oct 12, 2023
098cc21
docs: update with "experimental" message
nicojs Oct 13, 2023
65fe53f
docs: remove mention of old `runCore`
nicojs Oct 13, 2023
9f1a113
Replace old api docs with the new api using jest instance.
nicojs Oct 14, 2023
8da9922
Merge branch 'main' into feat/programmatic-api
nicojs Oct 14, 2023
6a26f7f
prettier
nicojs Oct 14, 2023
f6d0c5a
Merge branch 'main' into feat/programmatic-api
SimenB Oct 30, 2023
fe45d4f
Merge branch 'main' into feat/programmatic-api
nicojs Oct 31, 2023
ef783ed
refactor: remove unessesary config
nicojs Oct 31, 2023
46a7ce9
fix: use jest's native esm api in e2e test
nicojs Oct 31, 2023
9f056b8
refactor: remove `_` prefix
nicojs Oct 31, 2023
92d6d44
docs: remove reference to old `readInitialOptions`.
nicojs Oct 31, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions docs/ProgrammaticApi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
id: programmatic-api
title: Programmatic API
---

:::caution

The programmatic API is currently **experimental**. It is useful for advanced use cases only. You normally don't need to use it if you just want to run your tests.

:::

This page documents Jest's programmable API that can be used to run jest from `node`. TypeScript types are provided.

## Simple example

```js
import {createJest} from 'jest';

const jest = await createJest();
jest.globalConfig = {
collectCoverage: false,
watch: false,
...jest.globalConfig,
};
Comment on lines +20 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should expose something that can be mutated in this way. I'd rather have a mergeConfig or some such that is invoked with the current config and expects to get a new config back it assigns internally

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean something like this?

import {createJest, mergeConfig} from 'jest';
const jest = await createJest();

// Override global options
jest.globalConfig = mergeConfig(jest.globalConfig, {
  collectCoverage: false,
});

Or more like this?

import {createJest} from 'jest';
const jest = await createJest();

// Override global options
jest.mergeConfig(jest.globalConfig, {
  collectCoverage: false,
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import {createJest} from 'jest';
const jest = await createJest();

// Override global options
jest.mergeGlobalConfig({
  collectCoverage: false,
});

or

jest.globalConfig.merge({
  collectCoverage: false,
});

The latter would mean globalConfig was some sort of class or something instead of just the object

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you override a project-specific config?

I think allowing you to mutate config in-place makes more sense, actually. So removing the read-only feature. I don't think freezing the config serves a real purpose. 🤷‍♀️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally prefer encapsulation - makes it easier to debug when all mutations come though known methods and you can set a breakpoint in it.


How would you override a project-specific config?

jest.mergeProjectConfig(index, {
  testEnvironment: 'node',
});

This actually brings an important point to mind - any config modifications must go through its part in normalize within jest-config, to e.g. resolve testEnvironment to an absolute path.

I guess we can punt on that for now though. But if we do kick it down the line for later, I want an

jest.unstable_modifyConfig() or some such so we can add proper config normalization later without a breaking change.

const {results} = await jest.run();
console.log(`run success, ${result.numPassedTests} passed tests.`);
```

## Programmatic API reference

### `createJest(args: Partial<Config.Argv> = {}, projectPath = ['.']): Promise<Jest>` \[function]

Create a Jest instance asynchronously. You can provide command line arguments (for example, `process.argv`) as first argument and a list of custom [projects](./Configuration.md#projects-arraystring--projectconfig) as the second argument. If no `projects`, were configured, the current project will be provided as project config.

Examples:

```js
import {createJest} from 'jest';

const jest = await createJest();
const jest2 = await createJest({config: 'jest.alternative.config.js'});
```

### `jest.globalConfig` \[Readonly\<GlobalConfig>]

The global config associated with this jest instance. It is `readonly`, so it cannot be changed in-place. In order to change it, you will need to create a new object.

Example:

```js
jest.globalConfig = {
...jest.globalConfig,
collectCoverage: false,
watch: false,
};
```

### `jest.projectConfigs` \[Readonly\<ProjectConfig>\[]]

A list of project configurations associated with this jest instance. They are `readonly`, so it cannot be changed in-place. In order to change it, you will need to create a new object.

```js
jest.projectConfigs = jest.projectConfigs.map(config => ({
...config,
setupFiles: ['custom-setup.js', ...config.setupFiles],
}));
```

### `jest.run` \[function]

Async function that performs the run. It returns a promise that resolves in a `JestRunResult` object. This object has a `results` property that contains the actual results.

## Advanced use cases

These are more advanced use cases that demonstrate the power of the api.

### Overriding config options

You can use `createJest` to create a Jest instance, and alter some of the options using `globalConfig` adn `projectConfigs`.

```js
import {createJest} from 'jest';
const jest = await createJest();

// Override global options
jest.globalConfig = {
...jest.globalConfig,
collectCoverage: false,
reporters: [],
testResultsProcessor: undefined,
watch: false,
testPathPattern: 'my-test.spec.js',
};

// Override project options
jest.projectConfigs = jest.projectConfigs.map(config => ({
...config,
setupFiles: ['custom-setup.js', ...config.setupFiles],
}));

// Run
const {results} = await jest.run();
console.log(`run success, ${results.numPassedTests} passed tests.`);
```

### Override options based on the configured options

You might want to override options based on other options. For example, you might want to provide your own version of the `jsdom` or `node` test environment.

```js
import {createJest} from 'jest';

const jest = await createJest();

jest.projectConfigs = [
{
...jest.projectConfigs[0],
// Change the test environment based on the configured test environment
testEnvironment: overrideTestEnvironment(configs[0].testEnvironment),
},
];

const {results} = await jest.run();
console.log(results);
```
11 changes: 11 additions & 0 deletions e2e/__tests__/__snapshots__/runProgrammatically.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`createJest run programmatically: stdout 1`] = `"run success, 1 passed tests."`;

exports[`createJest run programmatically: summary 1`] = `
"Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`run programmatically with multiple projects: summary 1`] = `
exports[`run jest programmatically with multiple projects: summary 1`] = `
"Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites in 2 projects."
`;

exports[`runCLI programmatically with multiple projects: summary 1`] = `
"Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Expand Down
11 changes: 10 additions & 1 deletion e2e/__tests__/runProgrammatically.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
*/

import {resolve} from 'path';
import {run} from '../Utils';
import stripAnsi = require('strip-ansi');
import {extractSummary, run} from '../Utils';

const dir = resolve(__dirname, '..', 'run-programmatically');

Expand All @@ -19,3 +20,11 @@ test('run Jest programmatically esm', () => {
const {stdout} = run('node index.js --version', dir);
expect(stdout).toMatch(/\d{2}\.\d{1,2}\.\d{1,2}[-\S]*-dev$/);
});

test('createJest run programmatically', () => {
const {stderr, stdout} = run('node jest.mjs', dir);
const {summary} = extractSummary(stripAnsi(stderr));

expect(summary).toMatchSnapshot('summary');
expect(stdout).toMatchSnapshot('stdout');
});
9 changes: 8 additions & 1 deletion e2e/__tests__/runProgrammaticallyMultipleProjects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import {extractSummary, run} from '../Utils';

const dir = resolve(__dirname, '../run-programmatically-multiple-projects');

test('run programmatically with multiple projects', () => {
test('runCLI programmatically with multiple projects', () => {
const {stderr, exitCode} = run('node run-cli.js', dir);
const {summary} = extractSummary(stripAnsi(stderr));
expect(exitCode).toBe(0);
expect(summary).toMatchSnapshot('summary');
});

test('run jest programmatically with multiple projects', () => {
const {stderr, exitCode} = run('node run-jest.js', dir);
const {summary} = extractSummary(stripAnsi(stderr));
expect(exitCode).toBe(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
*/

describe('client', () => {
it('should work', () => {});
it('should work', () => {
expect(typeof document).toBe('object');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"testMatch": ["**/*.test.js"],
"testEnvironment": "jsdom"
}
8 changes: 7 additions & 1 deletion e2e/run-programmatically-multiple-projects/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
"name": "runcli-multiple-projects",
"version": "1.0.0",
"dependencies": {},
"jest": {}
"jest": {
"maxWorkers": 1,
"projects": [
"<rootDir>/server",
"<rootDir>/client"
]
}
}
24 changes: 24 additions & 0 deletions e2e/run-programmatically-multiple-projects/run-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

const {runCLI} = require('jest');

const config = {
projects: [
{testEnvironment: 'jsdom', testMatch: ['<rootDir>/client/**/*.test.js']},
{testEnvironment: 'node', testMatch: ['<rootDir>/server/**/*.test.js']},
],
};

runCLI({config: JSON.stringify(config)}, [process.cwd()])
.then(() =>
console.log('run-programmatically-cli-multiple-projects completed'),
)
.catch(err => {
console.error(err);
process.exitCode = 1;
});
28 changes: 15 additions & 13 deletions e2e/run-programmatically-multiple-projects/run-jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@
* LICENSE file in the root directory of this source tree.
*/

const {runCLI} = require('@jest/core');
const {createJest} = require('jest');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of changing this test, can you add a new one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicojs missed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved the old one to "run-cli.js", so this project now has 2 entry files.


const config = {
projects: [
{testMatch: ['<rootDir>/client/**/*.test.js']},
{testMatch: ['<rootDir>/server/**/*.test.js']},
],
};
async function main() {
const jest = await createJest();
jest.globalConfig = {
collectCoverage: false,
watch: false,
...jest.globalConfig,
};
await jest.run();
console.log('run-programmatically-core-multiple-projects completed');
}

runCLI({config: JSON.stringify(config)}, [process.cwd()])
.then(() => console.log('run-programmatically-mutiple-projects completed'))
.catch(err => {
console.error(err);
process.exitCode = 1;
});
main().catch(err => {
console.error(err);
process.exitCode = 1;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"testMatch": ["**/*.test.js"],
"testEnvironment": "node"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
*/

describe('server', () => {
it('should work', () => {});
it('should work', () => {
expect(typeof document).toBe('undefined');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you send a separate PR with this change?

});
});
16 changes: 16 additions & 0 deletions e2e/run-programmatically/jest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {createJest} from 'jest';

const jest = await createJest();
jest.globalConfig = {
collectCoverage: false,
watch: false,
...jest.globalConfig,
};
const {results} = await jest.run();
console.log(`run success, ${results.numPassedTests} passed tests.`);
11 changes: 11 additions & 0 deletions e2e/run-programmatically/src/app.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable no-undef */

describe('jest-core', () => {
it('should run this test', () => {});
});
Loading