Skip to content

Commit

Permalink
feat: standalone implementation
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

- `@nrwl/cypress` no longer required, versions - the plugin is a standalone implementation that is not dependent on `@nrwl/cypress`
- use the available options to configure the execution of your cypress runs
  • Loading branch information
agoldis committed Jan 10, 2023
1 parent 572c047 commit 06d1ff1
Show file tree
Hide file tree
Showing 8 changed files with 6,365 additions and 58 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2021 Currents.dev
Copyright (c) 2021 Currents Software Inc

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
53 changes: 28 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# @currents/nx

[NX](https://nx.dev/) plugin for running cypress tests on Currents.dev
[NX](https://nx.dev/) plugin for running cypress tests using [Currents](https://currents.dev) or [Sorry Cypress](https://sorry-cypress.dev) as an orchestration service.

The plugin is intended for usage in CI environments and runs Cypress in headless mode. Please use `@nrwl/cypress` for running cypress in interactive mode.

## Setup

Expand All @@ -10,44 +12,45 @@ Install `@currents/nx`
npm i --save-dev @currents/nx
```

Add `currents` target to your project configuration
Add target `currents` to your project configuration:

```js

{
// ...
"targets": {
"currents": {
"executor": "@currents/nx:currents",
"options": {
"cypressExecutor": "e2e" // target name that runs "@nrwl/cypress:cypress"
}
},
"e2e": {
"executor": "@nrwl/cypress:cypress",
"options": {
// ...
},
"configurations": {
// ...
// ...
"targets": {
"currents": {
"executor": "@currents/nx:currents",
"options": {
"record": true,
"parallel": true,
"cypressConfig": "apps/app-e2e/cypres.config.ts",
"devServerTarget": "my-react-app:serve",
"testingType": "e2e"
}
}
}
}
// ...
// ...
```
Run cypress tests, using Currents.dev as a dashboard
Options can be configured in `project.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/reference/project-configuration#targets.
```sh
nx run project:currents --group nx --record --key <key> --ci-build-id hello-currents-nx
```
- The plugin requires an already installed `@nrwl/cypress` and a configured target that's running `@nrwl/cypress:cypress`
- `@currents/nx:currents` will run `@nrwl/cypress:cypress` behind the scenes
- You can set predefined options in target definition
- Update your `cypress.json` file with `projectId` obtained at https://app.currents.dev
- Use the record key obtained at https://app.currents.dev
### Configuration
See the [schema.json](./src/executors//schema.json) for the list of available options
### Example
See https://github.com/currents-dev/currents-nx-example for example integration
See https://github.com/currents-dev/currents-nx-example for example integration.
## Migration
### Version `1.0.0`
- `@nrwl/cypress` no longer required - the plugin is a standalone implementation that is not dependent on `@nrwl/cypress`. Use the available configuration options to configure the execution of cypress runs.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"scripts": {
"lint": "eslint . --ext .ts",
"compile": "tsc -d",
"dev": "tsc -d --watch",
"prepare": "husky install",
"release": "release-it",
"clean": "rimraf dist"
Expand Down Expand Up @@ -70,6 +71,6 @@
"@nrwl/devkit": "*",
"@nrwl/linter": "*",
"@nrwl/workspace": "*",
"cy2": "3.4.1"
"cy2": "^4.0.3"
}
}
210 changes: 183 additions & 27 deletions src/executors/currents.impl.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,190 @@
import { ExecutorContext, runExecutor } from '@nrwl/devkit';
import { patch } from 'cy2';
import {
ExecutorContext,
logger,
parseTargetString,
readTargetOptions,
runExecutor,
stripIndents,
} from '@nrwl/devkit';
import 'dotenv/config';
import { basename, dirname, join } from 'path';

export interface CurrentsExecutorOptions {
cypressExecutor: string;
import { installedCypressVersion } from '../utils/cypress-version';

import { run } from 'cy2';

type CypressOptions = Parameters<typeof run>[1] & {
config?: Partial<{
baseUrl: string;
}>;
};

export type Json = { [k: string]: any };

export interface CypressExecutorOptions extends Json {
cypressApiUrl: string;
cypressConfig: string;
watch?: boolean;
tsConfig?: string;
devServerTarget?: string;
key?: string;
record?: boolean;
parallel?: boolean;
baseUrl?: string;
browser?: string;
env?: Record<string, string>;
spec?: string;
ciBuildId?: string | number;
group?: string;
ignoreTestFiles?: string;
reporter?: string;
reporterOptions?: string;
skipServe?: boolean;
testingType?: 'component' | 'e2e';
tag?: string;
}

export default async function cypressExecutor(
options: CypressExecutorOptions,
context: ExecutorContext
) {
options = normalizeOptions(options, context);
// this is used by cypress component testing presets to build the executor contexts with the correct configuration options.
process.env.NX_CYPRESS_TARGET_CONFIGURATION = context.configurationName;
let success;

for await (const baseUrl of startDevServer(options, context)) {
try {
success = await runCypress(baseUrl, options);
if (!options.watch) break;
} catch (e) {
logger.error(e.message);
success = false;
if (!options.watch) break;
}
}

return { success };
}

export default async function currentsExecutor(
options: CurrentsExecutorOptions,
function normalizeOptions(
options: CypressExecutorOptions,
context: ExecutorContext
): CypressExecutorOptions {
options.env = options.env || {};
if (options.tsConfig) {
const tsConfigPath = join(context.root, options.tsConfig);
options.env.tsConfig = tsConfigPath;
process.env.TS_NODE_PROJECT = tsConfigPath;
}

warnDeprecatedCypressVersion();
return options;
}

function warnDeprecatedCypressVersion() {
if (installedCypressVersion() < 10) {
logger.warn(stripIndents`
NOTE:
Support for Cypress versions < 10 is deprecated. Please upgrade to at least Cypress version 10.
A generator to migrate from v8 to v10 is provided. See https://nx.dev/cypress/v10-migration-guide
`);
}
}

async function* startDevServer(
opts: CypressExecutorOptions,
context: ExecutorContext
) {
process.env.CYPRESS_API_URL =
options.cypressApiUrl ?? 'https://cy.currents.dev';

await patch();

const result = await Promise.race([
await runExecutor(
{
project: context.projectName,
target: options.cypressExecutor,
configuration: context.configurationName,
},
{ ...options, watch: false },
context
),
]);
for await (const res of result) {
if (!res.success) return res;
}

return { success: true };
// no dev server, return the provisioned base url
if (!opts.devServerTarget || opts.skipServe) {
yield opts.baseUrl;
return;
}

const { project, target, configuration } = parseTargetString(
opts.devServerTarget
);
const devServerTargetOpts = readTargetOptions(
{ project, target, configuration },
context
);
const targetSupportsWatchOpt =
Object.keys(devServerTargetOpts).includes('watch');

for await (const output of await runExecutor<{
success: boolean;
baseUrl?: string;
}>(
{ project, target, configuration },
// @NOTE: Do not forward watch option if not supported by the target dev server,
// this is relevant for running Cypress against dev server target that does not support this option,
// for instance @nguniversal/builders:ssr-dev-server.
targetSupportsWatchOpt ? { watch: opts.watch } : {},
context
)) {
if (!output.success && !opts.watch)
throw new Error('Could not compile application files');
yield opts.baseUrl || (output.baseUrl as string);
}
}

/**
* Initialize the Cypress test runner with the provided project configuration.
* By default, Cypress will run tests from the CLI without the GUI and provide directly the results in the console output.
*/
async function runCypress(baseUrl: string, opts: CypressExecutorOptions) {
// Cypress expects the folder where a cypress config is present
const projectFolderPath = dirname(opts.cypressConfig);
const options: CypressOptions = {
project: projectFolderPath,
configFile: basename(opts.cypressConfig),
};

// If not, will use the `baseUrl` normally from `cypress.json`
if (baseUrl) {
options.config = { baseUrl };
}

if (opts.browser) {
options.browser = opts.browser;
}

if (opts.env) {
options.env = opts.env;
}
if (opts.spec) {
options.spec = opts.spec;
}

options.tag = opts.tag;

options.record = opts.record;
options.key = opts.key;
options.parallel = opts.parallel;
options.ciBuildId = opts.ciBuildId?.toString();
options.group = opts.group;

if (opts.reporter) {
options.reporter = opts.reporter;
}

if (opts.reporterOptions) {
options.reporterOptions = opts.reporterOptions;
}

options.testingType = opts.testingType;

const result = await run(opts.cypressApiUrl, options);

if (result.status === 'failed') {
return false;
}

return (
result.runs.reduce(
(acc, run) => acc + (run.stats.failures ?? 0) + (run.stats.skipped ?? 0),
0
) === 0
);
}
Loading

0 comments on commit 06d1ff1

Please sign in to comment.