Skip to content

Commit

Permalink
feat(testing): distribute cypress tests for ci (nrwl#20188)
Browse files Browse the repository at this point in the history
  • Loading branch information
FrozenPandaz authored Nov 15, 2023
1 parent e8780f3 commit 8a0707d
Show file tree
Hide file tree
Showing 15 changed files with 321 additions and 132 deletions.
2 changes: 1 addition & 1 deletion docs/generated/packages/nx/executors/noop.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"cli": "nx",
"outputCapture": "pipe",
"properties": {},
"additionalProperties": false,
"additionalProperties": true,
"presets": []
},
"description": "An executor that does nothing",
Expand Down
2 changes: 1 addition & 1 deletion packages/cypress/plugin.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { createNodes } from './src/plugins/plugin';
export { createNodes, createDependencies } from './src/plugins/plugin';
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ describe('add-nx-cypress-plugin migration', () => {
},
ciDevServerTarget: 'my-app:serve-static',
},
e2e: {},
e2e: {
specPattern: '**/*.cy.ts',
},
})
);
updateProjectConfiguration(tree, 'e2e', {
Expand All @@ -77,7 +79,13 @@ describe('add-nx-cypress-plugin migration', () => {

await update(tree);

expect(readProjectConfiguration(tree, 'e2e').targets.e2e).toBeUndefined();
expect(readProjectConfiguration(tree, 'e2e').targets.e2e).toEqual({
configurations: {
ci: {
devServerTarget: 'my-app:serve-static',
},
},
});
});

it('should not the e2e target when it uses a different executor', async () => {
Expand Down Expand Up @@ -119,7 +127,9 @@ describe('add-nx-cypress-plugin migration', () => {
},
ciDevServerTarget: 'my-app:serve-static',
},
e2e: {},
e2e: {
specPattern: '**/*.cy.ts',
},
})
);
updateProjectConfiguration(tree, 'e2e', {
Expand Down Expand Up @@ -149,6 +159,11 @@ describe('add-nx-cypress-plugin migration', () => {
options: {
watch: false,
},
configurations: {
ci: {
devServerTarget: 'my-app:serve-static',
},
},
});
});
});
83 changes: 63 additions & 20 deletions packages/cypress/src/plugins/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,35 @@ import { CreateNodesContext } from '@nx/devkit';
import { defineConfig } from 'cypress';

import { createNodes } from './plugin';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import { join } from 'path';

describe('@nx/cypress/plugin', () => {
let createNodesFunction = createNodes[1];
let context: CreateNodesContext;
let tempFs: TempFs;

beforeEach(async () => {
tempFs = new TempFs('cypress-plugin');

await tempFs.createFiles({
'package.json': '{}',
'test.cy.ts': '',
});
context = {
nxJsonConfiguration: {
namedInputs: {
default: ['{projectRoot}/**/*'],
production: ['!{projectRoot}/**/*.spec.ts'],
},
},
workspaceRoot: '',
workspaceRoot: tempFs.tempDir,
};
});

afterEach(() => {
jest.resetModules();
tempFs.cleanup();
});

it('should add a target for e2e', () => {
Expand All @@ -45,7 +55,6 @@ describe('@nx/cypress/plugin', () => {
"projects": {
".": {
"projectType": "application",
"root": ".",
"targets": {
"e2e": {
"cache": true,
Expand Down Expand Up @@ -98,7 +107,6 @@ describe('@nx/cypress/plugin', () => {
"projects": {
".": {
"projectType": "application",
"root": ".",
"targets": {
"component-test": {
"cache": true,
Expand All @@ -125,10 +133,11 @@ describe('@nx/cypress/plugin', () => {
`);
});

it('should use nxMetadata to create additional configurations', () => {
it('should use ciDevServerTarget to create additional configurations', () => {
mockCypressConfig(
defineConfig({
e2e: {
specPattern: '**/*.cy.ts',
env: {
devServerTargets: {
default: 'my-app:serve',
Expand All @@ -152,14 +161,10 @@ describe('@nx/cypress/plugin', () => {
"projects": {
".": {
"projectType": "application",
"root": ".",
"targets": {
"e2e": {
"cache": true,
"configurations": {
"ci": {
"devServerTarget": "my-app:serve-static",
},
"production": {
"devServerTarget": "my-app:serve:production",
},
Expand All @@ -179,22 +184,60 @@ describe('@nx/cypress/plugin', () => {
"{options.screenshotsFolder}",
],
},
"e2e-ci": {
"cache": true,
"dependsOn": [
{
"params": "forward",
"projects": "self",
"target": "e2e-ci--test.cy.ts",
},
],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
],
"outputs": [
"{options.videosFolder}",
"{options.screenshotsFolder}",
],
},
"e2e-ci--test.cy.ts": {
"cache": true,
"configurations": undefined,
"executor": "@nx/cypress:cypress",
"inputs": [
"default",
"^production",
],
"options": {
"cypressConfig": "cypress.config.js",
"devServerTarget": "my-app:serve-static",
"spec": "test.cy.ts",
"testingType": "e2e",
},
"outputs": [
"{options.videosFolder}",
"{options.screenshotsFolder}",
],
},
},
},
},
}
`);
});
});

function mockCypressConfig(cypressConfig: Cypress.ConfigOptions) {
jest.mock(
'cypress.config.js',
() => ({
default: cypressConfig,
}),
{
virtual: true,
}
);
}
function mockCypressConfig(cypressConfig: Cypress.ConfigOptions) {
jest.mock(
join(tempFs.tempDir, 'cypress.config.js'),
() => ({
default: cypressConfig,
}),
{
virtual: true,
}
);
}
});
112 changes: 97 additions & 15 deletions packages/cypress/src/plugins/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,59 @@
import {
CreateDependencies,
CreateNodes,
CreateNodesContext,
readJsonFile,
TargetConfiguration,
writeJsonFile,
} from '@nx/devkit';
import { basename, dirname, extname, join } from 'path';
import { dirname, extname, join } from 'path';
import { registerTsProject } from '@nx/js/src/internal';

import { getRootTsConfigPath } from '@nx/js';

import { CypressExecutorOptions } from '../executors/cypress/cypress.impl';
import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { readdirSync } from 'fs';
import { existsSync, readdirSync } from 'fs';
import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';

export interface CypressPluginOptions {
ciTargetName?: string;
targetName?: string;
componentTestingTargetName?: string;
}

const cachePath = join(projectGraphCacheDirectory, 'cypress.hash');
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};

const calculatedTargets: Record<
string,
Record<string, TargetConfiguration>
> = {};

function readTargetsCache(): Record<
string,
Record<string, TargetConfiguration<CypressExecutorOptions>>
> {
return readJsonFile(cachePath);
}

function writeTargetsToCache(
targets: Record<
string,
Record<string, TargetConfiguration<CypressExecutorOptions>>
>
) {
writeJsonFile(cachePath, targets);
}

export const createDependencies: CreateDependencies = () => {
writeTargetsToCache(calculatedTargets);
return [];
};

export const createNodes: CreateNodes<CypressPluginOptions> = [
'**/cypress.config.{js,ts,mjs,mts,cjs,cts}',
(configFilePath, options, context) => {
Expand All @@ -33,19 +69,19 @@ export const createNodes: CreateNodes<CypressPluginOptions> = [
return {};
}

const projectName = basename(projectRoot);
const hash = calculateHashForCreateNodes(projectRoot, options, context);

const targets = targetsCache[hash]
? targetsCache[hash]
: buildCypressTargets(configFilePath, projectRoot, options, context);

calculatedTargets[hash] = targets;

return {
projects: {
[projectName]: {
root: projectRoot,
[projectRoot]: {
projectType: 'application',
targets: buildCypressTargets(
configFilePath,
projectRoot,
options,
context
),
targets,
},
},
};
Expand Down Expand Up @@ -99,7 +135,6 @@ function getOutputs(

return outputs;
}

function buildCypressTargets(
configFilePath: string,
projectRoot: string,
Expand Down Expand Up @@ -183,10 +218,56 @@ function buildCypressTargets(

const ciDevServerTarget: string = cypressEnv?.ciDevServerTarget;
if (ciDevServerTarget) {
targets[options.targetName].configurations ??= {};
const specPatterns = Array.isArray(cypressConfig.e2e.specPattern)
? cypressConfig.e2e.specPattern.map((p) => join(projectRoot, p))
: [join(projectRoot, cypressConfig.e2e.specPattern)];

const excludeSpecPatterns: string[] = !cypressConfig.e2e
.excludeSpecPattern
? cypressConfig.e2e.excludeSpecPattern
: Array.isArray(cypressConfig.e2e.excludeSpecPattern)
? cypressConfig.e2e.excludeSpecPattern.map((p) => join(projectRoot, p))
: [join(projectRoot, cypressConfig.e2e.excludeSpecPattern)];
const specFiles = globWithWorkspaceContext(
context.workspaceRoot,
specPatterns,
excludeSpecPatterns
);

targets[options.targetName].configurations['ci'] = {
devServerTarget: ciDevServerTarget,
const dependsOn: TargetConfiguration['dependsOn'] = [];
const outputs = getOutputs(projectRoot, cypressConfig, 'e2e');
const inputs =
'production' in namedInputs
? ['default', '^production']
: ['default', '^default'];
for (const file of specFiles) {
const targetName = options.ciTargetName + '--' + file;
targets[targetName] = {
...targets[options.targetName],
outputs,
inputs,
cache: true,
configurations: undefined,
options: {
...targets[options.targetName].options,
devServerTarget: ciDevServerTarget,
spec: file,
},
};
dependsOn.push({
target: targetName,
projects: 'self',
params: 'forward',
});
}
targets[options.ciTargetName] ??= {};

targets[options.ciTargetName] = {
executor: 'nx:noop',
cache: true,
inputs,
outputs,
dependsOn,
};
}
}
Expand Down Expand Up @@ -260,5 +341,6 @@ function normalizeOptions(options: CypressPluginOptions): CypressPluginOptions {
options ??= {};
options.targetName ??= 'e2e';
options.componentTestingTargetName ??= 'component-test';
options.ciTargetName ??= 'e2e-ci';
return options;
}
17 changes: 17 additions & 0 deletions packages/devkit/src/utils/calculate-hash-for-create-nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { CreateNodesContext } from 'nx/src/devkit-exports';
import { requireNx } from '../../nx';
import { join } from 'path';
const { hashWithWorkspaceContext, hashArray, hashObject } = requireNx();

export function calculateHashForCreateNodes(
projectRoot: string,
options: object,
context: CreateNodesContext
): string {
return hashArray([
hashWithWorkspaceContext(context.workspaceRoot, [
join(projectRoot, '**/*'),
]),
hashObject(options),
]);
}
Loading

0 comments on commit 8a0707d

Please sign in to comment.