feat(testing): add setupFilesAfterEnv and other configs to project's jest config file (#3224)

* feat(testing): add util to update jest configs.

* feat(testing): place configurations in jest config file rather than just the builder

* feat(testing): create migration and unit tests

* feat(testing): fix jest template

* feat(testing): fix jest template to correct unit tests

* feat(testing): include globals.ts-jest for all non babel configs

* feat(testing): include globals.ts-jest for node e2e

* feat(testing): fix migration to run properly. Also check for angular tests using the setupfile rather than builder

* feat(testing): clean up jest config functions and fix errors with some migrations

* feat(testing): add new line to package.json

* feat(testing): update object check to actually check for undefined

* chore(testing): loop through all project targets as well as targets

* chore(testing): update migration to be 10.0.0-beta.2
This commit is contained in:
Jonathan Cammisuli 2020-07-13 10:45:04 -04:00 committed by GitHub
parent 4968b6e6e3
commit 3b8a10f073
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1008 additions and 136 deletions

View File

@ -6,6 +6,9 @@
"sourceType": "module", "sourceType": "module",
"project": "./tsconfig.json" "project": "./tsconfig.json"
}, },
"env": {
"node": true
},
"ignorePatterns": ["**/*"], "ignorePatterns": ["**/*"],
"plugins": ["@typescript-eslint", "@nrwl/nx"], "plugins": ["@typescript-eslint", "@nrwl/nx"],
"extends": [ "extends": [

View File

@ -136,7 +136,7 @@ Run all tests serially in the current process (rather than creating a worker poo
Type: `string` Type: `string`
The name of a setup file used by Jest. (https://jestjs.io/docs/en/configuration#setupfilesafterenv-array) [Deprecated] The name of a setup file used by Jest. (use Jest config file https://jestjs.io/docs/en/configuration#setupfilesafterenv-array)
### showConfig ### showConfig
@ -186,7 +186,7 @@ Node module that implements a custom results processor. (https://jestjs.io/docs/
Type: `string` Type: `string`
The name of the Typescript configuration file. [Deprecated] The name of the Typescript configuration file. Set the tsconfig option in the jest config file.
### updateSnapshot ### updateSnapshot

View File

@ -137,7 +137,7 @@ Run all tests serially in the current process (rather than creating a worker poo
Type: `string` Type: `string`
The name of a setup file used by Jest. (https://jestjs.io/docs/en/configuration#setupfilesafterenv-array) [Deprecated] The name of a setup file used by Jest. (use Jest config file https://jestjs.io/docs/en/configuration#setupfilesafterenv-array)
### showConfig ### showConfig
@ -187,7 +187,7 @@ Node module that implements a custom results processor. (https://jestjs.io/docs/
Type: `string` Type: `string`
The name of the Typescript configuration file. [Deprecated] The name of the Typescript configuration file. Set the tsconfig option in the jest config file.
### updateSnapshot ### updateSnapshot

View File

@ -337,6 +337,11 @@ forEachCli((currentCLIName) => {
stripIndents`module.exports = { stripIndents`module.exports = {
name: '${nestlib}', name: '${nestlib}',
preset: '../../jest.config.js', preset: '../../jest.config.js',
globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json',
},
},
testEnvironment: 'node', testEnvironment: 'node',
transform: { transform: {
'^.+\\.[tj]sx?$': 'ts-jest', '^.+\\.[tj]sx?$': 'ts-jest',

View File

@ -0,0 +1,8 @@
export {
addPropertyToJestConfig,
removePropertyFromJestConfig,
} from './src/utils/config/update-config';
export {
jestConfigObjectAst,
jestConfigObject,
} from './src/utils/config/functions';

View File

@ -24,6 +24,11 @@
"version": "9.2.0-beta.3", "version": "9.2.0-beta.3",
"description": "Update jest to v25", "description": "Update jest to v25",
"factory": "./src/migrations/update-9-2-0/update-9-2-0" "factory": "./src/migrations/update-9-2-0/update-9-2-0"
},
"update-10.0.0": {
"version": "10.0.0-beta.2",
"description": "update jest configs to include setup env files",
"factory": "./src/migrations/update-10-0-0/update-jest-configs"
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {

View File

@ -61,16 +61,6 @@ describe('Jest Builder', () => {
expect(runCLI).toHaveBeenCalledWith( expect(runCLI).toHaveBeenCalledWith(
{ {
_: [], _: [],
globals: JSON.stringify({
'ts-jest': {
tsConfig: '/root/tsconfig.test.json',
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
},
}),
testPathPattern: [], testPathPattern: [],
watch: false, watch: false,
}, },
@ -103,16 +93,6 @@ describe('Jest Builder', () => {
expect(runCLI).toHaveBeenCalledWith( expect(runCLI).toHaveBeenCalledWith(
{ {
_: ['lib.spec.ts'], _: ['lib.spec.ts'],
globals: JSON.stringify({
'ts-jest': {
tsConfig: '/root/tsconfig.test.json',
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
},
}),
coverage: false, coverage: false,
runInBand: true, runInBand: true,
testNamePattern: 'should load', testNamePattern: 'should load',
@ -147,16 +127,6 @@ describe('Jest Builder', () => {
expect(runCLI).toHaveBeenCalledWith( expect(runCLI).toHaveBeenCalledWith(
{ {
_: ['file1.ts', 'file2.ts'], _: ['file1.ts', 'file2.ts'],
globals: JSON.stringify({
'ts-jest': {
tsConfig: '/root/tsconfig.test.json',
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
},
}),
coverage: false, coverage: false,
findRelatedTests: true, findRelatedTests: true,
runInBand: true, runInBand: true,
@ -206,16 +176,6 @@ describe('Jest Builder', () => {
expect(runCLI).toHaveBeenCalledWith( expect(runCLI).toHaveBeenCalledWith(
{ {
_: [], _: [],
globals: JSON.stringify({
'ts-jest': {
tsConfig: '/root/tsconfig.test.json',
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
},
}),
coverage: true, coverage: true,
bail: 1, bail: 1,
color: false, color: false,
@ -260,16 +220,6 @@ describe('Jest Builder', () => {
expect(runCLI).toHaveBeenCalledWith( expect(runCLI).toHaveBeenCalledWith(
{ {
_: [], _: [],
globals: JSON.stringify({
'ts-jest': {
tsConfig: '/root/tsconfig.test.json',
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
},
}),
maxWorkers: '50%', maxWorkers: '50%',
testPathPattern: [], testPathPattern: [],
}, },
@ -292,16 +242,6 @@ describe('Jest Builder', () => {
expect(runCLI).toHaveBeenCalledWith( expect(runCLI).toHaveBeenCalledWith(
{ {
_: [], _: [],
globals: JSON.stringify({
'ts-jest': {
tsConfig: '/root/tsconfig.test.json',
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
},
}),
setupFilesAfterEnv: ['/root/test-setup.ts'], setupFilesAfterEnv: ['/root/test-setup.ts'],
testPathPattern: [], testPathPattern: [],
watch: false, watch: false,
@ -350,18 +290,6 @@ describe('Jest Builder', () => {
expect(runCLI).toHaveBeenCalledWith( expect(runCLI).toHaveBeenCalledWith(
{ {
_: [], _: [],
globals: JSON.stringify({
hereToStay: true,
'ts-jest': {
diagnostics: false,
tsConfig: '/root/tsconfig.test.json',
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
},
}),
setupFilesAfterEnv: ['/root/test-setup.ts'], setupFilesAfterEnv: ['/root/test-setup.ts'],
testPathPattern: [], testPathPattern: [],
watch: false, watch: false,
@ -400,7 +328,6 @@ describe('Jest Builder', () => {
expect(runCLI).toHaveBeenCalledWith( expect(runCLI).toHaveBeenCalledWith(
{ {
_: [], _: [],
globals: '{}',
testPathPattern: [], testPathPattern: [],
watch: false, watch: false,
}, },

View File

@ -11,14 +11,14 @@ import { JestBuilderOptions } from './schema';
try { try {
require('dotenv').config(); require('dotenv').config();
} catch (e) {} } catch (e) {
// noop
}
if (process.env.NODE_ENV == null || process.env.NODE_ENV == undefined) { if (process.env.NODE_ENV == null || process.env.NODE_ENV == undefined) {
(process.env as any).NODE_ENV = 'test'; (process.env as any).NODE_ENV = 'test';
} }
export default createBuilder<JestBuilderOptions>(run);
function run( function run(
options: JestBuilderOptions, options: JestBuilderOptions,
context: BuilderContext context: BuilderContext
@ -28,9 +28,11 @@ function run(
const jestConfig: { const jestConfig: {
transform: any; transform: any;
globals: any; globals: any;
setupFilesAfterEnv: any;
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = require(options.jestConfig); } = require(options.jestConfig);
let transformers = Object.values<string>(jestConfig.transform || {}); const transformers = Object.values<string>(jestConfig.transform || {});
if (transformers.includes('babel-jest') && transformers.includes('ts-jest')) { if (transformers.includes('babel-jest') && transformers.includes('ts-jest')) {
throw new Error( throw new Error(
'Using babel-jest and ts-jest together is not supported.\n' + 'Using babel-jest and ts-jest together is not supported.\n' +
@ -38,35 +40,6 @@ function run(
); );
} }
// use ts-jest by default
const globals = jestConfig.globals || {};
if (!transformers.includes('babel-jest')) {
const tsJestConfig = {
tsConfig: path.resolve(context.workspaceRoot, options.tsConfig),
};
// TODO: This is hacky, We should probably just configure it in the user's workspace
// If jest-preset-angular is installed, apply settings
try {
require.resolve('jest-preset-angular');
Object.assign(tsJestConfig, {
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
});
} catch (e) {}
// merge the jestConfig globals with our 'ts-jest' override
Object.assign(globals, {
'ts-jest': {
...(globals['ts-jest'] || {}),
...tsJestConfig,
},
});
}
const config: any = { const config: any = {
_: [], _: [],
config: options.config, config: options.config,
@ -95,13 +68,15 @@ function run(
useStderr: options.useStderr, useStderr: options.useStderr,
watch: options.watch, watch: options.watch,
watchAll: options.watchAll, watchAll: options.watchAll,
globals: JSON.stringify(globals),
}; };
// for backwards compatibility
if (options.setupFile) { if (options.setupFile) {
config.setupFilesAfterEnv = [ const setupFilesAfterEnvSet = new Set([
...(jestConfig.setupFilesAfterEnv ?? []),
path.resolve(context.workspaceRoot, options.setupFile), path.resolve(context.workspaceRoot, options.setupFile),
]; ]);
config.setupFilesAfterEnv = Array.from(setupFilesAfterEnvSet);
} }
if (options.testFile) { if (options.testFile) {
@ -132,3 +107,5 @@ function run(
}) })
); );
} }
export default createBuilder<JestBuilderOptions>(run);

View File

@ -29,12 +29,14 @@
"type": "string" "type": "string"
}, },
"tsConfig": { "tsConfig": {
"description": "The name of the Typescript configuration file.", "description": "[Deprecated] The name of the Typescript configuration file. Set the tsconfig option in the jest config file. ",
"type": "string" "type": "string",
"x-deprecated": true
}, },
"setupFile": { "setupFile": {
"description": "The name of a setup file used by Jest. (https://jestjs.io/docs/en/configuration#setupfilesafterenv-array)", "description": "[Deprecated] The name of a setup file used by Jest. (use Jest config file https://jestjs.io/docs/en/configuration#setupfilesafterenv-array)",
"type": "string" "type": "string",
"x-deprecated": true
}, },
"bail": { "bail": {
"alias": "b", "alias": "b",
@ -150,5 +152,5 @@
"type": "boolean" "type": "boolean"
} }
}, },
"required": ["jestConfig", "tsConfig"] "required": ["jestConfig"]
} }

View File

@ -0,0 +1,156 @@
import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import * as path from 'path';
import { serializeJson } from '@nrwl/workspace';
import { jestConfigObject } from '../../..';
describe('update 10.0.0', () => {
let initialTree: Tree;
let schematicRunner: SchematicTestRunner;
const jestConfig = String.raw`
module.exports = {
name: 'test-jest',
preset: '../../jest.config.js',
coverageDirectory: '../../coverage/libs/test-jest',
globals: {
"existing-global": "test"
},
snapshotSerializers: [
'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
'jest-preset-angular/build/AngularSnapshotSerializer.js',
'jest-preset-angular/build/HTMLCommentSerializer.js'
]
}
`;
const jestConfigReact = String.raw`
module.exports = {
name: 'my-react-app',
preset: '../../jest.config.js',
transform: {
'^(?!.*\\\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\\\.[tj]sx?$': [
'babel-jest',
{ cwd: __dirname, configFile: './babel-jest.config.json' }
]
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
coverageDirectory: '../../coverage/apps/my-react-app'
}
`;
beforeEach(() => {
initialTree = createEmptyWorkspace(Tree.empty());
initialTree.create('apps/products/jest.config.js', jestConfig);
initialTree.create(
'apps/products/src/test-setup.ts',
`import 'jest-preset-angular'`
);
initialTree.create('apps/cart/jest.config.js', jestConfigReact);
initialTree.overwrite(
'workspace.json',
serializeJson({
version: 1,
projects: {
products: {
root: 'apps/products',
sourceRoot: 'apps/products/src',
architect: {
build: {
builder: '@angular-devkit/build-angular:browser',
},
test: {
builder: '@nrwl/jest:jest',
options: {
jestConfig: 'apps/products/jest.config.js',
tsConfig: 'apps/products/tsconfig.spec.json',
setupFile: 'apps/products/src/test-setup.ts',
passWithNoTests: true,
},
},
},
},
cart: {
root: 'apps/cart',
sourceRoot: 'apps/cart/src',
architect: {
build: {
builder: '@nrwl/web:build',
},
test: {
builder: '@nrwl/jest:jest',
options: {
jestConfig: 'apps/cart/jest.config.js',
passWithNoTests: true,
},
},
},
},
},
})
);
schematicRunner = new SchematicTestRunner(
'@nrwl/jest',
path.join(__dirname, '../../../migrations.json')
);
});
it('should remove setupFile and tsconfig in test architect from workspace.json', async (done) => {
const result = await schematicRunner
.runSchematicAsync('update-10.0.0', {}, initialTree)
.toPromise();
const updatedWorkspace = JSON.parse(result.readContent('workspace.json'));
expect(updatedWorkspace.projects.products.architect.test.options).toEqual({
jestConfig: expect.anything(),
passWithNoTests: expect.anything(),
});
expect(updatedWorkspace.projects.cart.architect.test.options).toEqual({
jestConfig: expect.anything(),
passWithNoTests: expect.anything(),
});
done();
});
it('should update the jest.config files', async (done) => {
await schematicRunner
.runSchematicAsync('update-10.0.0', {}, initialTree)
.toPromise();
const jestObject = jestConfigObject(
initialTree,
'apps/products/jest.config.js'
);
const angularSetupFiles = jestObject.setupFilesAfterEnv;
const angularGlobals = jestObject.globals;
expect(angularSetupFiles).toEqual(['<rootDir>/src/test-setup.ts']);
expect(angularGlobals).toEqual({
'existing-global': 'test',
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
},
});
const reactJestObject = jestConfigObject(
initialTree,
'apps/cart/jest.config.js'
);
const reactSetupFiles = reactJestObject.setupFilesAfterEnv;
const reactGlobals = reactJestObject.globals;
expect(reactSetupFiles).toBeUndefined();
expect(reactGlobals).toBeUndefined();
done();
});
});

View File

@ -0,0 +1,173 @@
import {
chain,
Rule,
SchematicContext,
Tree,
} from '@angular-devkit/schematics';
import {
formatFiles,
getWorkspace,
getWorkspacePath,
serializeJson,
updateWorkspace,
} from '@nrwl/workspace';
import { addPropertyToJestConfig, jestConfigObject } from '../../..';
function checkJestPropertyObject(object: unknown): object is object {
return object !== null && object !== undefined;
}
function modifyJestConfig(
host: Tree,
context: SchematicContext,
project: string,
setupFile: string,
jestConfig: string,
tsConfig: string,
isAngular: boolean
) {
if (setupFile === '') {
return;
}
let globalTsJest: any = {
tsConfig,
};
if (isAngular) {
globalTsJest = {
...globalTsJest,
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
};
}
try {
const jestObject = jestConfigObject(host, jestConfig);
// add set up env file
// setupFilesAfterEnv
const existingSetupFiles = jestObject.setupFilesAfterEnv;
let setupFilesAfterEnv: string | string[] = [setupFile];
if (Array.isArray(existingSetupFiles)) {
setupFilesAfterEnv = setupFile;
}
addPropertyToJestConfig(
host,
jestConfig,
'setupFilesAfterEnv',
setupFilesAfterEnv
);
// check if jest config has babel transform
const transformProperty = jestObject.transform;
let hasBabelTransform = false;
if (transformProperty) {
for (const prop in transformProperty) {
const transformPropValue = transformProperty[prop];
if (Array.isArray(transformPropValue)) {
hasBabelTransform = transformPropValue.some(
(value) => typeof value === 'string' && value.includes('babel')
);
} else if (typeof transformPropValue === 'string') {
transformPropValue.includes('babel');
}
}
}
if (hasBabelTransform) {
return;
}
// Add ts-jest configurations
const existingGlobals = jestObject.globals;
if (!existingGlobals) {
addPropertyToJestConfig(host, jestConfig, 'globals', {
'ts-jest': globalTsJest,
});
} else {
const existingGlobalTsJest = existingGlobals['ts-jest'];
if (!checkJestPropertyObject(existingGlobalTsJest)) {
addPropertyToJestConfig(
host,
jestConfig,
'globals.ts-jest',
globalTsJest
);
}
}
} catch {
context.logger.warn(`
Cannot update jest config for the ${project} project.
This is most likely caused because the jest config at ${jestConfig} it not in a expected configuration format (ie. module.exports = {}).
Since this migration could not be ran on this project, please make sure to modify the Jest config file to have the following configured:
* setupFilesAfterEnv with: "${setupFile}"
* globals.ts-jest with:
"${serializeJson(globalTsJest)}"
`);
}
}
function updateJestConfigForProjects() {
return async (host: Tree, context: SchematicContext) => {
const workspace = await getWorkspace(host, getWorkspacePath(host));
for (const [projectName, projectDefinition] of workspace.projects) {
for (const [, testTarget] of projectDefinition.targets) {
if (testTarget.builder !== '@nrwl/jest:jest') {
continue;
}
const setupfile = testTarget.options?.setupFile;
const jestConfig = (testTarget.options?.jestConfig as string) ?? '';
const tsConfig = (testTarget.options?.tsConfig as string) ?? '';
const tsConfigWithRootDir = tsConfig.replace(
projectDefinition.root,
'<rootDir>'
);
let isAngular = false;
let setupFileWithRootDir = '';
if (typeof setupfile === 'string') {
isAngular = host
.read(setupfile)
?.toString()
.includes('jest-preset-angular');
setupFileWithRootDir = setupfile.replace(
projectDefinition.root,
'<rootDir>'
);
}
modifyJestConfig(
host,
context,
projectName,
setupFileWithRootDir,
jestConfig,
tsConfigWithRootDir,
isAngular
);
const updatedOptions = { ...testTarget.options };
delete updatedOptions.setupFile;
delete updatedOptions.tsConfig;
testTarget.options = updatedOptions;
}
}
return updateWorkspace(workspace);
};
}
export default function update(): Rule {
return chain([updateJestConfigForProjects(), formatFiles()]);
}

View File

@ -1,6 +1,18 @@
module.exports = { module.exports = {
name: '<%= project %>', name: '<%= project %>',
preset: '<%= offsetFromRoot %>jest.config.js',<% if(testEnvironment) { %> preset: '<%= offsetFromRoot %>jest.config.js',<% if(setupFile !== 'none') { %>
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
<% } %><% if (transformer === 'ts-jest') { %>
globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json',<%if (setupFile === 'angular') { %>
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer'
],<% } %>
}
},<% } %><% if(testEnvironment) { %>
testEnvironment: '<%= testEnvironment %>',<% } %><% if (supportTsx) { %> testEnvironment: '<%= testEnvironment %>',<% } %><% if (supportTsx) { %>
transform: { transform: {
'^.+\\.[tj]sx?$': <% if (transformer == 'babel-jest') { %>[ 'babel-jest', '^.+\\.[tj]sx?$': <% if (transformer == 'babel-jest') { %>[ 'babel-jest',

View File

@ -2,6 +2,7 @@ import { Tree } from '@angular-devkit/schematics';
import { readJsonInTree, updateJsonInTree } from '@nrwl/workspace'; import { readJsonInTree, updateJsonInTree } from '@nrwl/workspace';
import { createEmptyWorkspace } from '@nrwl/workspace/testing'; import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { callRule, runSchematic } from '../../utils/testing'; import { callRule, runSchematic } from '../../utils/testing';
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
describe('jestProject', () => { describe('jestProject', () => {
let appTree: Tree; let appTree: Tree;
@ -84,10 +85,15 @@ describe('jestProject', () => {
}, },
appTree appTree
); );
expect(resultTree.readContent('libs/lib1/jest.config.js')) expect(stripIndents`${resultTree.readContent('libs/lib1/jest.config.js')}`)
.toBe(`module.exports = { .toBe(stripIndents`module.exports = {
name: 'lib1', name: 'lib1',
preset: '../../jest.config.js', preset: '../../jest.config.js',
globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json',
}
},
coverageDirectory: '../../coverage/libs/lib1', coverageDirectory: '../../coverage/libs/lib1',
snapshotSerializers: [ snapshotSerializers: [
'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
@ -145,6 +151,47 @@ describe('jestProject', () => {
appTree appTree
); );
expect(resultTree.exists('src/test-setup.ts')).toBeFalsy(); expect(resultTree.exists('src/test-setup.ts')).toBeFalsy();
expect(resultTree.readContent('libs/lib1/jest.config.js')).not.toContain(
`setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],`
);
});
it('should have setupFilesAfterEnv in the jest.config when generated for web-components', async () => {
const resultTree = await runSchematic(
'jest-project',
{
project: 'lib1',
setupFile: 'web-components',
},
appTree
);
expect(resultTree.readContent('libs/lib1/jest.config.js')).toContain(
`setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],`
);
});
it('should have setupFilesAfterEnv and globals.ts-ject in the jest.config when generated for angular', async () => {
const resultTree = await runSchematic(
'jest-project',
{
project: 'lib1',
setupFile: 'angular',
},
appTree
);
const jestConfig = resultTree.readContent('libs/lib1/jest.config.js');
expect(jestConfig).toContain(
`setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],`
);
expect(stripIndents`${jestConfig}`).toContain(stripIndents`globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer'
],`);
}); });
it('should not list the setup file in workspace.json', async () => { it('should not list the setup file in workspace.json', async () => {
@ -261,4 +308,50 @@ describe('jestProject', () => {
); );
}); });
}); });
describe('--babelJest', () => {
it('should have globals.ts-jest configured when babelJest is false', async () => {
const resultTree = await runSchematic(
'jest-project',
{
project: 'lib1',
babelJest: false,
setupFile: 'none',
},
appTree
);
const jestConfig = stripIndents`${resultTree.readContent(
'libs/lib1/jest.config.js'
)}`;
expect(jestConfig).toContain(
stripIndents`globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json',
}
}`
);
});
it('should have NOT have globals.ts-jest configured when babelJest is true', async () => {
const resultTree = await runSchematic(
'jest-project',
{
project: 'lib1',
babelJest: true,
setupFile: 'none',
},
appTree
);
const jestConfig = stripIndents`${resultTree.readContent(
'libs/lib1/jest.config.js'
)}`;
expect(jestConfig).not.toContain(
stripIndents`globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json',
}
}`
);
});
});
}); });

View File

@ -0,0 +1,209 @@
import * as ts from 'typescript';
import { findNodes, InsertChange, ReplaceChange } from '@nrwl/workspace';
import { Tree } from '@angular-devkit/schematics';
import * as stripJsonComments from 'strip-json-comments';
import { Config } from '@jest/types';
function trailingCommaNeeded(needed: boolean) {
return needed ? ',' : '';
}
function createInsertChange(
path: string,
value: unknown,
position: number,
commaNeeded: boolean
) {
return new InsertChange(
path,
position,
`${trailingCommaNeeded(!commaNeeded)}${value}${trailingCommaNeeded(
commaNeeded
)}`
);
}
function findPropertyAssignment(
object: ts.ObjectLiteralExpression,
propertyName: string
) {
return object.properties.find((prop) => {
const propNameText = prop.name.getText();
if (propNameText.match(/^["'].+["']$/g)) {
return JSON.parse(propNameText.replace(/'/g, '"')) === propertyName;
}
return propNameText === propertyName;
}) as ts.PropertyAssignment | undefined;
}
export function getJsonObject(object: string) {
const value = stripJsonComments(object);
// react babel-jest has __dirname in the config.
// Put a temp variable in the anon function so that it doesnt fail.
// Migration script has a catch handler to give instructions on how to update the jest config if this fails.
return Function(`
"use strict";
let __dirname = '';
return (${value});
`)();
}
export function addOrUpdateProperty(
object: ts.ObjectLiteralExpression,
properties: string[],
value: unknown,
path: string
) {
const propertyName = properties.shift();
const propertyAssignment = findPropertyAssignment(object, propertyName);
if (propertyAssignment) {
if (
propertyAssignment.initializer.kind === ts.SyntaxKind.StringLiteral ||
propertyAssignment.initializer.kind === ts.SyntaxKind.NumericLiteral ||
propertyAssignment.initializer.kind === ts.SyntaxKind.FalseKeyword ||
propertyAssignment.initializer.kind === ts.SyntaxKind.TrueKeyword
) {
return [
new ReplaceChange(
path,
propertyAssignment.initializer.pos,
propertyAssignment.initializer.getFullText(),
value as string
),
];
}
if (
propertyAssignment.initializer.kind ===
ts.SyntaxKind.ArrayLiteralExpression
) {
const arrayLiteral = propertyAssignment.initializer as ts.ArrayLiteralExpression;
if (
arrayLiteral.elements.some((element) => {
return element.getText().replace(/'/g, '"') === value;
})
) {
return [];
}
return [
createInsertChange(
path,
value,
arrayLiteral.elements.end,
arrayLiteral.elements.hasTrailingComma
),
];
} else if (
propertyAssignment.initializer.kind ===
ts.SyntaxKind.ObjectLiteralExpression
) {
return addOrUpdateProperty(
propertyAssignment.initializer as ts.ObjectLiteralExpression,
properties,
value,
path
);
}
} else {
if (propertyName === undefined) {
throw new Error(
`Please use dot delimited paths to update an existing object. Eg. object.property `
);
}
return [
createInsertChange(
path,
`${JSON.stringify(propertyName)}: ${value}`,
object.properties.end,
object.properties.hasTrailingComma
),
];
}
}
export function removeProperty(
object: ts.ObjectLiteralExpression,
properties: string[]
): ts.PropertyAssignment | null {
const propertyName = properties.shift();
const propertyAssignment = findPropertyAssignment(object, propertyName);
if (propertyAssignment) {
if (
properties.length > 0 &&
propertyAssignment.initializer.kind ===
ts.SyntaxKind.ObjectLiteralExpression
) {
return removeProperty(
propertyAssignment.initializer as ts.ObjectLiteralExpression,
properties
);
}
return propertyAssignment;
} else {
return null;
}
}
/**
* Should be used to get the jest config object.
*
* @param host
* @param path
*/
export function jestConfigObjectAst(
host: Tree,
path: string
): ts.ObjectLiteralExpression {
if (!host.exists(path)) {
throw new Error(`Cannot find '${path}' in your workspace.`);
}
const fileContent = host.read(path).toString('utf-8');
const sourceFile = ts.createSourceFile(
'jest.config.js',
fileContent,
ts.ScriptTarget.Latest,
true
);
const expressions = findNodes(
sourceFile,
ts.SyntaxKind.BinaryExpression
) as ts.BinaryExpression[];
const moduleExports = expressions.find(
(node) =>
node.left.getText() === 'module.exports' &&
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isObjectLiteralExpression(node.right)
);
if (!moduleExports) {
throw new Error(
`
The provided jest config file does not have the expected 'module.exports' expression.
See https://jestjs.io/docs/en/configuration for more details.`
);
}
return moduleExports.right as ts.ObjectLiteralExpression;
}
/**
* Returns the jest config object
* @param host
* @param path
*/
export function jestConfigObject(
host: Tree,
path: string
): Partial<Config.InitialOptions> & { [index: string]: any } {
const jestConfigAst = jestConfigObjectAst(host, path);
return getJsonObject(jestConfigAst.getText());
}

View File

@ -0,0 +1,235 @@
import { Tree } from '@angular-devkit/schematics';
import {
addPropertyToJestConfig,
removePropertyFromJestConfig,
} from './update-config';
import { jestConfigObject } from './functions';
describe('Update jest.config.js', () => {
let host: Tree;
beforeEach(() => {
host = Tree.empty();
// create
host.create(
'jest.config.js',
String.raw`
module.exports = {
name: 'test',
boolean: false,
preset: 'nrwl-preset',
"update-me": "hello",
alreadyExistingArray: ['something'],
alreadyExistingObject: {
nestedProperty: {
primitive: 'string',
childArray: ['value1', 'value2']
},
'nested-object': {
childArray: ['value1', 'value2']
}
},
numeric: 0
}
`
);
});
describe('inserting or updating an existing property', () => {
it('should be able to update an existing property with a primitive value ', () => {
addPropertyToJestConfig(host, 'jest.config.js', 'name', 'test-case');
let json = jestConfigObject(host, 'jest.config.js');
expect(json.name).toBe('test-case');
addPropertyToJestConfig(host, 'jest.config.js', 'boolean', true);
json = jestConfigObject(host, 'jest.config.js');
expect(json.boolean).toBe(true);
addPropertyToJestConfig(host, 'jest.config.js', 'numeric', 1);
json = jestConfigObject(host, 'jest.config.js');
expect(json.numeric).toBe(1);
});
it('should be able to insert a new property with a primitive value', () => {
addPropertyToJestConfig(host, 'jest.config.js', 'bail', 0);
const json = jestConfigObject(host, 'jest.config.js');
expect(json.bail).toBe(0);
});
it('it should be able to insert a new property with an array value', () => {
const arrayValue = ['value', 'value2'];
addPropertyToJestConfig(host, 'jest.config.js', 'myArrayProperty', [
'value',
'value2',
]);
const json = jestConfigObject(host, 'jest.config.js');
expect(json.myArrayProperty).toEqual(arrayValue);
});
it('should be able to insert a new property with an object value', () => {
const objectValue = {
'some-property': { config1: '1', config2: ['value1', 'value2'] },
};
addPropertyToJestConfig(
host,
'jest.config.js',
'myObjectProperty',
objectValue
);
const json = jestConfigObject(host, 'jest.config.js');
expect(json.myObjectProperty).toEqual(objectValue);
});
it('should be able to update an existing array', () => {
addPropertyToJestConfig(
host,
'jest.config.js',
'alreadyExistingArray',
'something new'
);
const json = jestConfigObject(host, 'jest.config.js');
expect(json.alreadyExistingArray).toEqual(['something', 'something new']);
});
it('should not add duplicate values in an existing array', () => {
addPropertyToJestConfig(
host,
'jest.config.js',
'alreadyExistingArray',
'something'
);
const json = jestConfigObject(host, 'jest.config.js');
expect(json.alreadyExistingArray).toEqual(['something']);
});
it('should be able to update an existing object', () => {
const newPropertyValue = ['my new object'];
addPropertyToJestConfig(
host,
'jest.config.js',
'alreadyExistingObject.something-new',
newPropertyValue
);
const json = jestConfigObject(host, 'jest.config.js');
expect(json.alreadyExistingObject['something-new']).toEqual(
newPropertyValue
);
});
it('should be able to update an existing array in a nested object', () => {
const newPropertyValue = 'new value';
addPropertyToJestConfig(
host,
'jest.config.js',
'alreadyExistingObject.nestedProperty.childArray',
newPropertyValue
);
const json = jestConfigObject(host, 'jest.config.js');
expect(json.alreadyExistingObject.nestedProperty.childArray).toEqual([
'value1',
'value2',
newPropertyValue,
]);
});
it('should be able to update an existing value in a nested object', () => {
const newPropertyValue = false;
addPropertyToJestConfig(
host,
'jest.config.js',
'alreadyExistingObject.nestedProperty.primitive',
newPropertyValue
);
const json = jestConfigObject(host, 'jest.config.js');
expect(json.alreadyExistingObject.nestedProperty.primitive).toEqual(
newPropertyValue
);
});
it('should be able to modify an object with a string identifier', () => {
addPropertyToJestConfig(
host,
'jest.config.js',
'something-here',
'newPropertyValue'
);
let json = jestConfigObject(host, 'jest.config.js');
expect(json['something-here']).toEqual('newPropertyValue');
addPropertyToJestConfig(host, 'jest.config.js', 'update-me', 'goodbye');
json = jestConfigObject(host, 'jest.config.js');
expect(json['update-me']).toEqual('goodbye');
});
describe('errors', () => {
it('should throw an error when trying to add a value to an already existing object without being dot delimited', () => {
expect(() => {
addPropertyToJestConfig(
host,
'jest.config.js',
'alreadyExistingObject',
'should fail'
);
}).toThrow();
});
it('should throw an error if the jest.config doesnt match module.exports = {} style', () => {
host.create(
'jest.unconventional.js',
String.raw`
jestObject = {
stuffhere: true
}
module.exports = jestObject;
`
);
expect(() => {
addPropertyToJestConfig(
host,
'jest.unconventional.js',
'stuffhere',
'should fail'
);
}).toThrow();
});
it('should throw if the provided config does not exist in the tree', () => {
expect(() => {
addPropertyToJestConfig(host, 'jest.doesnotexist.js', '', '');
}).toThrow();
});
});
});
describe('removing values', () => {
it('should remove single nested properties in the jest config, ', () => {
removePropertyFromJestConfig(
host,
'jest.config.js',
'alreadyExistingObject.nested-object.childArray'
);
const json = jestConfigObject(host, 'jest.config.js');
expect(
json['alreadyExistingObject']['nested-object']['childArray']
).toEqual(undefined);
});
it('should remove single properties', () => {
removePropertyFromJestConfig(host, 'jest.config.js', 'update-me');
const json = jestConfigObject(host, 'jest.config.js');
expect(json['update-me']).toEqual(undefined);
});
it('should remove a whole object', () => {
removePropertyFromJestConfig(
host,
'jest.config.js',
'alreadyExistingObject'
);
const json = jestConfigObject(host, 'jest.config.js');
expect(json['alreadyExistingObject']).toEqual(undefined);
});
});
});

View File

@ -0,0 +1,61 @@
import { Tree } from '@angular-devkit/schematics';
import { insert, RemoveChange } from '@nrwl/workspace';
import {
addOrUpdateProperty,
jestConfigObjectAst,
removeProperty,
} from './functions';
/**
* Add a property to the jest config
* @param host
* @param path - path to the jest config file
* @param propertyName - Property to update. Can be dot delimited to access deeply nested properties
* @param value
*/
export function addPropertyToJestConfig(
host: Tree,
path: string,
propertyName: string,
value: unknown
) {
const configObject = jestConfigObjectAst(host, path);
const properties = propertyName.split('.');
const changes = addOrUpdateProperty(
configObject,
properties,
JSON.stringify(value),
path
);
insert(host, path, changes);
}
/**
* Remove a property value from the jest config
* @param host
* @param path
* @param propertyName - Property to remove. Can be dot delimited to access deeply nested properties
*/
export function removePropertyFromJestConfig(
host: Tree,
path: string,
propertyName: string
) {
const configObject = jestConfigObjectAst(host, path);
const propertyAssignment = removeProperty(
configObject,
propertyName.split('.')
);
if (propertyAssignment) {
const file = host.read(path).toString('utf-8');
const commaNeeded = file[propertyAssignment.end] === ',';
insert(host, path, [
new RemoveChange(
path,
propertyAssignment.getStart(),
`${propertyAssignment.getText()}${commaNeeded ? ',' : ''}`
),
]);
}
}

View File

@ -48,6 +48,9 @@ export {
updateNxJsonInTree, updateNxJsonInTree,
addProjectToNxJsonInTree, addProjectToNxJsonInTree,
readNxJsonInTree, readNxJsonInTree,
InsertChange,
ReplaceChange,
RemoveChange,
} from './src/utils/ast-utils'; } from './src/utils/ast-utils';
export { export {

View File

@ -163,8 +163,8 @@ export class RemoveChange implements Change {
constructor( constructor(
public path: string, public path: string,
private pos: number, public pos: number,
private toRemove: string public toRemove: string
) { ) {
if (pos < 0) { if (pos < 0) {
throw new Error('Negative positions are invalid'); throw new Error('Negative positions are invalid');
@ -189,9 +189,9 @@ export class ReplaceChange implements Change {
constructor( constructor(
public path: string, public path: string,
private pos: number, public pos: number,
private oldText: string, public oldText: string,
private newText: string public newText: string
) { ) {
if (pos < 0) { if (pos < 0) {
throw new Error('Negative positions are invalid'); throw new Error('Negative positions are invalid');
@ -350,22 +350,25 @@ export function addGlobal(
} }
} }
export function insert(host: Tree, modulePath: string, changes: any[]) { export function insert(host: Tree, modulePath: string, changes: Change[]) {
if (changes.length < 1) { if (changes.length < 1) {
return; return;
} }
// sort changes so that the highest pos goes first
const orderedChanges = changes.sort((a, b) => b.order - a.order);
const recorder = host.beginUpdate(modulePath); const recorder = host.beginUpdate(modulePath);
for (const change of changes) { for (const change of orderedChanges) {
if (change.type === 'insert') { if (change instanceof InsertChange) {
recorder.insertLeft(change.pos, change.toAdd); recorder.insertLeft(change.pos, change.toAdd);
} else if (change.type === 'remove') { } else if (change instanceof RemoveChange) {
recorder.remove((<any>change).pos - 1, (<any>change).toRemove.length + 1); recorder.remove(change.pos - 1, change.toRemove.length + 1);
} else if (change instanceof ReplaceChange) {
recorder.remove(change.pos, change.oldText.length);
recorder.insertLeft(change.pos, change.newText);
} else if (change.type === 'noop') { } else if (change.type === 'noop') {
// do nothing // do nothing
} else if (change.type === 'replace') {
const action = <any>change;
recorder.remove(action.pos, action.oldText.length);
recorder.insertLeft(action.pos, action.newText);
} else { } else {
throw new Error(`Unexpected Change '${change.constructor.name}'`); throw new Error(`Unexpected Change '${change.constructor.name}'`);
} }