feat(testing): migrate cypress to devkit

This commit is contained in:
Jason Jean 2021-01-27 14:58:06 -05:00 committed by Victor Savkin
parent 402f5069ad
commit 037c1b94b8
28 changed files with 335 additions and 346 deletions

View File

@ -51,7 +51,7 @@
},
"cypress": {
"tags": [],
"implicitDependencies": ["workspace"]
"implicitDependencies": ["workspace", "linter"]
},
"jest": {
"tags": [],

View File

@ -60,10 +60,10 @@ Array [
"/.storybook/main.js",
"/.storybook/tsconfig.json",
"/.storybook/webpack.config.js",
"/apps/test-ui-lib-e2e/.eslintrc.json",
"/apps/test-ui-lib-e2e/cypress.json",
"/apps/test-ui-lib-e2e/tsconfig.e2e.json",
"/apps/test-ui-lib-e2e/tsconfig.json",
"/apps/test-ui-lib-e2e/.eslintrc.json",
"/apps/test-ui-lib-e2e/src/fixtures/example.json",
"/apps/test-ui-lib-e2e/src/plugins/index.js",
"/apps/test-ui-lib-e2e/src/support/commands.ts",

View File

@ -1,5 +1,14 @@
{
"extends": "../../.eslintrc",
"rules": {},
"overrides": [
{
"files": ["**/*.ts"],
"excludedFiles": ["./src/migrations/**"],
"rules": {
"no-restricted-imports": ["error", "@nrwl/workspace"]
}
}
],
"ignorePatterns": ["!**/*"]
}

View File

@ -3,15 +3,30 @@
"version": "0.1",
"schematics": {
"init": {
"factory": "./src/schematics/init/init",
"schema": "./src/schematics/init/schema.json",
"factory": "./src/generators/init/init#initSchematic",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the @nrwl/cypress plugin",
"aliases": ["ng-add"],
"hidden": true
},
"cypress-project": {
"factory": "./src/schematics/cypress-project/cypress-project",
"schema": "./src/schematics/cypress-project/schema.json",
"factory": "./src/generators/cypress-project/cypress-project#cypressProjectSchematic",
"schema": "./src/generators/cypress-project/schema.json",
"description": "Add a Cypress E2E Project",
"hidden": true
}
},
"generators": {
"init": {
"factory": "./src/generators/init/init#initGenerator",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the @nrwl/cypress plugin",
"aliases": ["ng-add"],
"hidden": true
},
"cypress-project": {
"factory": "./src/generators/cypress-project/cypress-project#cypressProjectGenerator",
"schema": "./src/generators/cypress-project/schema.json",
"description": "Add a Cypress E2E Project",
"hidden": true
}

View File

@ -1 +1 @@
export { cypressProjectGenerator } from './src/schematics/cypress-project/cypress-project';
export { cypressProjectGenerator } from './src/generators/cypress-project/cypress-project';

View File

@ -36,9 +36,7 @@
},
"dependencies": {
"@nrwl/devkit": "*",
"@angular-devkit/architect": "~0.1100.1",
"@angular-devkit/core": "~11.0.1",
"@angular-devkit/schematics": "~11.0.1",
"@nrwl/linter": "*",
"@cypress/webpack-preprocessor": "~4.1.2",
"tree-kill": "1.2.2",
"ts-loader": "5.4.5",

View File

@ -1,23 +1,26 @@
import { Tree } from '@angular-devkit/schematics';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { runSchematic } from '../../utils/testing';
import { readJsonInTree, Linter, NxJson } from '@nrwl/workspace';
import { readJson, readProjectConfiguration, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { cypressProjectGenerator } from './cypress-project';
import { Schema } from './schema';
import { Linter } from '@nrwl/linter';
describe('schematic:cypress-project', () => {
let appTree: Tree;
let tree: Tree;
const defaultOptions: Omit<Schema, 'name' | 'project'> = {
linter: Linter.EsLint,
};
beforeEach(() => {
appTree = Tree.empty();
appTree = createEmptyWorkspace(appTree);
tree = createTreeWithEmptyWorkspace();
});
describe('Cypress Project', () => {
it('should generate files', async () => {
const tree = await runSchematic(
'cypress-project',
{ name: 'my-app-e2e', project: 'my-app' },
appTree
);
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
});
expect(tree.exists('apps/my-app-e2e/cypress.json')).toBeTruthy();
expect(tree.exists('apps/my-app-e2e/tsconfig.e2e.json')).toBeTruthy();
@ -37,12 +40,12 @@ describe('schematic:cypress-project', () => {
});
it('should add update `workspace.json` file', async () => {
const tree = await runSchematic(
'cypress-project',
{ name: 'my-app-e2e', project: 'my-app', linter: Linter.TsLint },
appTree
);
const workspaceJson = readJsonInTree(tree, 'workspace.json');
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.TsLint,
});
const workspaceJson = readJson(tree, 'workspace.json');
const project = workspaceJson.projects['my-app-e2e'];
expect(project.root).toEqual('apps/my-app-e2e');
@ -70,12 +73,12 @@ describe('schematic:cypress-project', () => {
});
it('should add update `workspace.json` file properly when eslint is passed', async () => {
const tree = await runSchematic(
'cypress-project',
{ name: 'my-app-e2e', project: 'my-app', linter: Linter.EsLint },
appTree
);
const workspaceJson = readJsonInTree(tree, 'workspace.json');
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.EsLint,
});
const workspaceJson = readJson(tree, 'workspace.json');
const project = workspaceJson.projects['my-app-e2e'];
expect(project.architect.lint).toEqual({
@ -87,26 +90,24 @@ describe('schematic:cypress-project', () => {
});
it('should update nx.json', async () => {
const tree = await runSchematic(
'cypress-project',
{ name: 'my-app-e2e', project: 'my-app', linter: Linter.EsLint },
appTree
);
const nxJson = readJsonInTree<NxJson>(tree, 'nx.json');
expect(nxJson.projects['my-app-e2e']).toEqual({
tags: [],
implicitDependencies: ['my-app'],
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.EsLint,
});
const project = readProjectConfiguration(tree, 'my-app-e2e');
expect(project.tags).toEqual([]);
expect(project.implicitDependencies).toEqual(['my-app']);
});
it('should set right path names in `cypress.json`', async () => {
const tree = await runSchematic(
'cypress-project',
{ name: 'my-app-e2e', project: 'my-app' },
appTree
);
const cypressJson = readJsonInTree(tree, 'apps/my-app-e2e/cypress.json');
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
});
const cypressJson = readJson(tree, 'apps/my-app-e2e/cypress.json');
expect(cypressJson).toEqual({
fileServerFolder: '.',
@ -123,15 +124,12 @@ describe('schematic:cypress-project', () => {
});
it('should set right path names in `tsconfig.e2e.json`', async () => {
const tree = await runSchematic(
'cypress-project',
{ name: 'my-app-e2e', project: 'my-app' },
appTree
);
const tsconfigJson = readJsonInTree(
tree,
'apps/my-app-e2e/tsconfig.e2e.json'
);
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
});
const tsconfigJson = readJson(tree, 'apps/my-app-e2e/tsconfig.e2e.json');
expect(tsconfigJson.extends).toEqual('./tsconfig.json');
expect(tsconfigJson.compilerOptions.outDir).toEqual('../../dist/out-tsc');
@ -139,17 +137,13 @@ describe('schematic:cypress-project', () => {
describe('nested', () => {
it('should update workspace.json', async () => {
const tree = await runSchematic(
'cypress-project',
{
name: 'my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir',
linter: Linter.TsLint,
},
appTree
);
const projectConfig = readJsonInTree(tree, 'workspace.json').projects[
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir',
linter: Linter.TsLint,
});
const projectConfig = readJson(tree, 'workspace.json').projects[
'my-dir-my-app-e2e'
];
@ -178,12 +172,13 @@ describe('schematic:cypress-project', () => {
});
it('should set right path names in `cypress.json`', async () => {
const tree = await runSchematic(
'cypress-project',
{ name: 'my-app-e2e', project: 'my-dir-my-app', directory: 'my-dir' },
appTree
);
const cypressJson = readJsonInTree(
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir',
});
const cypressJson = readJson(
tree,
'apps/my-dir/my-app-e2e/cypress.json'
);
@ -204,12 +199,13 @@ describe('schematic:cypress-project', () => {
});
it('should set right path names in `tsconfig.e2e.json`', async () => {
const tree = await runSchematic(
'cypress-project',
{ name: 'my-app-e2e', project: 'my-dir-my-app', directory: 'my-dir' },
appTree
);
const tsconfigJson = readJsonInTree(
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir',
});
const tsconfigJson = readJson(
tree,
'apps/my-dir/my-app-e2e/tsconfig.e2e.json'
);
@ -223,13 +219,12 @@ describe('schematic:cypress-project', () => {
describe('--project', () => {
describe('none', () => {
it('should not add any implicit dependencies', async () => {
const tree = await runSchematic(
'cypress-project',
{ name: 'my-app-e2e' },
appTree
);
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
});
const nxJson = readJsonInTree(tree, 'nx.json');
const nxJson = readJson(tree, 'nx.json');
expect(nxJson.projects['my-app-e2e']).toEqual({ tags: [] });
});
});
@ -238,16 +233,13 @@ describe('schematic:cypress-project', () => {
describe('--linter', () => {
describe('eslint', () => {
it('should add eslint-plugin-cypress', async () => {
const tree = await runSchematic(
'cypress-project',
{ name: 'my-app-e2e', project: 'my-app', linter: Linter.EsLint },
appTree
);
const packageJson = readJsonInTree(tree, 'package.json');
const eslintrcJson = readJsonInTree(
tree,
'apps/my-app-e2e/.eslintrc.json'
);
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.EsLint,
});
const packageJson = readJson(tree, 'package.json');
const eslintrcJson = readJson(tree, 'apps/my-app-e2e/.eslintrc.json');
expect(
packageJson.devDependencies['eslint-plugin-cypress']

View File

@ -0,0 +1,140 @@
import {
addDependenciesToPackageJson,
addProjectConfiguration,
convertNxGenerator,
formatFiles,
generateFiles,
getWorkspaceLayout,
joinPathFragments,
names,
offsetFromRoot,
toJS,
Tree,
updateJson,
} from '@nrwl/devkit';
import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { join } from 'path';
// app
import { Schema } from './schema';
import { eslintPluginCypressVersion } from '../../utils/versions';
export interface CypressProjectSchema extends Schema {
projectName: string;
projectRoot: string;
}
function createFiles(host: Tree, options: CypressProjectSchema) {
generateFiles(host, join(__dirname, './files'), options.projectRoot, {
tmpl: '',
...options,
project: options.project || 'Project',
ext: options.js ? 'js' : 'ts',
offsetFromRoot: offsetFromRoot(options.projectRoot),
});
if (options.js) {
toJS(host);
}
}
function addProject(host: Tree, options: CypressProjectSchema) {
addProjectConfiguration(host, options.projectName, {
root: options.projectRoot,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
projectType: 'application',
targets: {
e2e: {
executor: '@nrwl/cypress:cypress',
options: {
cypressConfig: joinPathFragments(options.projectRoot, 'cypress.json'),
tsConfig: joinPathFragments(options.projectRoot, 'tsconfig.e2e.json'),
devServerTarget: `${options.project}:serve`,
},
configurations: {
production: {
devServerTarget: `${options.project}:serve:production`,
},
},
},
},
tags: [],
implicitDependencies: options.project ? [options.project] : undefined,
});
}
async function addLinter(host: Tree, options: CypressProjectSchema) {
const installTask = await lintProjectGenerator(host, {
project: options.projectName,
linter: options.linter,
skipFormat: true,
tsConfigPaths: [
joinPathFragments(options.projectRoot, 'tsconfig.e2e.json'),
],
eslintFilePatterns: [
`${options.projectRoot}/**/*.${options.js ? 'js' : '{js,ts}'}`,
],
});
if (options.linter !== Linter.EsLint) {
return installTask;
}
const installTask2 = addDependenciesToPackageJson(
host,
{},
{ 'eslint-plugin-cypress': eslintPluginCypressVersion }
);
updateJson(host, join(options.projectRoot, '.eslintrc.json'), (json) => {
json.extends = ['plugin:cypress/recommended', ...json.extends];
json.overrides = [
{
files: ['src/plugins/index.js'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
'no-undef': 'off',
},
},
];
return json;
});
return installTask || installTask2;
}
export async function cypressProjectGenerator(host: Tree, schema: Schema) {
const options = normalizeOptions(host, schema);
createFiles(host, options);
addProject(host, options);
const installTask = await addLinter(host, options);
if (!options.skipFormat) {
await formatFiles(host);
}
return installTask;
}
function normalizeOptions(host: Tree, options: Schema): CypressProjectSchema {
const { appsDir } = getWorkspaceLayout(host);
const projectName = options.directory
? names(options.directory).fileName + '-' + options.name
: options.name;
const projectRoot = options.directory
? joinPathFragments(
appsDir,
names(options.directory).fileName,
options.name
)
: join(appsDir, options.name);
return {
...options,
projectName,
projectRoot,
};
}
export default cypressProjectGenerator;
export const cypressProjectSchematic = convertNxGenerator(
cypressProjectGenerator
);

View File

@ -0,0 +1,10 @@
import { Linter } from '@nrwl/linter';
export interface Schema {
project?: string;
name: string;
directory?: string;
linter: Linter;
js?: boolean;
skipFormat?: boolean;
}

View File

@ -1,6 +1,7 @@
{
"$schema": "http://json-schema.org/schema",
"id": "Nx Cypress Project Generator Schema",
"cli": "nx",
"title": "Create Cypress Configuration for the workspace",
"type": "object",
"properties": {
@ -34,6 +35,11 @@
"description": "Generate JavaScript files rather than TypeScript files",
"type": "boolean",
"default": false
},
"skipFormat": {
"description": "Skip formatting files",
"type": "boolean",
"default": false
}
},
"required": ["name"]

View File

@ -0,0 +1,33 @@
import { readJson, Tree, updateJson } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { cypressVersion } from '../../utils/versions';
import initGenerator from './init';
describe('init', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should add dependencies into `package.json` file', async () => {
const existing = 'existing';
const existingVersion = '1.0.0';
updateJson(tree, 'package.json', (json) => {
json.dependencies['@nrwl/cypress'] = cypressVersion;
json.dependencies[existing] = existingVersion;
json.devDependencies[existing] = existingVersion;
return json;
});
initGenerator(tree);
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies.cypress).toBeDefined();
expect(packageJson.devDependencies['@nrwl/cypress']).toBeDefined();
expect(packageJson.devDependencies[existing]).toBeDefined();
expect(packageJson.dependencies['@nrwl/cypress']).toBeUndefined();
expect(packageJson.dependencies[existing]).toBeDefined();
});
});

View File

@ -0,0 +1,32 @@
import {
addDependenciesToPackageJson,
convertNxGenerator,
Tree,
updateJson,
} from '@nrwl/devkit';
import { cypressVersion, nxVersion } from '../../utils/versions';
function updateDependencies(host: Tree) {
updateJson(host, 'package.json', (json) => {
json.dependencies = json.dependencies || {};
delete json.dependencies['@nrwl/cypress'];
return json;
});
return addDependenciesToPackageJson(
host,
{},
{
['@nrwl/cypress']: nxVersion,
cypress: cypressVersion,
}
);
}
export function initGenerator(host: Tree) {
return updateDependencies(host);
}
export default initGenerator;
export const initSchematic = convertNxGenerator(initGenerator);

View File

@ -1,6 +1,7 @@
{
"$schema": "http://json-schema.org/schema",
"id": "NxCypressInit",
"cli": "nx",
"title": "Add Cypress Configuration to the workspace",
"type": "object",
"properties": {}

View File

@ -1,169 +0,0 @@
import {
apply,
chain,
mergeWith,
move,
noop,
Rule,
SchematicContext,
template,
Tree,
url,
} from '@angular-devkit/schematics';
import { join, normalize } from '@angular-devkit/core';
// app
import {
NxJson,
Linter,
addDepsToPackageJson,
addLintFiles,
generateProjectLint,
updateJsonInTree,
updateWorkspaceInTree,
} from '@nrwl/workspace';
import { Schema } from './schema';
import { toJS } from '@nrwl/workspace/src/utils/rules/to-js';
import { appsDir } from '@nrwl/workspace/src/utils/ast-utils';
import { eslintPluginCypressVersion } from '../../utils/versions';
import { names, offsetFromRoot } from '@nrwl/devkit';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
export interface CypressProjectSchema extends Schema {
projectName: string;
projectRoot: string;
}
function generateFiles(options: CypressProjectSchema): Rule {
return (): Rule => {
return mergeWith(
apply(url('./files'), [
template({
tmpl: '',
...options,
ext: options.js ? 'js' : 'ts',
offsetFromRoot: offsetFromRoot(options.projectRoot),
}),
move(options.projectRoot),
options.js ? toJS() : noop(),
])
);
};
}
function updateNxJson(options: CypressProjectSchema): Rule {
return updateJsonInTree<NxJson>('nx.json', (json) => {
json.projects[options.projectName] = {
tags: [],
};
if (options.project) {
json.projects[options.projectName].implicitDependencies = [
options.project,
];
}
return json;
});
}
function updateWorkspaceJson(options: CypressProjectSchema): Rule {
return updateWorkspaceInTree((json) => {
const architect: any = {};
architect.e2e = {
builder: '@nrwl/cypress:cypress',
options: {
cypressConfig: join(normalize(options.projectRoot), 'cypress.json'),
tsConfig: join(normalize(options.projectRoot), 'tsconfig.e2e.json'),
devServerTarget: `${options.project}:serve`,
},
configurations: {
production: {
devServerTarget: `${options.project}:serve:production`,
},
},
};
architect.lint = generateProjectLint(
normalize(options.projectRoot),
join(normalize(options.projectRoot), 'tsconfig.e2e.json'),
options.linter,
[`${options.projectRoot}/**/*.${options.js ? 'js' : '{js,ts}'}`]
);
json.projects[options.projectName] = {
root: options.projectRoot,
sourceRoot: join(normalize(options.projectRoot), 'src'),
projectType: 'application',
architect,
};
return json;
});
}
function addLinter(options: CypressProjectSchema): Rule {
return chain([
options.linter === Linter.EsLint
? addDepsToPackageJson(
{},
{ 'eslint-plugin-cypress': eslintPluginCypressVersion }
)
: noop(),
addLintFiles(options.projectRoot, options.linter, {
localConfig: {
extends: ['plugin:cypress/recommended'],
// we need this overrides because we enabled
// allowJS in the tsconfig to allow for JS based
// Cypress tests. That however leads to issues
// with the CommonJS Cypress plugin file
overrides: [
{
files: ['src/plugins/index.js'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
'no-undef': 'off',
},
},
],
},
}),
]);
}
export default function (options: CypressProjectSchema): Rule {
return (host: Tree, context: SchematicContext) => {
options = normalizeOptions(host, options);
return chain([
addLinter(options),
generateFiles(options),
updateWorkspaceJson(options),
updateNxJson(options),
])(host, context);
};
}
function normalizeOptions(
host: Tree,
options: CypressProjectSchema
): CypressProjectSchema {
const projectName = options.directory
? names(options.directory).fileName + '-' + options.name
: options.name;
const projectRoot = options.directory
? join(
normalize(appsDir(host)),
names(options.directory).fileName,
options.name
)
: join(normalize(appsDir(host)), options.name);
return {
...options,
projectName,
projectRoot,
};
}
export const cypressProjectGenerator = wrapAngularDevkitSchematic(
'@nrwl/cypress',
'cypress-project'
);

View File

@ -1,9 +0,0 @@
import { Linter } from '@nrwl/workspace';
export interface Schema {
project: string;
name: string;
directory: string;
linter: Linter;
js?: boolean;
}

View File

@ -1,37 +0,0 @@
import { Tree } from '@angular-devkit/schematics';
import { addDepsToPackageJson, readJsonInTree } from '@nrwl/workspace';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { callRule, runSchematic } from '../../utils/testing';
import { cypressVersion } from '../../utils/versions';
describe('init', () => {
let appTree: Tree;
beforeEach(() => {
appTree = Tree.empty();
appTree = createEmptyWorkspace(appTree);
});
it('should add dependencies into `package.json` file', async () => {
const existing = 'existing';
const existingVersion = '1.0.0';
await callRule(
addDepsToPackageJson(
{ '@nrwl/cypress': cypressVersion, [existing]: existingVersion },
{ [existing]: existingVersion },
false
),
appTree
);
const tree = await runSchematic('init', {}, appTree);
const packageJson = readJsonInTree(tree, 'package.json');
expect(packageJson.devDependencies.cypress).toBeDefined();
expect(packageJson.devDependencies['@nrwl/cypress']).toBeDefined();
expect(packageJson.devDependencies[existing]).toBeDefined();
expect(packageJson.dependencies['@nrwl/cypress']).toBeUndefined();
expect(packageJson.dependencies[existing]).toBeDefined();
});
});

View File

@ -1,32 +0,0 @@
import { Rule, Tree } from '@angular-devkit/schematics';
import { Schema } from './schema';
import { cypressVersion, nxVersion } from '../../utils/versions';
import { readJsonInTree, updateJsonInTree } from '@nrwl/workspace';
function updateDependencies(): Rule {
return (host: Tree): Rule => {
const packageJson = readJsonInTree(host, 'package.json');
const devDependencies = { ...packageJson.devDependencies };
const dependencies = { ...packageJson.dependencies };
if (!devDependencies['cypress']) {
devDependencies['cypress'] = cypressVersion;
}
if (!devDependencies['@nrwl/cypress']) {
devDependencies['@nrwl/cypress'] = nxVersion;
}
if (packageJson.dependencies['@nrwl/cypress']) {
delete dependencies['@nrwl/cypress'];
}
return updateJsonInTree('package.json', (json) => {
json.dependencies = dependencies;
json.devDependencies = devDependencies;
return json;
});
};
}
export default function (schema: Schema) {
return updateDependencies();
}

View File

@ -18,7 +18,7 @@ const IGNORE_MATCHES = {
'rxjs',
],
cli: ['@nrwl/cli'],
cypress: ['cypress'],
cypress: ['cypress', '@angular-devkit/schematics', '@nrwl/cypress'],
devkit: ['@angular-devkit/architect', 'rxjs'],
gatsby: ['@angular-devkit/architect', 'babel-preset-gatsby', 'rxjs'],
jest: [