Skip to content
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

fix: ignore custom Jest configuration file and warn if one is found #26852

Merged
merged 1 commit into from Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 51 additions & 4 deletions packages/angular_devkit/build_angular/src/builders/jest/index.ts
Expand Up @@ -7,9 +7,10 @@
*/

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import { execFile as execFileCb } from 'child_process';
import * as path from 'path';
import { promisify } from 'util';
import { execFile as execFileCb } from 'node:child_process';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { promisify } from 'node:util';
import { colors } from '../../utils/color';
import { findTestFiles } from '../../utils/test-files';
import { buildApplicationInternal } from '../application';
Expand Down Expand Up @@ -54,8 +55,25 @@ export default createBuilder(
};
}

const [testFiles, customConfig] = await Promise.all([
findTestFiles(options.include, options.exclude, context.workspaceRoot),
findCustomJestConfig(context.workspaceRoot),
]);

// Warn if a custom Jest configuration is found. We won't use it, so if a developer is trying to use a custom config, this hopefully
// makes a better experience than silently ignoring the configuration.
// Ideally, this would be a hard error. However a Jest config could exist for testing other files in the workspace outside of Angular
// CLI, so we likely can't produce a hard error in this situation without an opt-out.
if (customConfig) {
context.logger.warn(
'A custom Jest config was found, but this is not supported by `@angular-devkit/build-angular:jest` and will be' +
` ignored: ${customConfig}. This is an experiment to see if completely abstracting away Jest's configuration is viable. Please` +
` consider if your use case can be met without directly modifying the Jest config. If this is a major obstacle for your use` +
` case, please post it in this issue so we can collect feedback and evaluate: https://github.com/angular/angular-cli/issues/25434.`,
);
}

// Build all the test files.
const testFiles = await findTestFiles(options.include, options.exclude, context.workspaceRoot);
const jestGlobal = path.join(__dirname, 'jest-global.mjs');
const initTestBed = path.join(__dirname, 'init-test-bed.mjs');
const buildResult = await build(context, {
Expand Down Expand Up @@ -85,6 +103,7 @@ export default createBuilder(
jest,

`--rootDir="${path.join(testOut, 'browser')}"`,
`--config=${path.join(__dirname, 'jest.config.mjs')}`,
alan-agius4 marked this conversation as resolved.
Show resolved Hide resolved
'--testEnvironment=jsdom',

// TODO(dgp1130): Enable cache once we have a mechanism for properly clearing / disabling it.
Expand Down Expand Up @@ -162,3 +181,31 @@ function resolveModule(module: string): string | undefined {
return undefined;
}
}

/** Returns whether or not the provided directory includes a Jest configuration file. */
async function findCustomJestConfig(dir: string): Promise<string | undefined> {
const entries = await fs.readdir(dir, { withFileTypes: true });

// Jest supports many file extensions (`js`, `ts`, `cjs`, `cts`, `json`, etc.) Just look
// for anything with that prefix.
const config = entries.find((entry) => entry.isFile() && entry.name.startsWith('jest.config.'));
if (config) {
return path.join(dir, config.name);
}

// Jest also supports a `jest` key in `package.json`, look for a config there.
const packageJsonPath = path.join(dir, 'package.json');
let packageJson: string | undefined;
try {
packageJson = await fs.readFile(packageJsonPath, 'utf8');
} catch {
return undefined; // No package.json, therefore no Jest configuration in it.
}

const json = JSON.parse(packageJson);
if ('jest' in json) {
return packageJsonPath;
}

return undefined;
}
@@ -0,0 +1,11 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

// Empty config file, everything is specified via CLI options right now.
// This file is used just so Jest doesn't accidentally inherit a custom user-specified Jest config.
export default {};
53 changes: 53 additions & 0 deletions tests/legacy-cli/e2e/tests/jest/custom-config.ts
@@ -0,0 +1,53 @@
import { deleteFile, writeFile } from '../../utils/fs';
import { applyJestBuilder } from '../../utils/jest';
import { ng } from '../../utils/process';
import { updateJsonFile } from '../../utils/project';

export default async function (): Promise<void> {
await applyJestBuilder();

{
// Users may incorrectly write a Jest config believing it to be used by Angular.
await writeFile(
'jest.config.mjs',
`
export default {
runner: 'does-not-exist',
};
`.trim(),
);

// Should not fail from the above (broken) configuration. Shouldn't use it at all.
const { stderr } = await ng('test');

// Should warn that a Jest configuration was found but not used.
if (!stderr.includes('A custom Jest config was found')) {
throw new Error(`No warning about custom Jest config:\nSTDERR:\n\n${stderr}`);
}
if (!stderr.includes('jest.config.mjs')) {
throw new Error(`Warning did not call out 'jest.config.mjs':\nSTDERR:\n\n${stderr}`);
}

await deleteFile('jest.config.mjs');
}

{
// Use `package.json` configuration instead of a `jest.config` file.
await updateJsonFile('package.json', (json) => {
json['jest'] = {
runner: 'does-not-exist',
};
});

// Should not fail from the above (broken) configuration. Shouldn't use it at all.
const { stderr } = await ng('test');

// Should warn that a Jest configuration was found but not used.
if (!stderr.includes('A custom Jest config was found')) {
throw new Error(`No warning about custom Jest config:\nSTDERR:\n\n${stderr}`);
}
if (!stderr.includes('package.json')) {
throw new Error(`Warning did not call out 'package.json':\nSTDERR:\n\n${stderr}`);
}
}
}