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
This commit is contained in:
parent
7c8313504c
commit
c7249db386
@ -1207,7 +1207,7 @@
|
||||
"name": "cypress-component-configuration",
|
||||
"factory": "./src/generators/cypress-component-configuration/cypress-component-configuration#cypressComponentConfigGenerator",
|
||||
"schema": {
|
||||
"$schema": "http://json-schema.org/schema",
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"cli": "nx",
|
||||
"$id": "NxReactCypressComponentTestConfiguration",
|
||||
"title": "Add Cypress component testing",
|
||||
@ -1230,6 +1230,11 @@
|
||||
"$default": { "$source": "projectName" },
|
||||
"x-prompt": "What project should we add Cypress component testing to?"
|
||||
},
|
||||
"buildTarget": {
|
||||
"type": "string",
|
||||
"description": "A build target used to configure Cypress component testing in the format of `project:target[:configuration]`. The build target should be from a React app. If not provided we will try to infer it from your projects usage.",
|
||||
"pattern": "^[^:\\s]+:[^:\\s]+(:\\S+)?$"
|
||||
},
|
||||
"generateTests": {
|
||||
"type": "boolean",
|
||||
"description": "Generate default component tests for existing components in the project",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { newProject, runCLI, uniq } from '../../utils';
|
||||
import { createFile, newProject, runCLI, uniq, updateFile } from '../../utils';
|
||||
|
||||
describe('React Cypress Component Tests', () => {
|
||||
beforeAll(() => newProject());
|
||||
@ -17,14 +17,35 @@ describe('React Cypress Component Tests', () => {
|
||||
);
|
||||
}, 1000000);
|
||||
|
||||
it('should successfully test react app', () => {
|
||||
it('should successfully test react lib', () => {
|
||||
const libName = uniq('cy-react-lib');
|
||||
const appName = uniq('cy-react-app-target');
|
||||
runCLI(`generate @nrwl/react:app ${appName} --no-interactive`);
|
||||
runCLI(`generate @nrwl/react:lib ${libName} --component --no-interactive`);
|
||||
runCLI(
|
||||
`generate @nrwl/react:setup-tailwind --project=${libName} --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nrwl/react:component fancy-component --project=${libName} --no-interactive`
|
||||
);
|
||||
createFile(
|
||||
`libs/${libName}/src/styles.css`,
|
||||
`
|
||||
@tailwind components;
|
||||
@tailwind base;
|
||||
@tailwind utilities;
|
||||
`
|
||||
);
|
||||
updateFile(
|
||||
`libs/${libName}/src/lib/fancy-component/fancy-component.tsx`,
|
||||
(content) => {
|
||||
return `
|
||||
import '../../styles.css';
|
||||
${content}`;
|
||||
}
|
||||
);
|
||||
runCLI(
|
||||
`generate @nrwl/react:cypress-component-configuration --project=${libName} --generate-tests`
|
||||
`generate @nrwl/react:cypress-component-configuration --project=${libName} --build-target=${appName}:build --generate-tests`
|
||||
);
|
||||
expect(runCLI(`component-test ${libName} --no-watch`)).toContain(
|
||||
'All specs passed!'
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
// mock so we can test multiple versions
|
||||
jest.mock('@nrwl/cypress/src/utils/cypress-version');
|
||||
|
||||
// mock bc the nxE2EPreset uses fs for path normalization
|
||||
jest.mock('fs', () => {
|
||||
return {
|
||||
...jest.requireActual('fs'),
|
||||
lstatSync: jest.fn(() => ({
|
||||
isDirectory: jest.fn(() => true),
|
||||
})),
|
||||
};
|
||||
});
|
||||
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
|
||||
import {
|
||||
joinPathFragments,
|
||||
@ -13,6 +21,7 @@ import {
|
||||
writeJson,
|
||||
} from '@nrwl/devkit';
|
||||
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
|
||||
import { lstatSync } from 'fs';
|
||||
import { E2eMigrator } from './e2e.migrator';
|
||||
import { MigrationProjectConfiguration } from './types';
|
||||
|
||||
|
||||
@ -562,7 +562,7 @@ export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
|
||||
}
|
||||
|
||||
private updateCypress10ConfigFile(configFilePath: string): void {
|
||||
this.cypressPreset = nxE2EPreset(this.project.newRoot);
|
||||
this.cypressPreset = nxE2EPreset(configFilePath);
|
||||
|
||||
const fileContent = this.tree.read(configFilePath, 'utf-8');
|
||||
let sourceFile = tsquery.ast(fileContent);
|
||||
|
||||
@ -30,6 +30,12 @@
|
||||
"version": "12.8.0-beta.0",
|
||||
"description": "Remove Typescript Preprocessor Plugin",
|
||||
"factory": "./src/migrations/update-12-8-0/remove-typescript-plugin"
|
||||
},
|
||||
"update-cypress-configs-preset": {
|
||||
"cli": "nx",
|
||||
"version": "14.6.1-beta.0",
|
||||
"description": "Change Cypress e2e and component testing presets to use __filename instead of __dirname and include a devServerTarget for component testing.",
|
||||
"factory": "./src/migrations/update-14-6-1/update-cypress-configs-presets"
|
||||
}
|
||||
},
|
||||
"packageJsonUpdates": {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { workspaceRoot } from '@nrwl/devkit';
|
||||
import { join, relative } from 'path';
|
||||
import { dirname, join, relative } from 'path';
|
||||
import { lstatSync } from 'fs';
|
||||
|
||||
interface BaseCypressPreset {
|
||||
videosFolder: string;
|
||||
@ -9,8 +10,13 @@ interface BaseCypressPreset {
|
||||
}
|
||||
|
||||
export function nxBaseCypressPreset(pathToConfig: string): BaseCypressPreset {
|
||||
const projectPath = relative(workspaceRoot, pathToConfig);
|
||||
const offset = relative(pathToConfig, workspaceRoot);
|
||||
// prevent from placing path outside the root of the workspace
|
||||
// if they pass in a file or directory
|
||||
const normalizedPath = lstatSync(pathToConfig).isDirectory()
|
||||
? pathToConfig
|
||||
: dirname(pathToConfig);
|
||||
const projectPath = relative(workspaceRoot, normalizedPath);
|
||||
const offset = relative(normalizedPath, workspaceRoot);
|
||||
const videosFolder = join(offset, 'dist', 'cypress', projectPath, 'videos');
|
||||
const screenshotsFolder = join(
|
||||
offset,
|
||||
|
||||
@ -45,7 +45,8 @@ export default async function cypressExecutor(
|
||||
context: ExecutorContext
|
||||
) {
|
||||
options = normalizeOptions(options, context);
|
||||
|
||||
// this is used by cypress component testing presets to build the executor contexts with the correct configuration options.
|
||||
process.env.NX_CYPRESS_TARGET_CONFIGURATION = context.configurationName;
|
||||
let success;
|
||||
|
||||
for await (const baseUrl of startDevServer(options, context)) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
addProjectConfiguration,
|
||||
ProjectConfiguration,
|
||||
readJson,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateProjectConfiguration,
|
||||
@ -118,6 +119,19 @@ describe('Cypress Component Project', () => {
|
||||
expect(projectConfig.targets['component-test']).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should update cacheable operations', async () => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
await cypressComponentProject(tree, {
|
||||
project: 'cool-lib',
|
||||
skipFormat: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
readJson(tree, 'nx.json').tasksRunnerOptions.default.options
|
||||
.cacheableOperations
|
||||
).toEqual(expect.arrayContaining(['component-test']));
|
||||
});
|
||||
|
||||
it('should not error when rerunning on an existing project', async () => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
tree.write('libs/cool-lib/cypress.config.ts', '');
|
||||
|
||||
@ -7,7 +7,9 @@ import {
|
||||
ProjectConfiguration,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateJson,
|
||||
updateProjectConfiguration,
|
||||
NxJsonConfiguration,
|
||||
} from '@nrwl/devkit';
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
|
||||
@ -35,7 +37,7 @@ export async function cypressComponentProject(
|
||||
|
||||
addProjectFiles(tree, projectConfig, options);
|
||||
addTargetToProject(tree, projectConfig, options);
|
||||
|
||||
addToCacheableOperations(tree);
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
@ -87,3 +89,25 @@ function addTargetToProject(
|
||||
|
||||
updateProjectConfiguration(tree, options.project, projectConfig);
|
||||
}
|
||||
|
||||
export function addToCacheableOperations(tree: Tree) {
|
||||
updateJson(tree, 'nx.json', (json) => ({
|
||||
...json,
|
||||
tasksRunnerOptions: {
|
||||
...json.tasksRunnerOptions,
|
||||
default: {
|
||||
...json.tasksRunnerOptions?.default,
|
||||
options: {
|
||||
...json.tasksRunnerOptions?.default?.options,
|
||||
cacheableOperations: Array.from(
|
||||
new Set([
|
||||
...(json.tasksRunnerOptions?.default?.options
|
||||
?.cacheableOperations ?? []),
|
||||
'component-test',
|
||||
])
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
@ -0,0 +1,415 @@
|
||||
import { updateCypressConfigsPresets } from './update-cypress-configs-presets';
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
import {
|
||||
addProjectConfiguration,
|
||||
DependencyType,
|
||||
logger,
|
||||
ProjectGraph,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateProjectConfiguration,
|
||||
} from '@nrwl/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
|
||||
import { cypressProjectGenerator } from '../../generators/cypress-project/cypress-project';
|
||||
import { libraryGenerator } from '@nrwl/workspace';
|
||||
|
||||
let projectGraph: ProjectGraph;
|
||||
jest.mock('@nrwl/devkit', () => {
|
||||
return {
|
||||
...jest.requireActual('@nrwl/devkit'),
|
||||
createProjectGraphAsync: jest.fn().mockImplementation(() => projectGraph),
|
||||
};
|
||||
});
|
||||
jest.mock('../../utils/cypress-version');
|
||||
describe('updateComponentTestingConfig', () => {
|
||||
let tree: Tree;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof installedCypressVersion>
|
||||
> = installedCypressVersion as never;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
it('should update', async () => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
await setup(tree, { name: 'something' });
|
||||
await updateCypressConfigsPresets(tree);
|
||||
expect(
|
||||
tree.read('libs/something-lib/cypress.config.ts', 'utf-8')
|
||||
).toContain(
|
||||
`export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename),
|
||||
});
|
||||
`
|
||||
);
|
||||
expect(
|
||||
tree.read('libs/something-lib/cypress.config-two.ts', 'utf-8')
|
||||
).toContain(
|
||||
`export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename, { ctTargetName: 'ct' }),
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
expect(tree.read('apps/something-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toContain(`export default defineConfig({
|
||||
e2e: nxE2EPreset(__filename),
|
||||
});
|
||||
`);
|
||||
expect(tree.read('apps/something-e2e/cypress.storybook-config.ts', 'utf-8'))
|
||||
.toContain(`export default defineConfig({
|
||||
e2e: nxE2EStorybookPreset(__filename),
|
||||
});
|
||||
`);
|
||||
const libProjectConfig = readProjectConfiguration(tree, 'something-lib');
|
||||
expect(libProjectConfig.targets['component-test']).toEqual({
|
||||
executor: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'libs/something-lib/cypress.config.ts',
|
||||
testingType: 'component',
|
||||
devServerTarget: 'something-app:build',
|
||||
skipServe: true,
|
||||
},
|
||||
});
|
||||
expect(libProjectConfig.targets['ct']).toEqual({
|
||||
executor: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'libs/something-lib/cypress.config-two.ts',
|
||||
testingType: 'component',
|
||||
devServerTarget: 'something-app:build',
|
||||
skipServe: true,
|
||||
},
|
||||
configurations: {
|
||||
prod: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should list out projects when unable to update config', async () => {
|
||||
const loggerSpy = jest.spyOn(logger, 'warn');
|
||||
await setup(tree, { name: 'something' });
|
||||
projectGraph = {
|
||||
nodes: {},
|
||||
dependencies: {},
|
||||
};
|
||||
await updateCypressConfigsPresets(tree);
|
||||
|
||||
expect(loggerSpy.mock.calls).toEqual([
|
||||
[
|
||||
'Unable to find a build target to add to the component testing target in the following projects:',
|
||||
],
|
||||
['- something-lib'],
|
||||
[
|
||||
`You can manually add the 'devServerTarget' option to the
|
||||
component testing target to specify the build target to use.
|
||||
The build configuration should be using @nrwl/web:webpack as the executor.
|
||||
Usually this is a React app in your workspace.
|
||||
Component testing will fallback to a default configuration if one isn't provided,
|
||||
but might require modifications if your projects are more complex.`,
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle already updated config', async () => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
await setup(tree, { name: 'something' });
|
||||
|
||||
expect(async () => {
|
||||
await updateCypressConfigsPresets(tree);
|
||||
}).not.toThrow();
|
||||
expect(tree.read('libs/something-lib/cypress.config.ts', 'utf-8'))
|
||||
.toContain(`export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename),
|
||||
});
|
||||
`);
|
||||
expect(
|
||||
tree.read('libs/something-lib/cypress.config-two.ts', 'utf-8')
|
||||
).toContain(
|
||||
`export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename, { ctTargetName: 'ct' }),
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
expect(tree.read('apps/something-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toContain(`export default defineConfig({
|
||||
e2e: nxE2EPreset(__filename),
|
||||
});
|
||||
`);
|
||||
});
|
||||
it('should not update if using < v10', async () => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(9);
|
||||
await setup(tree, { name: 'something' });
|
||||
await updateCypressConfigsPresets(tree);
|
||||
expect(
|
||||
tree.read('libs/something-lib/cypress.config.ts', 'utf-8')
|
||||
).toContain(
|
||||
`export default defineConfig({
|
||||
component: nxComponentTestingPreset(__dirname),
|
||||
});
|
||||
`
|
||||
);
|
||||
expect(
|
||||
tree.read('libs/something-lib/cypress.config-two.ts', 'utf-8')
|
||||
).toContain(
|
||||
`export default defineConfig({
|
||||
component: nxComponentTestingPreset(__dirname),
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
expect(tree.read('apps/something-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toContain(`export default defineConfig({
|
||||
e2e: nxE2EPreset(__dirname),
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should be idempotent', async () => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
await setup(tree, { name: 'something' });
|
||||
await updateCypressConfigsPresets(tree);
|
||||
expect(
|
||||
tree.read('libs/something-lib/cypress.config.ts', 'utf-8')
|
||||
).toContain(
|
||||
`export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename),
|
||||
});
|
||||
`
|
||||
);
|
||||
expect(
|
||||
tree.read('libs/something-lib/cypress.config-two.ts', 'utf-8')
|
||||
).toContain(
|
||||
`export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename, { ctTargetName: 'ct' }),
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
expect(tree.read('apps/something-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toContain(`export default defineConfig({
|
||||
e2e: nxE2EPreset(__filename),
|
||||
});
|
||||
`);
|
||||
const libProjectConfig = readProjectConfiguration(tree, 'something-lib');
|
||||
expect(libProjectConfig.targets['component-test']).toEqual({
|
||||
executor: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'libs/something-lib/cypress.config.ts',
|
||||
testingType: 'component',
|
||||
devServerTarget: 'something-app:build',
|
||||
skipServe: true,
|
||||
},
|
||||
});
|
||||
expect(libProjectConfig.targets['ct']).toEqual({
|
||||
executor: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'libs/something-lib/cypress.config-two.ts',
|
||||
testingType: 'component',
|
||||
devServerTarget: 'something-app:build',
|
||||
skipServe: true,
|
||||
},
|
||||
configurations: {
|
||||
prod: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await updateCypressConfigsPresets(tree);
|
||||
expect(
|
||||
tree.read('libs/something-lib/cypress.config.ts', 'utf-8')
|
||||
).toContain(
|
||||
`export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename),
|
||||
});
|
||||
`
|
||||
);
|
||||
expect(
|
||||
tree.read('libs/something-lib/cypress.config-two.ts', 'utf-8')
|
||||
).toContain(
|
||||
`export default defineConfig({
|
||||
component: nxComponentTestingPreset(__filename, { ctTargetName: 'ct' }),
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
expect(tree.read('apps/something-e2e/cypress.config.ts', 'utf-8'))
|
||||
.toContain(`export default defineConfig({
|
||||
e2e: nxE2EPreset(__filename),
|
||||
});
|
||||
`);
|
||||
const libProjectConfig2 = readProjectConfiguration(tree, 'something-lib');
|
||||
expect(libProjectConfig2.targets['component-test']).toEqual({
|
||||
executor: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'libs/something-lib/cypress.config.ts',
|
||||
testingType: 'component',
|
||||
devServerTarget: 'something-app:build',
|
||||
skipServe: true,
|
||||
},
|
||||
});
|
||||
expect(libProjectConfig2.targets['ct']).toEqual({
|
||||
executor: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'libs/something-lib/cypress.config-two.ts',
|
||||
testingType: 'component',
|
||||
devServerTarget: 'something-app:build',
|
||||
skipServe: true,
|
||||
},
|
||||
configurations: {
|
||||
prod: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function setup(tree: Tree, options: { name: string }) {
|
||||
const appName = `${options.name}-app`;
|
||||
const libName = `${options.name}-lib`;
|
||||
const e2eName = `${options.name}-e2e`;
|
||||
tree.write(
|
||||
'apps/my-app/cypress.config.ts',
|
||||
`import { defineConfig } from 'cypress';
|
||||
import { nxComponentTestingPreset } from '@nrwl/cypress/plugins/component-testing';
|
||||
|
||||
export default defineConfig({
|
||||
component: nxComponentTestingPreset(__dirname),
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
addProjectConfiguration(tree, appName, {
|
||||
root: `apps/my-app`,
|
||||
sourceRoot: `apps/${appName}/src`,
|
||||
targets: {
|
||||
build: {
|
||||
executor: '@nrwl/web:webpack',
|
||||
outputs: ['{options.outputPath}'],
|
||||
options: {
|
||||
compiler: 'babel',
|
||||
outputPath: `dist/apps/${appName}`,
|
||||
index: `apps/${appName}/src/index.html`,
|
||||
baseHref: '/',
|
||||
main: `apps/${appName}/src/main.tsx`,
|
||||
polyfills: `apps/${appName}/src/polyfills.ts`,
|
||||
tsConfig: `apps/${appName}/tsconfig.app.json`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await cypressProjectGenerator(tree, { project: appName, name: e2eName });
|
||||
const e2eProjectConfig = readProjectConfiguration(tree, e2eName);
|
||||
e2eProjectConfig.targets['e2e'].configurations = {
|
||||
...e2eProjectConfig.targets['e2e'].configurations,
|
||||
sb: {
|
||||
cypressConfig: `apps/${e2eName}/cypress.storybook-config.ts`,
|
||||
},
|
||||
};
|
||||
updateProjectConfiguration(tree, e2eName, e2eProjectConfig);
|
||||
tree.write(
|
||||
`apps/${e2eName}/cypress.config.ts`,
|
||||
`import { defineConfig } from 'cypress';
|
||||
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: nxE2EPreset(__dirname),
|
||||
});
|
||||
`
|
||||
);
|
||||
tree.write(
|
||||
`apps/${e2eName}/cypress.storybook-config.ts`,
|
||||
`
|
||||
import { defineConfig } from 'cypress';
|
||||
import { nxE2EStorybookPreset } from '@nrwl/cypress/plugins/cypress-preset';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: nxE2EStorybookPreset(__dirname),
|
||||
});
|
||||
`
|
||||
);
|
||||
// lib
|
||||
await libraryGenerator(tree, { name: libName });
|
||||
const libProjectConfig = readProjectConfiguration(tree, libName);
|
||||
libProjectConfig.targets = {
|
||||
...libProjectConfig.targets,
|
||||
'component-test': {
|
||||
executor: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
testingType: 'component',
|
||||
cypressConfig: `libs/${libName}/cypress.config.ts`,
|
||||
},
|
||||
},
|
||||
ct: {
|
||||
executor: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
testingType: 'component',
|
||||
cypressConfig: `libs/${libName}/cypress.config-two.ts`,
|
||||
},
|
||||
configurations: {
|
||||
prod: {
|
||||
baseUrl: 'https://example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
updateProjectConfiguration(tree, libName, libProjectConfig);
|
||||
tree.write(
|
||||
`libs/${libName}/cypress.config.ts`,
|
||||
`import { defineConfig } from 'cypress';
|
||||
import { nxComponentTestingPreset } from '@nrwl/cypress/plugins/component-testing';
|
||||
|
||||
export default defineConfig({
|
||||
component: nxComponentTestingPreset(__dirname),
|
||||
});
|
||||
`
|
||||
);
|
||||
tree.write(
|
||||
`libs/${libName}/cypress.config-two.ts`,
|
||||
`import { defineConfig } from 'cypress';
|
||||
import { nxComponentTestingPreset } from '@nrwl/cypress/plugins/component-testing';
|
||||
|
||||
export default defineConfig({
|
||||
component: nxComponentTestingPreset(__dirname),
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
projectGraph = {
|
||||
nodes: {
|
||||
[appName]: {
|
||||
name: appName,
|
||||
type: 'app',
|
||||
data: {
|
||||
...readProjectConfiguration(tree, appName),
|
||||
},
|
||||
},
|
||||
[e2eName]: {
|
||||
name: e2eName,
|
||||
type: 'e2e',
|
||||
data: {
|
||||
...readProjectConfiguration(tree, e2eName),
|
||||
},
|
||||
},
|
||||
[libName]: {
|
||||
name: libName,
|
||||
type: 'lib',
|
||||
data: {
|
||||
...readProjectConfiguration(tree, libName),
|
||||
},
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
[appName]: [
|
||||
{ type: DependencyType.static, source: appName, target: libName },
|
||||
],
|
||||
[e2eName]: [
|
||||
{ type: DependencyType.implicit, source: e2eName, target: libName },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,168 @@
|
||||
import {
|
||||
logger,
|
||||
readProjectConfiguration,
|
||||
stripIndents,
|
||||
Tree,
|
||||
updateJson,
|
||||
updateProjectConfiguration,
|
||||
} from '@nrwl/devkit';
|
||||
import { forEachExecutorOptions } from '@nrwl/workspace/src/utilities/executor-options-utils';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import * as ts from 'typescript';
|
||||
import { CypressExecutorOptions } from '../../executors/cypress/cypress.impl';
|
||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||
import { findBuildConfig } from '../../utils/find-target-options';
|
||||
|
||||
export async function updateCypressConfigsPresets(tree: Tree) {
|
||||
if (installedCypressVersion() < 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
const projectsWithoutDevServerTarget = new Set<string>();
|
||||
const updateTasks = [];
|
||||
forEachExecutorOptions<CypressExecutorOptions>(
|
||||
tree,
|
||||
'@nrwl/cypress:cypress',
|
||||
(options, projectName, targetName, configName) => {
|
||||
if (options.cypressConfig && tree.exists(options.cypressConfig)) {
|
||||
updatePreset(tree, options, targetName);
|
||||
}
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, projectName);
|
||||
const testingType =
|
||||
options.testingType ||
|
||||
projectConfig.targets[targetName]?.options?.testingType;
|
||||
const devServerTarget =
|
||||
options.devServerTarget ||
|
||||
projectConfig.targets[targetName]?.options?.devServerTarget;
|
||||
|
||||
if (!devServerTarget && testingType === 'component') {
|
||||
updateTasks.push(
|
||||
addBuildTargetToConfig(
|
||||
tree,
|
||||
projectName,
|
||||
targetName,
|
||||
configName
|
||||
).then((didUpdate) => {
|
||||
if (!didUpdate) {
|
||||
projectsWithoutDevServerTarget.add(projectName);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (updateTasks.length > 0) {
|
||||
cacheComponentTestTarget(tree);
|
||||
}
|
||||
|
||||
await Promise.all(updateTasks);
|
||||
|
||||
if (projectsWithoutDevServerTarget.size > 0) {
|
||||
logger.warn(
|
||||
`Unable to find a build target to add to the component testing target in the following projects:`
|
||||
);
|
||||
logger.warn(`- ${Array.from(projectsWithoutDevServerTarget).join('\n- ')}`);
|
||||
logger.warn(stripIndents`
|
||||
You can manually add the 'devServerTarget' option to the
|
||||
component testing target to specify the build target to use.
|
||||
The build configuration should be using @nrwl/web:webpack as the executor.
|
||||
Usually this is a React app in your workspace.
|
||||
Component testing will fallback to a default configuration if one isn't provided,
|
||||
but might require modifications if your projects are more complex.
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreset(
|
||||
tree: Tree,
|
||||
options: CypressExecutorOptions,
|
||||
targetName: string | undefined
|
||||
) {
|
||||
let contents = tsquery.replace(
|
||||
tree.read(options.cypressConfig, 'utf-8'),
|
||||
'CallExpression',
|
||||
(node: ts.CallExpression) => {
|
||||
// technically someone could have both component and e2e in the same project.
|
||||
const expression = node.expression.getText();
|
||||
if (expression === 'nxE2EPreset') {
|
||||
return 'nxE2EPreset(__filename)';
|
||||
} else if (expression === 'nxE2EStorybookPreset') {
|
||||
return 'nxE2EStorybookPreset(__filename)';
|
||||
} else if (node.expression.getText() === 'nxComponentTestingPreset') {
|
||||
return targetName && targetName !== 'component-test' // the default
|
||||
? `nxComponentTestingPreset(__filename, { ctTargetName: '${targetName}' })`
|
||||
: 'nxComponentTestingPreset(__filename)';
|
||||
}
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
tree.write(options.cypressConfig, contents);
|
||||
}
|
||||
|
||||
async function addBuildTargetToConfig(
|
||||
tree: Tree,
|
||||
projectName: string,
|
||||
targetName: string,
|
||||
configName?: string
|
||||
): Promise<boolean> {
|
||||
const projectWithBuild = await findBuildConfig(tree, {
|
||||
project: projectName,
|
||||
validExecutorNames: new Set(['@nrwl/web:webpack']),
|
||||
});
|
||||
// didn't find the config so can't update. consumer should collect list of them and display a warning at the end
|
||||
// no reason to fail since the preset will fallback to a default config so should still keep working.
|
||||
if (!projectWithBuild?.target || !projectWithBuild?.config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, projectName);
|
||||
// if using a custom config and the devServerTarget default args
|
||||
// has a different target, then add it to the custom target config
|
||||
// otherwise add it to the default options
|
||||
if (
|
||||
configName &&
|
||||
projectWithBuild.target !==
|
||||
projectConfig.targets[targetName]?.options?.devServerTarget
|
||||
) {
|
||||
projectConfig.targets[targetName].configurations[configName] = {
|
||||
...projectConfig.targets[targetName].configurations[configName],
|
||||
devServerTarget: projectWithBuild.target,
|
||||
skipServe: true,
|
||||
};
|
||||
} else {
|
||||
projectConfig.targets[targetName].options = {
|
||||
...projectConfig.targets[targetName].options,
|
||||
devServerTarget: projectWithBuild.target,
|
||||
skipServe: true,
|
||||
};
|
||||
}
|
||||
|
||||
updateProjectConfiguration(tree, projectName, projectConfig);
|
||||
return true;
|
||||
}
|
||||
|
||||
function cacheComponentTestTarget(tree: Tree) {
|
||||
updateJson(tree, 'nx.json', (json) => ({
|
||||
...json,
|
||||
tasksRunnerOptions: {
|
||||
...json.tasksRunnerOptions,
|
||||
default: {
|
||||
...json.tasksRunnerOptions?.default,
|
||||
options: {
|
||||
...json.tasksRunnerOptions?.default?.options,
|
||||
cacheableOperations: Array.from(
|
||||
new Set([
|
||||
...(json.tasksRunnerOptions?.default?.options
|
||||
?.cacheableOperations ?? []),
|
||||
'component-test',
|
||||
])
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
export default updateCypressConfigsPresets;
|
||||
174
packages/cypress/src/utils/find-target-options.ts
Normal file
174
packages/cypress/src/utils/find-target-options.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import {
|
||||
createProjectGraphAsync,
|
||||
logger,
|
||||
parseTargetString,
|
||||
ProjectGraph,
|
||||
ProjectGraphDependency,
|
||||
readProjectConfiguration,
|
||||
stripIndents,
|
||||
TargetConfiguration,
|
||||
Tree,
|
||||
reverse,
|
||||
readTargetOptions,
|
||||
ExecutorContext,
|
||||
workspaceRoot,
|
||||
readNxJson,
|
||||
} from '@nrwl/devkit';
|
||||
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
|
||||
|
||||
interface FindTargetOptions {
|
||||
project: string;
|
||||
/**
|
||||
* contains buildable target such as react app or angular app
|
||||
* <project>:<target>[:<configuration>]
|
||||
*/
|
||||
buildTarget?: string;
|
||||
validExecutorNames: Set<string>;
|
||||
}
|
||||
|
||||
interface FoundTarget {
|
||||
config: TargetConfiguration;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export async function findBuildConfig(
|
||||
tree: Tree,
|
||||
options: FindTargetOptions
|
||||
): Promise<FoundTarget> {
|
||||
// attempt to use the provided target
|
||||
const graph = await createProjectGraphAsync();
|
||||
if (options.buildTarget) {
|
||||
return {
|
||||
target: options.buildTarget,
|
||||
config: findInTarget(tree, graph, options),
|
||||
};
|
||||
}
|
||||
// check to see if there is a valid config in the given project
|
||||
const selfProject = findTargetOptionsInProject(
|
||||
tree,
|
||||
graph,
|
||||
options.project,
|
||||
options.validExecutorNames
|
||||
);
|
||||
if (selfProject) {
|
||||
return selfProject;
|
||||
}
|
||||
|
||||
// attempt to find any projects with the valid config in the graph that consumes this project
|
||||
return await findInGraph(tree, graph, options);
|
||||
}
|
||||
|
||||
function findInTarget(
|
||||
tree: Tree,
|
||||
graph: ProjectGraph,
|
||||
options: FindTargetOptions
|
||||
): TargetConfiguration {
|
||||
const { project, target, configuration } = parseTargetString(
|
||||
options.buildTarget
|
||||
);
|
||||
const projectConfig = readProjectConfiguration(tree, project);
|
||||
const foundConfig =
|
||||
configuration || projectConfig?.targets?.[target]?.defaultConfiguration;
|
||||
|
||||
return readTargetOptions(
|
||||
{ project, target, configuration: foundConfig },
|
||||
createExecutorContext(
|
||||
graph,
|
||||
projectConfig.targets,
|
||||
project,
|
||||
target,
|
||||
foundConfig
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function findInGraph(
|
||||
tree: Tree,
|
||||
graph: ProjectGraph,
|
||||
options: FindTargetOptions
|
||||
): Promise<FoundTarget> {
|
||||
const parents = findParentsOfProject(graph, options.project);
|
||||
const potentialTargets = [];
|
||||
|
||||
for (const parent of parents) {
|
||||
const parentProject = findTargetOptionsInProject(
|
||||
tree,
|
||||
graph,
|
||||
parent.target,
|
||||
options.validExecutorNames
|
||||
);
|
||||
if (parentProject) {
|
||||
potentialTargets.push(parentProject);
|
||||
}
|
||||
}
|
||||
|
||||
if (potentialTargets.length > 1) {
|
||||
logger.warn(stripIndents`Multiple potential targets found for ${options.project}. Found ${potentialTargets.length}.
|
||||
Using ${potentialTargets[0].target}.
|
||||
To specify a different target use the --build-target flag.
|
||||
`);
|
||||
}
|
||||
return potentialTargets[0];
|
||||
}
|
||||
|
||||
function findParentsOfProject(
|
||||
graph: ProjectGraph,
|
||||
projectName: string
|
||||
): ProjectGraphDependency[] {
|
||||
const reversedGraph = reverse(graph);
|
||||
return reversedGraph.dependencies[projectName]
|
||||
? Object.values(reversedGraph.dependencies[projectName])
|
||||
: [];
|
||||
}
|
||||
|
||||
function findTargetOptionsInProject(
|
||||
tree: Tree,
|
||||
graph: ProjectGraph,
|
||||
projectName: string,
|
||||
includes: Set<string>
|
||||
): FoundTarget {
|
||||
const projectConfig = readProjectConfiguration(tree, projectName);
|
||||
|
||||
for (const targetName in projectConfig.targets) {
|
||||
const targetConfig = projectConfig.targets[targetName];
|
||||
if (includes.has(targetConfig.executor)) {
|
||||
return {
|
||||
target: `${projectName}:${targetName}`,
|
||||
config: readTargetOptions(
|
||||
{ project: projectName, target: targetName },
|
||||
createExecutorContext(
|
||||
graph,
|
||||
projectConfig.targets,
|
||||
projectName,
|
||||
targetName,
|
||||
null
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,7 +1,36 @@
|
||||
import { nxBaseCypressPreset } from '@nrwl/cypress/plugins/cypress-preset';
|
||||
import { getCSSModuleLocalIdent } from '@nrwl/web/src/utils/web.config';
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
||||
import type { Configuration } from 'webpack';
|
||||
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
|
||||
@ -18,9 +47,60 @@ import type { Configuration } from 'webpack';
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* @param pathToConfig will be used to construct the output paths for videos and screenshots
|
||||
* @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) {
|
||||
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: {
|
||||
@ -28,152 +108,139 @@ export function nxComponentTestingPreset(pathToConfig: string) {
|
||||
// need to use const to prevent typing to string
|
||||
framework: 'react',
|
||||
bundler: 'webpack',
|
||||
webpackConfig: buildBaseWebpackConfig({
|
||||
tsConfigPath: 'tsconfig.cy.json',
|
||||
compiler: 'babel',
|
||||
}),
|
||||
webpackConfig,
|
||||
} as const,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(caleb): use the webpack utils to build the config
|
||||
// can't seem to get css modules to play nice when using it 🤔
|
||||
function buildBaseWebpackConfig({
|
||||
tsConfigPath = 'tsconfig.cy.json',
|
||||
compiler = 'babel',
|
||||
}: {
|
||||
tsConfigPath: string;
|
||||
compiler: 'swc' | 'babel';
|
||||
}): Configuration {
|
||||
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
|
||||
const config: Configuration = {
|
||||
target: 'web',
|
||||
resolve: {
|
||||
extensions,
|
||||
plugins: [
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: tsConfigPath,
|
||||
extensions,
|
||||
}) as never,
|
||||
],
|
||||
},
|
||||
mode: 'development',
|
||||
devtool: false,
|
||||
output: {
|
||||
publicPath: '/',
|
||||
chunkFilename: '[name].bundle.js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(bmp|png|jpe?g|gif|webp|avif)$/,
|
||||
type: 'asset',
|
||||
parser: {
|
||||
dataUrlCondition: {
|
||||
maxSize: 10_000, // 10 kB
|
||||
},
|
||||
},
|
||||
},
|
||||
CSS_MODULES_LOADER,
|
||||
],
|
||||
},
|
||||
};
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if (compiler === 'swc') {
|
||||
config.module.rules.push({
|
||||
test: /\.([jt])sx?$/,
|
||||
loader: require.resolve('swc-loader'),
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
decorators: true,
|
||||
tsx: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'automatic',
|
||||
},
|
||||
},
|
||||
loose: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (compiler === 'babel') {
|
||||
config.module.rules.push({
|
||||
test: /\.(js|jsx|mjs|ts|tsx)$/,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
presets: [`@nrwl/react/babel`],
|
||||
rootMode: 'upward',
|
||||
babelrc: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return config;
|
||||
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;
|
||||
}
|
||||
|
||||
const loaderModulesOptions = {
|
||||
modules: {
|
||||
mode: 'local',
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
importLoaders: 1,
|
||||
};
|
||||
function buildTargetWebpack(
|
||||
graph: ProjectGraph,
|
||||
buildTarget: string,
|
||||
componentTestingProjectName: string
|
||||
) {
|
||||
const parsed = parseTargetString(buildTarget);
|
||||
|
||||
const commonLoaders = [
|
||||
{
|
||||
loader: require.resolve('style-loader'),
|
||||
},
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: loaderModulesOptions,
|
||||
},
|
||||
];
|
||||
const buildableProjectConfig = graph.nodes[parsed.project]?.data;
|
||||
const ctProjectConfig = graph.nodes[componentTestingProjectName]?.data;
|
||||
|
||||
const CSS_MODULES_LOADER = {
|
||||
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
|
||||
oneOf: [
|
||||
{
|
||||
test: /\.module\.css$/,
|
||||
use: commonLoaders,
|
||||
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,
|
||||
},
|
||||
{
|
||||
test: /\.module\.(scss|sass)$/,
|
||||
use: [
|
||||
...commonLoaders,
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
sassOptions: {
|
||||
fiber: false,
|
||||
precision: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.module\.less$/,
|
||||
use: [
|
||||
...commonLoaders,
|
||||
{
|
||||
loader: require.resolve('less-loader'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.module\.styl$/,
|
||||
use: [
|
||||
...commonLoaders,
|
||||
{
|
||||
loader: require.resolve('stylus-loader'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
143
packages/react/plugins/component-testing/webpack-fallback.ts
Normal file
143
packages/react/plugins/component-testing/webpack-fallback.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { getCSSModuleLocalIdent } from '@nrwl/web/src/utils/web.config';
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
||||
import { Configuration } from 'webpack';
|
||||
|
||||
export function buildBaseWebpackConfig({
|
||||
tsConfigPath = 'tsconfig.cy.json',
|
||||
compiler = 'babel',
|
||||
}: {
|
||||
tsConfigPath: string;
|
||||
compiler: 'swc' | 'babel';
|
||||
}): Configuration {
|
||||
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
|
||||
const config: Configuration = {
|
||||
target: 'web',
|
||||
resolve: {
|
||||
extensions,
|
||||
plugins: [
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: tsConfigPath,
|
||||
extensions,
|
||||
}) as never,
|
||||
],
|
||||
},
|
||||
mode: 'development',
|
||||
devtool: false,
|
||||
output: {
|
||||
publicPath: '/',
|
||||
chunkFilename: '[name].bundle.js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(bmp|png|jpe?g|gif|webp|avif)$/,
|
||||
type: 'asset',
|
||||
parser: {
|
||||
dataUrlCondition: {
|
||||
maxSize: 10_000, // 10 kB
|
||||
},
|
||||
},
|
||||
},
|
||||
CSS_MODULES_LOADER,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (compiler === 'swc') {
|
||||
config.module.rules.push({
|
||||
test: /\.([jt])sx?$/,
|
||||
loader: require.resolve('swc-loader'),
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
decorators: true,
|
||||
tsx: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'automatic',
|
||||
},
|
||||
},
|
||||
loose: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (compiler === 'babel') {
|
||||
config.module.rules.push({
|
||||
test: /\.(js|jsx|mjs|ts|tsx)$/,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
presets: [`@nrwl/react/babel`],
|
||||
rootMode: 'upward',
|
||||
babelrc: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
const loaderModulesOptions = {
|
||||
modules: {
|
||||
mode: 'local',
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
importLoaders: 1,
|
||||
};
|
||||
|
||||
const commonLoaders = [
|
||||
{
|
||||
loader: require.resolve('style-loader'),
|
||||
},
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: loaderModulesOptions,
|
||||
},
|
||||
];
|
||||
|
||||
const CSS_MODULES_LOADER = {
|
||||
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
|
||||
oneOf: [
|
||||
{
|
||||
test: /\.module\.css$/,
|
||||
use: commonLoaders,
|
||||
},
|
||||
{
|
||||
test: /\.module\.(scss|sass)$/,
|
||||
use: [
|
||||
...commonLoaders,
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
sassOptions: {
|
||||
fiber: false,
|
||||
precision: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.module\.less$/,
|
||||
use: [
|
||||
...commonLoaders,
|
||||
{
|
||||
loader: require.resolve('less-loader'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.module\.styl$/,
|
||||
use: [
|
||||
...commonLoaders,
|
||||
{
|
||||
loader: require.resolve('stylus-loader'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -55,6 +55,28 @@ describe(componentTestGenerator.name, () => {
|
||||
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.js')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not overwrite exising component test', async () => {
|
||||
mockedAssertMinimumCypressVersion.mockReturnValue();
|
||||
await libraryGenerator(tree, {
|
||||
linter: Linter.EsLint,
|
||||
name: 'some-lib',
|
||||
skipFormat: true,
|
||||
skipTsConfig: false,
|
||||
style: 'scss',
|
||||
unitTestRunner: 'none',
|
||||
component: true,
|
||||
});
|
||||
tree.write('libs/some-lib/src/lib/some-lib.cy.tsx', 'existing content');
|
||||
componentTestGenerator(tree, {
|
||||
project: 'some-lib',
|
||||
componentPath: 'lib/some-lib.tsx',
|
||||
});
|
||||
|
||||
expect(tree.read('libs/some-lib/src/lib/some-lib.cy.tsx', 'utf-8')).toEqual(
|
||||
'existing content'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw if path is invalid', async () => {
|
||||
mockedAssertMinimumCypressVersion.mockReturnValue();
|
||||
await libraryGenerator(tree, {
|
||||
|
||||
@ -50,6 +50,11 @@ function generateSpecsForComponents(tree: Tree, filePath: string) {
|
||||
const componentDir = dirname(filePath);
|
||||
const ext = extname(filePath);
|
||||
const fileName = basename(filePath, ext);
|
||||
|
||||
if (tree.exists(joinPathFragments(componentDir, `${fileName}.cy${ext}`))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultExport = getComponentNode(sourceFile);
|
||||
|
||||
if (cmpNodes?.length) {
|
||||
|
||||
@ -1,11 +1,25 @@
|
||||
import { assertMinimumCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
|
||||
import { readJson, Tree } from '@nrwl/devkit';
|
||||
import {
|
||||
DependencyType,
|
||||
ProjectGraph,
|
||||
readJson,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
} from '@nrwl/devkit';
|
||||
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
|
||||
import { Linter } from '@nrwl/linter';
|
||||
import componentGenerator from '../component/component';
|
||||
import libraryGenerator from '../library/library';
|
||||
import { applicationGenerator } from '../application/application';
|
||||
import { componentGenerator } from '../component/component';
|
||||
import { libraryGenerator } from '../library/library';
|
||||
import { cypressComponentConfigGenerator } from './cypress-component-configuration';
|
||||
|
||||
let projectGraph: ProjectGraph;
|
||||
jest.mock('@nrwl/devkit', () => ({
|
||||
...jest.requireActual<any>('@nrwl/devkit'),
|
||||
createProjectGraphAsync: jest
|
||||
.fn()
|
||||
.mockImplementation(async () => projectGraph),
|
||||
}));
|
||||
jest.mock('@nrwl/cypress/src/utils/cypress-version');
|
||||
describe('React:CypressComponentTestConfiguration', () => {
|
||||
let tree: Tree;
|
||||
@ -15,9 +29,17 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyV1Workspace();
|
||||
});
|
||||
it('should generate cypress component test config', async () => {
|
||||
it('should generate cypress component test config with --build-target', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
skipFormat: true,
|
||||
style: 'scss',
|
||||
unitTestRunner: 'none',
|
||||
name: 'my-app',
|
||||
});
|
||||
await libraryGenerator(tree, {
|
||||
linter: Linter.EsLint,
|
||||
name: 'some-lib',
|
||||
@ -27,17 +49,43 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
unitTestRunner: 'none',
|
||||
component: true,
|
||||
});
|
||||
|
||||
// --build-target still needs to build the graph in order for readTargetOptions to work
|
||||
projectGraph = {
|
||||
nodes: {
|
||||
'my-app': {
|
||||
name: 'my-app',
|
||||
type: 'app',
|
||||
data: {
|
||||
...readProjectConfiguration(tree, 'my-app'),
|
||||
},
|
||||
},
|
||||
'some-lib': {
|
||||
name: 'some-lib',
|
||||
type: 'lib',
|
||||
data: {
|
||||
...readProjectConfiguration(tree, 'some-lib'),
|
||||
},
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
'my-app': [
|
||||
{ type: DependencyType.static, source: 'my-app', target: 'some-lib' },
|
||||
],
|
||||
},
|
||||
};
|
||||
await cypressComponentConfigGenerator(tree, {
|
||||
project: 'some-lib',
|
||||
generateTests: false,
|
||||
buildTarget: 'my-app:build',
|
||||
});
|
||||
|
||||
const config = tree.read('libs/some-lib/cypress.config.ts', 'utf-8');
|
||||
expect(config).toContain(
|
||||
"import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing"
|
||||
);
|
||||
expect(config).toContain('component: nxComponentTestingPreset(__dirname),');
|
||||
expect(config).toContain(
|
||||
'component: nxComponentTestingPreset(__filename),'
|
||||
);
|
||||
|
||||
const cyTsConfig = readJson(tree, 'libs/some-lib/tsconfig.cy.json');
|
||||
expect(cyTsConfig.include).toEqual([
|
||||
@ -63,11 +111,123 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
expect(baseTsConfig.references).toEqual(
|
||||
expect.arrayContaining([{ path: './tsconfig.cy.json' }])
|
||||
);
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'some-lib').targets['component-test']
|
||||
).toEqual({
|
||||
executor: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'libs/some-lib/cypress.config.ts',
|
||||
devServerTarget: 'my-app:build',
|
||||
skipServe: true,
|
||||
testingType: 'component',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate cypress component test config with project graph', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
skipFormat: true,
|
||||
style: 'scss',
|
||||
unitTestRunner: 'none',
|
||||
name: 'my-app',
|
||||
});
|
||||
await libraryGenerator(tree, {
|
||||
linter: Linter.EsLint,
|
||||
name: 'some-lib',
|
||||
skipFormat: true,
|
||||
skipTsConfig: false,
|
||||
style: 'scss',
|
||||
unitTestRunner: 'none',
|
||||
component: true,
|
||||
});
|
||||
|
||||
projectGraph = {
|
||||
nodes: {
|
||||
'my-app': {
|
||||
name: 'my-app',
|
||||
type: 'app',
|
||||
data: {
|
||||
...readProjectConfiguration(tree, 'my-app'),
|
||||
},
|
||||
},
|
||||
'some-lib': {
|
||||
name: 'some-lib',
|
||||
type: 'lib',
|
||||
data: {
|
||||
...readProjectConfiguration(tree, 'some-lib'),
|
||||
},
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
'my-app': [
|
||||
{ type: DependencyType.static, source: 'my-app', target: 'some-lib' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await cypressComponentConfigGenerator(tree, {
|
||||
project: 'some-lib',
|
||||
generateTests: false,
|
||||
});
|
||||
|
||||
const config = tree.read('libs/some-lib/cypress.config.ts', 'utf-8');
|
||||
expect(config).toContain(
|
||||
"import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing"
|
||||
);
|
||||
expect(config).toContain(
|
||||
'component: nxComponentTestingPreset(__filename),'
|
||||
);
|
||||
|
||||
const cyTsConfig = readJson(tree, 'libs/some-lib/tsconfig.cy.json');
|
||||
expect(cyTsConfig.include).toEqual([
|
||||
'cypress.config.ts',
|
||||
'**/*.cy.ts',
|
||||
'**/*.cy.tsx',
|
||||
'**/*.cy.js',
|
||||
'**/*.cy.jsx',
|
||||
'**/*.d.ts',
|
||||
]);
|
||||
const libTsConfig = readJson(tree, 'libs/some-lib/tsconfig.lib.json');
|
||||
expect(libTsConfig.exclude).toEqual(
|
||||
expect.arrayContaining([
|
||||
'cypress/**/*',
|
||||
'cypress.config.ts',
|
||||
'**/*.cy.ts',
|
||||
'**/*.cy.js',
|
||||
'**/*.cy.tsx',
|
||||
'**/*.cy.jsx',
|
||||
])
|
||||
);
|
||||
const baseTsConfig = readJson(tree, 'libs/some-lib/tsconfig.json');
|
||||
expect(baseTsConfig.references).toEqual(
|
||||
expect.arrayContaining([{ path: './tsconfig.cy.json' }])
|
||||
);
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'some-lib').targets['component-test']
|
||||
).toEqual({
|
||||
executor: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'libs/some-lib/cypress.config.ts',
|
||||
devServerTarget: 'my-app:build',
|
||||
skipServe: true,
|
||||
testingType: 'component',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate tests for existing tsx components', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
skipFormat: true,
|
||||
style: 'scss',
|
||||
unitTestRunner: 'none',
|
||||
name: 'my-app',
|
||||
});
|
||||
await libraryGenerator(tree, {
|
||||
linter: Linter.EsLint,
|
||||
name: 'some-lib',
|
||||
@ -86,6 +246,7 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
await cypressComponentConfigGenerator(tree, {
|
||||
project: 'some-lib',
|
||||
generateTests: true,
|
||||
buildTarget: 'my-app:build',
|
||||
});
|
||||
|
||||
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
|
||||
@ -106,7 +267,14 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
});
|
||||
it('should generate tests for existing js components', async () => {
|
||||
mockedAssertCypressVersion.mockReturnValue();
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
skipFormat: true,
|
||||
style: 'scss',
|
||||
unitTestRunner: 'none',
|
||||
name: 'my-app',
|
||||
});
|
||||
await libraryGenerator(tree, {
|
||||
linter: Linter.EsLint,
|
||||
name: 'some-lib',
|
||||
@ -133,6 +301,7 @@ describe('React:CypressComponentTestConfiguration', () => {
|
||||
await cypressComponentConfigGenerator(tree, {
|
||||
project: 'some-lib',
|
||||
generateTests: true,
|
||||
buildTarget: 'my-app:build',
|
||||
});
|
||||
|
||||
expect(tree.exists('libs/some-lib/src/lib/some-cmp.cy.js')).toBeTruthy();
|
||||
|
||||
@ -1,21 +1,8 @@
|
||||
import { cypressComponentProject } from '@nrwl/cypress';
|
||||
import {
|
||||
formatFiles,
|
||||
generateFiles,
|
||||
joinPathFragments,
|
||||
ProjectConfiguration,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateJson,
|
||||
visitNotIgnoredFiles,
|
||||
} from '@nrwl/devkit';
|
||||
import * as ts from 'typescript';
|
||||
import { getComponentNode } from '../../utils/ast-utils';
|
||||
import componentTestGenerator from '../component-test/component-test';
|
||||
import { CypressComponentConfigurationSchema } from './schema';
|
||||
|
||||
const allowedFileExt = new RegExp(/\.[jt]sx?/g);
|
||||
const isSpecFile = new RegExp(/(spec|test)\./g);
|
||||
import { formatFiles, readProjectConfiguration, Tree } from '@nrwl/devkit';
|
||||
import { addFiles } from './lib/add-files';
|
||||
import { updateProjectConfig, updateTsConfig } from './lib/update-configs';
|
||||
import { CypressComponentConfigurationSchema } from './schema.d';
|
||||
|
||||
/**
|
||||
* This is for using cypresses own Component testing, if you want to use test
|
||||
@ -32,6 +19,7 @@ export async function cypressComponentConfigGenerator(
|
||||
skipFormat: true,
|
||||
});
|
||||
|
||||
await updateProjectConfig(tree, options);
|
||||
addFiles(tree, projectConfig, options);
|
||||
updateTsConfig(tree, projectConfig);
|
||||
if (options.skipFormat) {
|
||||
@ -43,110 +31,4 @@ export async function cypressComponentConfigGenerator(
|
||||
};
|
||||
}
|
||||
|
||||
function addFiles(
|
||||
tree: Tree,
|
||||
projectConfig: ProjectConfiguration,
|
||||
options: CypressComponentConfigurationSchema
|
||||
) {
|
||||
const cypressConfigPath = joinPathFragments(
|
||||
projectConfig.root,
|
||||
'cypress.config.ts'
|
||||
);
|
||||
if (tree.exists(cypressConfigPath)) {
|
||||
tree.delete(cypressConfigPath);
|
||||
}
|
||||
|
||||
generateFiles(
|
||||
tree,
|
||||
joinPathFragments(__dirname, 'files'),
|
||||
projectConfig.root,
|
||||
{
|
||||
tpl: '',
|
||||
}
|
||||
);
|
||||
|
||||
if (options.generateTests) {
|
||||
visitNotIgnoredFiles(tree, projectConfig.sourceRoot, (filePath) => {
|
||||
if (isComponent(tree, filePath)) {
|
||||
componentTestGenerator(tree, {
|
||||
project: options.project,
|
||||
componentPath: filePath,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateTsConfig(tree: Tree, projectConfig: ProjectConfiguration) {
|
||||
const tsConfigPath = joinPathFragments(
|
||||
projectConfig.root,
|
||||
projectConfig.projectType === 'library'
|
||||
? 'tsconfig.lib.json'
|
||||
: 'tsconfig.app.json'
|
||||
);
|
||||
if (tree.exists(tsConfigPath)) {
|
||||
updateJson(tree, tsConfigPath, (json) => {
|
||||
const excluded = new Set([
|
||||
...(json.exclude || []),
|
||||
'cypress/**/*',
|
||||
'cypress.config.ts',
|
||||
'**/*.cy.ts',
|
||||
'**/*.cy.js',
|
||||
'**/*.cy.tsx',
|
||||
'**/*.cy.jsx',
|
||||
]);
|
||||
|
||||
json.exclude = Array.from(excluded);
|
||||
return json;
|
||||
});
|
||||
}
|
||||
|
||||
const projectBaseTsConfig = joinPathFragments(
|
||||
projectConfig.root,
|
||||
'tsconfig.json'
|
||||
);
|
||||
if (tree.exists(projectBaseTsConfig)) {
|
||||
updateJson(tree, projectBaseTsConfig, (json) => {
|
||||
if (json.references) {
|
||||
const hasCyTsConfig = json.references.some(
|
||||
(r) => r.path === './tsconfig.cy.json'
|
||||
);
|
||||
if (!hasCyTsConfig) {
|
||||
json.references.push({ path: './tsconfig.cy.json' });
|
||||
}
|
||||
} else {
|
||||
const excluded = new Set([
|
||||
...(json.exclude || []),
|
||||
'cypress/**/*',
|
||||
'cypress.config.ts',
|
||||
'**/*.cy.ts',
|
||||
'**/*.cy.js',
|
||||
'**/*.cy.tsx',
|
||||
'**/*.cy.jsx',
|
||||
]);
|
||||
|
||||
json.exclude = Array.from(excluded);
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isComponent(tree: Tree, filePath: string): boolean {
|
||||
if (isSpecFile.test(filePath) || !allowedFileExt.test(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = tree.read(filePath, 'utf-8');
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
const cmpDeclaration = getComponentNode(sourceFile);
|
||||
return !!cmpDeclaration;
|
||||
}
|
||||
|
||||
export default cypressComponentConfigGenerator;
|
||||
|
||||
@ -2,5 +2,5 @@ import { defineConfig } from 'cypress';
|
||||
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
|
||||
|
||||
export default defineConfig({
|
||||
component: nxComponentTestingPreset(__dirname),
|
||||
component: nxComponentTestingPreset(__filename),
|
||||
});
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import {
|
||||
generateFiles,
|
||||
joinPathFragments,
|
||||
ProjectConfiguration,
|
||||
Tree,
|
||||
visitNotIgnoredFiles,
|
||||
} from '@nrwl/devkit';
|
||||
import * as ts from 'typescript';
|
||||
import { getComponentNode } from '../../../utils/ast-utils';
|
||||
import { componentTestGenerator } from '../../component-test/component-test';
|
||||
import { CypressComponentConfigurationSchema } from '../schema';
|
||||
|
||||
const allowedFileExt = new RegExp(/\.[jt]sx?/g);
|
||||
const isSpecFile = new RegExp(/(spec|test)\./g);
|
||||
|
||||
export function addFiles(
|
||||
tree: Tree,
|
||||
projectConfig: ProjectConfiguration,
|
||||
options: CypressComponentConfigurationSchema
|
||||
) {
|
||||
const cypressConfigPath = joinPathFragments(
|
||||
projectConfig.root,
|
||||
'cypress.config.ts'
|
||||
);
|
||||
if (tree.exists(cypressConfigPath)) {
|
||||
tree.delete(cypressConfigPath);
|
||||
}
|
||||
|
||||
generateFiles(
|
||||
tree,
|
||||
joinPathFragments(__dirname, '..', 'files'),
|
||||
projectConfig.root,
|
||||
{
|
||||
tpl: '',
|
||||
}
|
||||
);
|
||||
|
||||
if (options.generateTests) {
|
||||
visitNotIgnoredFiles(tree, projectConfig.sourceRoot, (filePath) => {
|
||||
if (isComponent(tree, filePath)) {
|
||||
componentTestGenerator(tree, {
|
||||
project: options.project,
|
||||
componentPath: filePath,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isComponent(tree: Tree, filePath: string): boolean {
|
||||
if (isSpecFile.test(filePath) || !allowedFileExt.test(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = tree.read(filePath, 'utf-8');
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
const cmpDeclaration = getComponentNode(sourceFile);
|
||||
return !!cmpDeclaration;
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
import { findBuildConfig } from '@nrwl/cypress/src/utils/find-target-options';
|
||||
import {
|
||||
joinPathFragments,
|
||||
ProjectConfiguration,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateJson,
|
||||
updateProjectConfiguration,
|
||||
} from '@nrwl/devkit';
|
||||
import { CypressComponentConfigurationSchema } from '../schema';
|
||||
|
||||
export function updateTsConfig(
|
||||
tree: Tree,
|
||||
projectConfig: ProjectConfiguration
|
||||
) {
|
||||
const tsConfigPath = joinPathFragments(
|
||||
projectConfig.root,
|
||||
projectConfig.projectType === 'library'
|
||||
? 'tsconfig.lib.json'
|
||||
: 'tsconfig.app.json'
|
||||
);
|
||||
if (tree.exists(tsConfigPath)) {
|
||||
updateJson(tree, tsConfigPath, (json) => {
|
||||
const excluded = new Set([
|
||||
...(json.exclude || []),
|
||||
'cypress/**/*',
|
||||
'cypress.config.ts',
|
||||
'**/*.cy.ts',
|
||||
'**/*.cy.js',
|
||||
'**/*.cy.tsx',
|
||||
'**/*.cy.jsx',
|
||||
]);
|
||||
|
||||
json.exclude = Array.from(excluded);
|
||||
return json;
|
||||
});
|
||||
}
|
||||
|
||||
const projectBaseTsConfig = joinPathFragments(
|
||||
projectConfig.root,
|
||||
'tsconfig.json'
|
||||
);
|
||||
if (tree.exists(projectBaseTsConfig)) {
|
||||
updateJson(tree, projectBaseTsConfig, (json) => {
|
||||
if (json.references) {
|
||||
const hasCyTsConfig = json.references.some(
|
||||
(r) => r.path === './tsconfig.cy.json'
|
||||
);
|
||||
if (!hasCyTsConfig) {
|
||||
json.references.push({ path: './tsconfig.cy.json' });
|
||||
}
|
||||
} else {
|
||||
const excluded = new Set([
|
||||
...(json.exclude || []),
|
||||
'cypress/**/*',
|
||||
'cypress.config.ts',
|
||||
'**/*.cy.ts',
|
||||
'**/*.cy.js',
|
||||
'**/*.cy.tsx',
|
||||
'**/*.cy.jsx',
|
||||
]);
|
||||
|
||||
json.exclude = Array.from(excluded);
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProjectConfig(
|
||||
tree: Tree,
|
||||
options: CypressComponentConfigurationSchema
|
||||
) {
|
||||
const found = await findBuildConfig(tree, {
|
||||
project: options.project,
|
||||
buildTarget: options.buildTarget,
|
||||
validExecutorNames: new Set<string>(['@nrwl/web:webpack']),
|
||||
});
|
||||
|
||||
assetValidConfig(found.config);
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||
projectConfig.targets['component-test'].options = {
|
||||
...projectConfig.targets['component-test'].options,
|
||||
devServerTarget: found.target,
|
||||
skipServe: true,
|
||||
};
|
||||
updateProjectConfiguration(tree, options.project, projectConfig);
|
||||
}
|
||||
|
||||
function assetValidConfig(config: unknown) {
|
||||
if (!config) {
|
||||
throw new Error(
|
||||
'Unable to find a valid build configuration. Try passing in a target for a React app. --build-target=<project>:<target>[:<configuration>]'
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,4 +2,5 @@ export interface CypressComponentConfigurationSchema {
|
||||
project: string;
|
||||
generateTests: boolean;
|
||||
skipFormat?: boolean;
|
||||
buildTarget?: string;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema",
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"cli": "nx",
|
||||
"$id": "NxReactCypressComponentTestConfiguration",
|
||||
"title": "Add Cypress component testing",
|
||||
@ -24,6 +24,11 @@
|
||||
},
|
||||
"x-prompt": "What project should we add Cypress component testing to?"
|
||||
},
|
||||
"buildTarget": {
|
||||
"type": "string",
|
||||
"description": "A build target used to configure Cypress component testing in the format of `project:target[:configuration]`. The build target should be from a React app. If not provided we will try to infer it from your projects usage.",
|
||||
"pattern": "^[^:\\s]+:[^:\\s]+(:\\S+)?$"
|
||||
},
|
||||
"generateTests": {
|
||||
"type": "boolean",
|
||||
"description": "Generate default component tests for existing components in the project",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user