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",
|
"name": "cypress-component-configuration",
|
||||||
"factory": "./src/generators/cypress-component-configuration/cypress-component-configuration#cypressComponentConfigGenerator",
|
"factory": "./src/generators/cypress-component-configuration/cypress-component-configuration#cypressComponentConfigGenerator",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$schema": "http://json-schema.org/schema",
|
"$schema": "https://json-schema.org/schema",
|
||||||
"cli": "nx",
|
"cli": "nx",
|
||||||
"$id": "NxReactCypressComponentTestConfiguration",
|
"$id": "NxReactCypressComponentTestConfiguration",
|
||||||
"title": "Add Cypress component testing",
|
"title": "Add Cypress component testing",
|
||||||
@ -1230,6 +1230,11 @@
|
|||||||
"$default": { "$source": "projectName" },
|
"$default": { "$source": "projectName" },
|
||||||
"x-prompt": "What project should we add Cypress component testing to?"
|
"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": {
|
"generateTests": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Generate default component tests for existing components in the project",
|
"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', () => {
|
describe('React Cypress Component Tests', () => {
|
||||||
beforeAll(() => newProject());
|
beforeAll(() => newProject());
|
||||||
@ -17,14 +17,35 @@ describe('React Cypress Component Tests', () => {
|
|||||||
);
|
);
|
||||||
}, 1000000);
|
}, 1000000);
|
||||||
|
|
||||||
it('should successfully test react app', () => {
|
it('should successfully test react lib', () => {
|
||||||
const libName = uniq('cy-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:lib ${libName} --component --no-interactive`);
|
||||||
|
runCLI(
|
||||||
|
`generate @nrwl/react:setup-tailwind --project=${libName} --no-interactive`
|
||||||
|
);
|
||||||
runCLI(
|
runCLI(
|
||||||
`generate @nrwl/react:component fancy-component --project=${libName} --no-interactive`
|
`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(
|
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(
|
expect(runCLI(`component-test ${libName} --no-watch`)).toContain(
|
||||||
'All specs passed!'
|
'All specs passed!'
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
// mock so we can test multiple versions
|
// mock so we can test multiple versions
|
||||||
jest.mock('@nrwl/cypress/src/utils/cypress-version');
|
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 { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
|
||||||
import {
|
import {
|
||||||
joinPathFragments,
|
joinPathFragments,
|
||||||
@ -13,6 +21,7 @@ import {
|
|||||||
writeJson,
|
writeJson,
|
||||||
} from '@nrwl/devkit';
|
} from '@nrwl/devkit';
|
||||||
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
|
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
|
||||||
|
import { lstatSync } from 'fs';
|
||||||
import { E2eMigrator } from './e2e.migrator';
|
import { E2eMigrator } from './e2e.migrator';
|
||||||
import { MigrationProjectConfiguration } from './types';
|
import { MigrationProjectConfiguration } from './types';
|
||||||
|
|
||||||
|
|||||||
@ -562,7 +562,7 @@ export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateCypress10ConfigFile(configFilePath: string): void {
|
private updateCypress10ConfigFile(configFilePath: string): void {
|
||||||
this.cypressPreset = nxE2EPreset(this.project.newRoot);
|
this.cypressPreset = nxE2EPreset(configFilePath);
|
||||||
|
|
||||||
const fileContent = this.tree.read(configFilePath, 'utf-8');
|
const fileContent = this.tree.read(configFilePath, 'utf-8');
|
||||||
let sourceFile = tsquery.ast(fileContent);
|
let sourceFile = tsquery.ast(fileContent);
|
||||||
|
|||||||
@ -30,6 +30,12 @@
|
|||||||
"version": "12.8.0-beta.0",
|
"version": "12.8.0-beta.0",
|
||||||
"description": "Remove Typescript Preprocessor Plugin",
|
"description": "Remove Typescript Preprocessor Plugin",
|
||||||
"factory": "./src/migrations/update-12-8-0/remove-typescript-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": {
|
"packageJsonUpdates": {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { workspaceRoot } from '@nrwl/devkit';
|
import { workspaceRoot } from '@nrwl/devkit';
|
||||||
import { join, relative } from 'path';
|
import { dirname, join, relative } from 'path';
|
||||||
|
import { lstatSync } from 'fs';
|
||||||
|
|
||||||
interface BaseCypressPreset {
|
interface BaseCypressPreset {
|
||||||
videosFolder: string;
|
videosFolder: string;
|
||||||
@ -9,8 +10,13 @@ interface BaseCypressPreset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function nxBaseCypressPreset(pathToConfig: string): BaseCypressPreset {
|
export function nxBaseCypressPreset(pathToConfig: string): BaseCypressPreset {
|
||||||
const projectPath = relative(workspaceRoot, pathToConfig);
|
// prevent from placing path outside the root of the workspace
|
||||||
const offset = relative(pathToConfig, workspaceRoot);
|
// 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 videosFolder = join(offset, 'dist', 'cypress', projectPath, 'videos');
|
||||||
const screenshotsFolder = join(
|
const screenshotsFolder = join(
|
||||||
offset,
|
offset,
|
||||||
|
|||||||
@ -45,7 +45,8 @@ export default async function cypressExecutor(
|
|||||||
context: ExecutorContext
|
context: ExecutorContext
|
||||||
) {
|
) {
|
||||||
options = normalizeOptions(options, context);
|
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;
|
let success;
|
||||||
|
|
||||||
for await (const baseUrl of startDevServer(options, context)) {
|
for await (const baseUrl of startDevServer(options, context)) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
addProjectConfiguration,
|
addProjectConfiguration,
|
||||||
ProjectConfiguration,
|
ProjectConfiguration,
|
||||||
|
readJson,
|
||||||
readProjectConfiguration,
|
readProjectConfiguration,
|
||||||
Tree,
|
Tree,
|
||||||
updateProjectConfiguration,
|
updateProjectConfiguration,
|
||||||
@ -118,6 +119,19 @@ describe('Cypress Component Project', () => {
|
|||||||
expect(projectConfig.targets['component-test']).toMatchSnapshot();
|
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 () => {
|
it('should not error when rerunning on an existing project', async () => {
|
||||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||||
tree.write('libs/cool-lib/cypress.config.ts', '');
|
tree.write('libs/cool-lib/cypress.config.ts', '');
|
||||||
|
|||||||
@ -7,7 +7,9 @@ import {
|
|||||||
ProjectConfiguration,
|
ProjectConfiguration,
|
||||||
readProjectConfiguration,
|
readProjectConfiguration,
|
||||||
Tree,
|
Tree,
|
||||||
|
updateJson,
|
||||||
updateProjectConfiguration,
|
updateProjectConfiguration,
|
||||||
|
NxJsonConfiguration,
|
||||||
} from '@nrwl/devkit';
|
} from '@nrwl/devkit';
|
||||||
import { installedCypressVersion } from '../../utils/cypress-version';
|
import { installedCypressVersion } from '../../utils/cypress-version';
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ export async function cypressComponentProject(
|
|||||||
|
|
||||||
addProjectFiles(tree, projectConfig, options);
|
addProjectFiles(tree, projectConfig, options);
|
||||||
addTargetToProject(tree, projectConfig, options);
|
addTargetToProject(tree, projectConfig, options);
|
||||||
|
addToCacheableOperations(tree);
|
||||||
if (!options.skipFormat) {
|
if (!options.skipFormat) {
|
||||||
await formatFiles(tree);
|
await formatFiles(tree);
|
||||||
}
|
}
|
||||||
@ -87,3 +89,25 @@ function addTargetToProject(
|
|||||||
|
|
||||||
updateProjectConfiguration(tree, options.project, projectConfig);
|
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 { nxBaseCypressPreset } from '@nrwl/cypress/plugins/cypress-preset';
|
||||||
import { getCSSModuleLocalIdent } from '@nrwl/web/src/utils/web.config';
|
import type { CypressExecutorOptions } from '@nrwl/cypress/src/executors/cypress/cypress.impl';
|
||||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
import {
|
||||||
import type { Configuration } from 'webpack';
|
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
|
* 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 {
|
return {
|
||||||
...nxBaseCypressPreset(pathToConfig),
|
...nxBaseCypressPreset(pathToConfig),
|
||||||
devServer: {
|
devServer: {
|
||||||
@ -28,152 +108,139 @@ export function nxComponentTestingPreset(pathToConfig: string) {
|
|||||||
// need to use const to prevent typing to string
|
// need to use const to prevent typing to string
|
||||||
framework: 'react',
|
framework: 'react',
|
||||||
bundler: 'webpack',
|
bundler: 'webpack',
|
||||||
webpackConfig: buildBaseWebpackConfig({
|
webpackConfig,
|
||||||
tsConfigPath: 'tsconfig.cy.json',
|
|
||||||
compiler: 'babel',
|
|
||||||
}),
|
|
||||||
} as const,
|
} 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 🤔
|
* apply the schema.json defaults from the @nrwl/web:webpack executor to the target options
|
||||||
function buildBaseWebpackConfig({
|
*/
|
||||||
tsConfigPath = 'tsconfig.cy.json',
|
function withSchemaDefaults(
|
||||||
compiler = 'babel',
|
target: Target,
|
||||||
}: {
|
context: ExecutorContext
|
||||||
tsConfigPath: string;
|
): WebWebpackExecutorOptions {
|
||||||
compiler: 'swc' | 'babel';
|
const options = readTargetOptions<WebWebpackExecutorOptions>(target, context);
|
||||||
}): 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') {
|
options.compiler ??= 'babel';
|
||||||
config.module.rules.push({
|
options.deleteOutputPath ??= true;
|
||||||
test: /\.([jt])sx?$/,
|
options.vendorChunk ??= true;
|
||||||
loader: require.resolve('swc-loader'),
|
options.commonChunk ??= true;
|
||||||
exclude: /node_modules/,
|
options.runtimeChunk ??= true;
|
||||||
options: {
|
options.sourceMap ??= true;
|
||||||
jsc: {
|
options.assets ??= [];
|
||||||
parser: {
|
options.scripts ??= [];
|
||||||
syntax: 'typescript',
|
options.styles ??= [];
|
||||||
decorators: true,
|
options.budgets ??= [];
|
||||||
tsx: true,
|
options.namedChunks ??= true;
|
||||||
},
|
options.outputHashing ??= 'none';
|
||||||
transform: {
|
options.extractCss ??= true;
|
||||||
react: {
|
options.memoryLimit ??= 2048;
|
||||||
runtime: 'automatic',
|
options.maxWorkers ??= 2;
|
||||||
},
|
options.fileReplacements ??= [];
|
||||||
},
|
options.buildLibsFromSource ??= true;
|
||||||
loose: true,
|
options.generateIndexHtml ??= true;
|
||||||
},
|
return options;
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = {
|
function buildTargetWebpack(
|
||||||
modules: {
|
graph: ProjectGraph,
|
||||||
mode: 'local',
|
buildTarget: string,
|
||||||
getLocalIdent: getCSSModuleLocalIdent,
|
componentTestingProjectName: string
|
||||||
},
|
) {
|
||||||
importLoaders: 1,
|
const parsed = parseTargetString(buildTarget);
|
||||||
};
|
|
||||||
|
|
||||||
const commonLoaders = [
|
const buildableProjectConfig = graph.nodes[parsed.project]?.data;
|
||||||
{
|
const ctProjectConfig = graph.nodes[componentTestingProjectName]?.data;
|
||||||
loader: require.resolve('style-loader'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: require.resolve('css-loader'),
|
|
||||||
options: loaderModulesOptions,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const CSS_MODULES_LOADER = {
|
if (!buildableProjectConfig || !ctProjectConfig) {
|
||||||
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
|
throw new Error(stripIndents`Unable to load project configs from graph.
|
||||||
oneOf: [
|
Using build target '${buildTarget}'
|
||||||
{
|
Has build config? ${!!buildableProjectConfig}
|
||||||
test: /\.module\.css$/,
|
Has component config? ${!!ctProjectConfig}
|
||||||
use: commonLoaders,
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
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 () => {
|
it('should not throw if path is invalid', async () => {
|
||||||
mockedAssertMinimumCypressVersion.mockReturnValue();
|
mockedAssertMinimumCypressVersion.mockReturnValue();
|
||||||
await libraryGenerator(tree, {
|
await libraryGenerator(tree, {
|
||||||
|
|||||||
@ -50,6 +50,11 @@ function generateSpecsForComponents(tree: Tree, filePath: string) {
|
|||||||
const componentDir = dirname(filePath);
|
const componentDir = dirname(filePath);
|
||||||
const ext = extname(filePath);
|
const ext = extname(filePath);
|
||||||
const fileName = basename(filePath, ext);
|
const fileName = basename(filePath, ext);
|
||||||
|
|
||||||
|
if (tree.exists(joinPathFragments(componentDir, `${fileName}.cy${ext}`))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultExport = getComponentNode(sourceFile);
|
const defaultExport = getComponentNode(sourceFile);
|
||||||
|
|
||||||
if (cmpNodes?.length) {
|
if (cmpNodes?.length) {
|
||||||
|
|||||||
@ -1,11 +1,25 @@
|
|||||||
import { assertMinimumCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
|
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 { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
|
||||||
import { Linter } from '@nrwl/linter';
|
import { Linter } from '@nrwl/linter';
|
||||||
import componentGenerator from '../component/component';
|
import { applicationGenerator } from '../application/application';
|
||||||
import libraryGenerator from '../library/library';
|
import { componentGenerator } from '../component/component';
|
||||||
|
import { libraryGenerator } from '../library/library';
|
||||||
import { cypressComponentConfigGenerator } from './cypress-component-configuration';
|
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');
|
jest.mock('@nrwl/cypress/src/utils/cypress-version');
|
||||||
describe('React:CypressComponentTestConfiguration', () => {
|
describe('React:CypressComponentTestConfiguration', () => {
|
||||||
let tree: Tree;
|
let tree: Tree;
|
||||||
@ -15,9 +29,17 @@ describe('React:CypressComponentTestConfiguration', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tree = createTreeWithEmptyV1Workspace();
|
tree = createTreeWithEmptyV1Workspace();
|
||||||
});
|
});
|
||||||
it('should generate cypress component test config', async () => {
|
it('should generate cypress component test config with --build-target', async () => {
|
||||||
mockedAssertCypressVersion.mockReturnValue();
|
mockedAssertCypressVersion.mockReturnValue();
|
||||||
|
|
||||||
|
await applicationGenerator(tree, {
|
||||||
|
e2eTestRunner: 'none',
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
skipFormat: true,
|
||||||
|
style: 'scss',
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
name: 'my-app',
|
||||||
|
});
|
||||||
await libraryGenerator(tree, {
|
await libraryGenerator(tree, {
|
||||||
linter: Linter.EsLint,
|
linter: Linter.EsLint,
|
||||||
name: 'some-lib',
|
name: 'some-lib',
|
||||||
@ -27,17 +49,43 @@ describe('React:CypressComponentTestConfiguration', () => {
|
|||||||
unitTestRunner: 'none',
|
unitTestRunner: 'none',
|
||||||
component: true,
|
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, {
|
await cypressComponentConfigGenerator(tree, {
|
||||||
project: 'some-lib',
|
project: 'some-lib',
|
||||||
generateTests: false,
|
generateTests: false,
|
||||||
|
buildTarget: 'my-app:build',
|
||||||
});
|
});
|
||||||
|
|
||||||
const config = tree.read('libs/some-lib/cypress.config.ts', 'utf-8');
|
const config = tree.read('libs/some-lib/cypress.config.ts', 'utf-8');
|
||||||
expect(config).toContain(
|
expect(config).toContain(
|
||||||
"import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing"
|
"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');
|
const cyTsConfig = readJson(tree, 'libs/some-lib/tsconfig.cy.json');
|
||||||
expect(cyTsConfig.include).toEqual([
|
expect(cyTsConfig.include).toEqual([
|
||||||
@ -63,11 +111,123 @@ describe('React:CypressComponentTestConfiguration', () => {
|
|||||||
expect(baseTsConfig.references).toEqual(
|
expect(baseTsConfig.references).toEqual(
|
||||||
expect.arrayContaining([{ path: './tsconfig.cy.json' }])
|
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 () => {
|
it('should generate tests for existing tsx components', async () => {
|
||||||
mockedAssertCypressVersion.mockReturnValue();
|
mockedAssertCypressVersion.mockReturnValue();
|
||||||
|
await applicationGenerator(tree, {
|
||||||
|
e2eTestRunner: 'none',
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
skipFormat: true,
|
||||||
|
style: 'scss',
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
name: 'my-app',
|
||||||
|
});
|
||||||
await libraryGenerator(tree, {
|
await libraryGenerator(tree, {
|
||||||
linter: Linter.EsLint,
|
linter: Linter.EsLint,
|
||||||
name: 'some-lib',
|
name: 'some-lib',
|
||||||
@ -86,6 +246,7 @@ describe('React:CypressComponentTestConfiguration', () => {
|
|||||||
await cypressComponentConfigGenerator(tree, {
|
await cypressComponentConfigGenerator(tree, {
|
||||||
project: 'some-lib',
|
project: 'some-lib',
|
||||||
generateTests: true,
|
generateTests: true,
|
||||||
|
buildTarget: 'my-app:build',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(tree.exists('libs/some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
|
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 () => {
|
it('should generate tests for existing js components', async () => {
|
||||||
mockedAssertCypressVersion.mockReturnValue();
|
mockedAssertCypressVersion.mockReturnValue();
|
||||||
|
await applicationGenerator(tree, {
|
||||||
|
e2eTestRunner: 'none',
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
skipFormat: true,
|
||||||
|
style: 'scss',
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
name: 'my-app',
|
||||||
|
});
|
||||||
await libraryGenerator(tree, {
|
await libraryGenerator(tree, {
|
||||||
linter: Linter.EsLint,
|
linter: Linter.EsLint,
|
||||||
name: 'some-lib',
|
name: 'some-lib',
|
||||||
@ -133,6 +301,7 @@ describe('React:CypressComponentTestConfiguration', () => {
|
|||||||
await cypressComponentConfigGenerator(tree, {
|
await cypressComponentConfigGenerator(tree, {
|
||||||
project: 'some-lib',
|
project: 'some-lib',
|
||||||
generateTests: true,
|
generateTests: true,
|
||||||
|
buildTarget: 'my-app:build',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(tree.exists('libs/some-lib/src/lib/some-cmp.cy.js')).toBeTruthy();
|
expect(tree.exists('libs/some-lib/src/lib/some-cmp.cy.js')).toBeTruthy();
|
||||||
|
|||||||
@ -1,21 +1,8 @@
|
|||||||
import { cypressComponentProject } from '@nrwl/cypress';
|
import { cypressComponentProject } from '@nrwl/cypress';
|
||||||
import {
|
import { formatFiles, readProjectConfiguration, Tree } from '@nrwl/devkit';
|
||||||
formatFiles,
|
import { addFiles } from './lib/add-files';
|
||||||
generateFiles,
|
import { updateProjectConfig, updateTsConfig } from './lib/update-configs';
|
||||||
joinPathFragments,
|
import { CypressComponentConfigurationSchema } from './schema.d';
|
||||||
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);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is for using cypresses own Component testing, if you want to use test
|
* This is for using cypresses own Component testing, if you want to use test
|
||||||
@ -32,6 +19,7 @@ export async function cypressComponentConfigGenerator(
|
|||||||
skipFormat: true,
|
skipFormat: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await updateProjectConfig(tree, options);
|
||||||
addFiles(tree, projectConfig, options);
|
addFiles(tree, projectConfig, options);
|
||||||
updateTsConfig(tree, projectConfig);
|
updateTsConfig(tree, projectConfig);
|
||||||
if (options.skipFormat) {
|
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;
|
export default cypressComponentConfigGenerator;
|
||||||
|
|||||||
@ -2,5 +2,5 @@ import { defineConfig } from 'cypress';
|
|||||||
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
|
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
|
||||||
|
|
||||||
export default defineConfig({
|
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;
|
project: string;
|
||||||
generateTests: boolean;
|
generateTests: boolean;
|
||||||
skipFormat?: boolean;
|
skipFormat?: boolean;
|
||||||
|
buildTarget?: string;
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/schema",
|
"$schema": "https://json-schema.org/schema",
|
||||||
"cli": "nx",
|
"cli": "nx",
|
||||||
"$id": "NxReactCypressComponentTestConfiguration",
|
"$id": "NxReactCypressComponentTestConfiguration",
|
||||||
"title": "Add Cypress component testing",
|
"title": "Add Cypress component testing",
|
||||||
@ -24,6 +24,11 @@
|
|||||||
},
|
},
|
||||||
"x-prompt": "What project should we add Cypress component testing to?"
|
"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": {
|
"generateTests": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Generate default component tests for existing components in the project",
|
"description": "Generate default component tests for existing components in the project",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user