Caleb Ukle c7249db386
fix(testing): handle more complex projects for react component testing (#11725)
* fix(testing): use @nrwl/web:webpack utils to generate a more robust webpack config

fixes: #11372

* fix(testing): do not overwrite existing component test

* fix(testing): add component-test to cacheable operations

* chore(testing): address pr feedback
2022-08-30 16:42:42 +00:00

247 lines
7.2 KiB
TypeScript

import { nxBaseCypressPreset } from '@nrwl/cypress/plugins/cypress-preset';
import type { CypressExecutorOptions } from '@nrwl/cypress/src/executors/cypress/cypress.impl';
import {
ExecutorContext,
logger,
parseTargetString,
ProjectConfiguration,
ProjectGraph,
readCachedProjectGraph,
readNxJson,
readTargetOptions,
stripIndents,
Target,
TargetConfiguration,
workspaceRoot,
} from '@nrwl/devkit';
import type { WebWebpackExecutorOptions } from '@nrwl/web/src/executors/webpack/webpack.impl';
import { normalizeWebBuildOptions } from '@nrwl/web/src/utils/normalize';
import { getWebConfig } from '@nrwl/web/src/utils/web.config';
import { mapProjectGraphFiles } from '@nrwl/workspace/src/utils/runtime-lint-utils';
import { lstatSync } from 'fs';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
import { extname, relative } from 'path';
import { buildBaseWebpackConfig } from './webpack-fallback';
export interface ReactComponentTestingOptions {
/**
* the component testing target name.
* this is only when customized away from the default value of `component-test`
* @example 'component-test'
*/
ctTargetName: string;
}
/**
* React nx preset for Cypress Component Testing
*
* This preset contains the base configuration
* for your component tests that nx recommends.
* including a devServer that supports nx workspaces.
* you can easily extend this within your cypress config via spreading the preset
* @example
* export default defineConfig({
* component: {
* ...nxComponentTestingPreset(__dirname)
* // add your own config here
* }
* })
*
* @param pathToConfig will be used for loading project options and to construct the output paths for videos and screenshots
* @param options override options
*/
export function nxComponentTestingPreset(
pathToConfig: string,
options?: ReactComponentTestingOptions
) {
let webpackConfig;
try {
const graph = readCachedProjectGraph();
const { targets: ctTargets, name: ctProjectName } = getConfigByPath(
graph,
pathToConfig
);
const ctTargetName = options?.ctTargetName || 'component-test';
const ctConfigurationName = process.env.NX_CYPRESS_TARGET_CONFIGURATION;
const ctExecutorContext = createExecutorContext(
graph,
ctTargets,
ctProjectName,
ctTargetName,
ctConfigurationName
);
const ctExecutorOptions = readTargetOptions<CypressExecutorOptions>(
{
project: ctProjectName,
target: ctTargetName,
configuration: ctConfigurationName,
},
ctExecutorContext
);
const buildTarget = ctExecutorOptions.devServerTarget;
if (!buildTarget) {
throw new Error(
`Unable to find the 'devServerTarget' executor option in the '${ctTargetName}' target of the '${ctProjectName}' project`
);
}
webpackConfig = buildTargetWebpack(graph, buildTarget, ctProjectName);
} catch (e) {
logger.warn(
stripIndents`Unable to build a webpack config with the project graph.
Falling back to default webpack config.`
);
logger.warn(e);
webpackConfig = buildBaseWebpackConfig({
tsConfigPath: 'tsconfig.cy.json',
compiler: 'babel',
});
}
return {
...nxBaseCypressPreset(pathToConfig),
devServer: {
// cypress uses string union type,
// need to use const to prevent typing to string
framework: 'react',
bundler: 'webpack',
webpackConfig,
} as const,
};
}
/**
* apply the schema.json defaults from the @nrwl/web:webpack executor to the target options
*/
function withSchemaDefaults(
target: Target,
context: ExecutorContext
): WebWebpackExecutorOptions {
const options = readTargetOptions<WebWebpackExecutorOptions>(target, context);
options.compiler ??= 'babel';
options.deleteOutputPath ??= true;
options.vendorChunk ??= true;
options.commonChunk ??= true;
options.runtimeChunk ??= true;
options.sourceMap ??= true;
options.assets ??= [];
options.scripts ??= [];
options.styles ??= [];
options.budgets ??= [];
options.namedChunks ??= true;
options.outputHashing ??= 'none';
options.extractCss ??= true;
options.memoryLimit ??= 2048;
options.maxWorkers ??= 2;
options.fileReplacements ??= [];
options.buildLibsFromSource ??= true;
options.generateIndexHtml ??= true;
return options;
}
function buildTargetWebpack(
graph: ProjectGraph,
buildTarget: string,
componentTestingProjectName: string
) {
const parsed = parseTargetString(buildTarget);
const buildableProjectConfig = graph.nodes[parsed.project]?.data;
const ctProjectConfig = graph.nodes[componentTestingProjectName]?.data;
if (!buildableProjectConfig || !ctProjectConfig) {
throw new Error(stripIndents`Unable to load project configs from graph.
Using build target '${buildTarget}'
Has build config? ${!!buildableProjectConfig}
Has component config? ${!!ctProjectConfig}
`);
}
const options = normalizeWebBuildOptions(
withSchemaDefaults(
parsed,
createExecutorContext(
graph,
buildableProjectConfig.targets,
parsed.project,
parsed.target,
parsed.target
)
),
workspaceRoot,
buildableProjectConfig.sourceRoot!
);
const isScriptOptimizeOn =
typeof options.optimization === 'boolean'
? options.optimization
: options.optimization && options.optimization.scripts
? options.optimization.scripts
: false;
return getWebConfig(
workspaceRoot,
ctProjectConfig.root,
ctProjectConfig.sourceRoot,
options,
true,
isScriptOptimizeOn,
parsed.configuration
);
}
function getConfigByPath(
graph: ProjectGraph,
configPath: string
): ProjectConfiguration {
const configFileFromWorkspaceRoot = relative(workspaceRoot, configPath);
const normalizedPathFromWorkspaceRoot = lstatSync(configPath).isFile()
? configFileFromWorkspaceRoot.replace(extname(configPath), '')
: configFileFromWorkspaceRoot;
const mappedGraph = mapProjectGraphFiles(graph);
const componentTestingProjectName =
mappedGraph.allFiles[normalizedPathFromWorkspaceRoot];
if (
!componentTestingProjectName ||
!graph.nodes[componentTestingProjectName]?.data
) {
throw new Error(
stripIndents`Unable to find the project configuration that includes ${normalizedPathFromWorkspaceRoot}.
Found project name? ${componentTestingProjectName}.
Graph has data? ${!!graph.nodes[componentTestingProjectName]?.data}`
);
}
// make sure name is set since it can be undefined
graph.nodes[componentTestingProjectName].data.name ??=
componentTestingProjectName;
return graph.nodes[componentTestingProjectName].data;
}
function createExecutorContext(
graph: ProjectGraph,
targets: Record<string, TargetConfiguration>,
projectName: string,
targetName: string,
configurationName: string
): ExecutorContext {
const projectConfigs = readProjectsConfigurationFromProjectGraph(graph);
return {
cwd: process.cwd(),
projectGraph: graph,
target: targets[targetName],
targetName,
configurationName,
root: workspaceRoot,
isVerbose: false,
projectName,
workspace: {
...readNxJson(),
...projectConfigs,
},
};
}