diff --git a/.github/ISSUE_TEMPLATE/1.bug.yaml b/.github/ISSUE_TEMPLATE/1.bug.yaml index ebf2ecbc..8a15d764 100644 --- a/.github/ISSUE_TEMPLATE/1.bug.yaml +++ b/.github/ISSUE_TEMPLATE/1.bug.yaml @@ -3,16 +3,31 @@ description: Create a report to help us improve cypress-cloud labels: bug body: + - type: markdown + attributes: + value: | + ## Before Opening a New Issue + + Thank you for taking the time to open an issue! + + `cypress-cloud` runs on various environments with different setups and configurations, we have to ask you to provide more information about your specific setup, otherwise we won't be able to help you. + + Here are a few resources that can help you: + + - [`cypress-cloud` Documentation](https://currents.dev/readme/integration-with-cypress) + - [Troubleshooting Guide](https://currents.dev/readme/integration-with-cypress/troubleshooting) + - [Common Configuration Pitfalls](https://github.com/currents-dev/cypress-cloud#setup-with-existing-plugins) + - type: checkboxes attributes: label: | - Before opening, please confirm: + Please confirm options: - label: I have searched for [duplicate or closed issues](https://github.com/currents-dev/cypress-cloud/issues) and [discussions](https://github.com/currents-dev/cypress-cloud/discussions). required: true - - label: I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue. + - label: I will include a minimal, self-contained set of instructions for consistently reproducing the issue. required: true - - label: I acknowledge that I will attach a **full debug log**, otherwise the issue will be closed with no response. + - label: I will attach a **full debug log**, otherwise the issue will be closed with no response. required: true - type: markdown @@ -23,9 +38,9 @@ body: attributes: label: Environment information description: | - Please run the following command inside your project and copy/paste the output below. + We need to know what package versions you're using. Please run the following command inside your project and copy/paste the output below. - **šŸ‘‰šŸ» Run the command in the right environment šŸ‘ˆšŸ», e.g. if the problem is in CI environment, run it in the CI environment. + **šŸ‘‰šŸ» Run the command in the right environment šŸ‘ˆšŸ»**, e.g. if the problem is in CI environment, run it in the CI environment. ``` npx envinfo --system --binaries --browsers --npmPackages --duplicates --npmGlobalPackages @@ -62,10 +77,10 @@ body: - type: textarea attributes: - label: Command and Setup + label: Setup and Command description: | - - The exact command or code snippet - - Your cloud provider or sorry-cypress setup details + - Your `cypress.config.js` file, including standalone plugins configuration. + - The exact command or code snippet used to run the tests. validations: required: true @@ -74,17 +89,18 @@ body: attributes: label: Full log and debug output description: | - Run in debug mode to provide more info - error messages and stack traces. + Enable the [debug mode](https://currents.dev/readme/integration-with-cypress/troubleshooting#enabling-debug-mode-for-cypress-cloud-1.9.0+) to provide more info - error messages and stack traces. - - **šŸ‘‰šŸ» Include the full log šŸ‘ˆšŸ» - starting from running the command till receiving an error.** + - Follow [the guide to enable debug mode](https://currents.dev/readme/integration-with-cypress/troubleshooting#enabling-debug-mode-for-cypress-cloud-1.9.0+) + - **šŸ‘‰šŸ» Include the full log šŸ‘ˆšŸ»** - starting from running the command till receiving an error. - Attach a link / file for long outputs. Example: - - Linux: `DEBUG=currents:*,cypress:* cypress-cloud run ...` - - Windows: `cmd /V /C "set DEBUG=currents:*,cypress:* && cypress-cloud run ..."` + - `npx cypress-cloud run ... --cloud-debug` + + **Remove any sensitive data.** - **Be sure to remove any sensitive data.** value: |
diff --git a/.github/README.md b/.github/README.md index 3a3dd22f..918d1a8a 100644 --- a/.github/README.md +++ b/.github/README.md @@ -16,6 +16,33 @@ Integrate Cypress with alternative cloud services like Currents or Sorry Cypress

+## Table of Contents + +- [Requirements](#requirements) +- [Setup](#setup) +- [Usage](#usage) +- [Example](#example) +- [Configuration](#configuration) + - [Configuration File Discovery](#configuration-file-discovery) + - [Configuration Overrides](#configuration-overrides) +- [Batched Orchestration](#batched-orchestration) +- [API](#api) + - [`run`](#run) +- [Guides](#guides) + - [Usage with `@cypress/grep`](#usage-with-cypressgrep) + - [Setup with existing plugins](#setup-with-existing-plugins) + - [Preserving `config.env` values](#preserving-configenv-values) + - [Chaining `config`](#chaining-config) + - [Event callbacks for multiple plugins](#event-callbacks-for-multiple-plugins) +- [Spec files discovery](#spec-files-discovery) +- [Usage with ESM project](#usage-with-esm-project) +- [Troubleshooting](#troubleshooting) +- [Testing](#testing) +- [Releasing](#releasing) + - [Beta channel](#beta-channel) + - [Latest channel](#latest-channel) + - [Localhost](#localhost) + ## Requirements - Cypress version 10+ @@ -44,14 +71,17 @@ module.exports = { Add `cypress-cloud/plugin` to `cypress.config.{js|ts|mjs}` -```js +```ts // cypress.config.js -const { defineConfig } = require("cypress"); -const { cloudPlugin } = require("cypress-cloud/plugin"); -module.exports = defineConfig({ +import { defineConfig } from "cypress"; +import { cloudPlugin } from "cypress-cloud/plugin"; + +export default defineConfig({ e2e: { - setupNodeEvents(on, config) { - return cloudPlugin(on, config); + video: true; // enable video for cypress@13+ + async setupNodeEvents(on, config) { + const result = await cloudPlugin(on, config); + return result; }, }, }); @@ -60,7 +90,7 @@ module.exports = defineConfig({ Add `cypress-cloud/support` to Cypress Support file (matching your test type - e2e or component, or both) ```ts -import `cypress-cloud/support` +import "cypress-cloud/support"; ``` ## Usage @@ -89,7 +119,7 @@ module.exports = { networkHeaders: { "User-Agent": "Custom", "x-ms-blob-type": "BlockBlob" - } + }, e2e: { batchSize: 3, // orchestration batch size for e2e tests (Currents only, read below) }, @@ -132,7 +162,7 @@ The configuration variables will resolve as follows: ## Batched Orchestration -This package uses its own orchestration and reporting protocol that is independent of cypress native implementation. The new [orchestration protocol](https://currents.dev/readme/integration-with-cypress/cypress-cloud#batched-orchestration) uses cypress in "offline" mode and allows batching multiple spec files for better efficiency. You can adjust the batching configuration in `currents.config.js` and use different values for e2e and component tests. +This package uses its own orchestration and reporting protocol that is independent of cypress native implementation. The new [orchestration protocol]([https://currents.dev/readme/integration-with-cypress/cypress-cloud#batched-orchestration](https://currents.dev/readme/integration-with-cypress/cypress-cloud/batched-orchestration)) uses cypress in "offline" mode and allows batching multiple spec files for better efficiency. You can adjust the batching configuration in `currents.config.js` and use different values for e2e and component tests. ## API @@ -167,48 +197,108 @@ const results = await run({ ### Usage with `@cypress/grep` -The package is compatible with [`@cypress/grep`](https://www.npmjs.com/package/@cypress/grep). Make sure to run `require("@cypress/grep/src/plugin")(config);` before `await currents(on, config);`, for example: +The package is compatible with [`@cypress/grep`](https://www.npmjs.com/package/@cypress/grep). -```js -setupNodeEvents(on, config) { - require("cypress-terminal-report/src/installLogsPrinter")(on); - require("@cypress/grep/src/plugin")(config); - return currents(on, config); -} +`@cypress/grep` modifies cypress configuration and alters `specPattern` property. Install `@cypress/grep` **before** `cypress-cloud/plugin` to apply the modified configuration. For example: + +```ts +import { defineConfig } from "cypress"; +import grepPlugin from "@cypress/grep/src/plugin"; +import { cloudPlugin } from "cypress-cloud/plugin"; + +export default defineConfig({ + e2e: { + // ... + async setupNodeEvents(on, config) { + grepPlugin(config); + const result = await cloudPlugin(on, config); + return result; + }, + }, +}); ``` Please refer to the [issue](https://github.com/currents-dev/cypress-cloud/issues/50#issuecomment-1645095284) for details. ### Setup with existing plugins -`cypress-cloud/plugin` needs access to certain environment variables that are injected into the `config` parameter of `setupNodeEvents(on, config)`. +#### Preserving `config.env` values -Please make sure to preserve the original `config.env` parameters in case you are using additional plugins, e.g.: +The `config` parameter of `setupNodeEvents(on, config)` has pre-defined `config.env` values. Please make sure to preserve the original `config.env` value when altering the property. For example: -```js -const { defineConfig } = require("cypress"); -const { cloudPlugin } = require("cypress-cloud/plugin"); +```ts +import { defineConfig } from "cypress"; +import { cloudPlugin } from "cypress-cloud/plugin"; -module.exports = defineConfig({ +export default defineConfig({ e2e: { // ... - setupNodeEvents(on, config) { - // alternative: activate the plugin first - // cloudPlugin(on, config) + async setupNodeEvents(on, config) { const enhancedConfig = { env: { - // preserve the original env - ...config.env, + ...config.env, // šŸ‘ˆšŸ» preserve the original env customVariable: "value", }, }; - return cloudPlugin(on, enhancedConfig); + const result = await cloudPlugin(on, enhancedConfig); + return result; + }, + }, +}); +``` + +#### Chaining `config` + +Certain plugins (e.g. `@cypress/grep`) modify or alter the `config` parameter and change the default Cypress behaviour. Make sure that `cypress-cloud` is initialized with the most recently updated `config`, e.g.: + +```ts +import { defineConfig } from "cypress"; +import { cloudPlugin } from "cypress-cloud/plugin"; + +export default defineConfig({ + e2e: { + // ... + async setupNodeEvents(on, config) { + const configA = pluginA(on, config); // configA has the modified config from pluginA + const configB = pluginB(on, configA); // configA has the modified config from pluginA + pluginB + // ... + const configX = pluginX(on, configY); // configX has the modified config from all preceding plugins + const result = await cloudPlugin(on, configX); // cloudPlugin has the accumulated config from all plugins + return result; }, }, }); ``` -As an alternative, you can activate the `cloudPlugin` first, and then implement the custom setup. Please contact our support if you have a complex plugin configuration to get assistance with the setup. +#### Event callbacks for multiple plugins + +`cypress-cloud/plugin` uses certain Cypress Plugin events. Unfortunately if there are mutliple listeners for an event, only the last listener is called (see the [GitHub issue](https://github.com/cypress-io/cypress/issues/22428)). Setups with multiple plugins can create conflicts - one plugin can replace listeners of others. + +The existing workaround is to patch the `on` function by using either of: + +- https://github.com/bahmutov/cypress-on-fix +- https://github.com/elaichenkov/cypress-plugin-init + +For example: + +```ts +import { defineConfig } from "cypress"; +import { cloudPlugin } from "cypress-cloud/plugin"; +import patchCypressOn from "cypress-on-fix"; + +export default defineConfig({ + e2e: { + // ... + async setupNodeEvents(cypressOn, config) { + const on = patchCypressOn(cypressOn); + // the rest of the plugins use the patched "on" function + const configAlt = somePlugin(on, config); + const result = await cloudPlugin(on, configAlt); + return result; + }, + }, +}); +``` ### Spec files discovery diff --git a/.github/workflows/e2e-smoke-windows.yaml b/.github/e2e-smoke-windows.yaml similarity index 100% rename from .github/workflows/e2e-smoke-windows.yaml rename to .github/e2e-smoke-windows.yaml diff --git a/.github/workflows/e2e-exports.yml b/.github/workflows/e2e-exports.yml index 4ea488ae..ac9ee190 100644 --- a/.github/workflows/e2e-exports.yml +++ b/.github/workflows/e2e-exports.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: ["18", "16", "14"] + node-version: ["18", "20"] steps: - uses: actions/checkout@v3 @@ -20,7 +20,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Install npm (node14) + - name: Install npm run: npm install -g npm@latest - name: Install dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd661b7..8d96df29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ +## [1.9.6](https://github.com/currents-dev/cypress-cloud/compare/v1.9.5...v1.9.6) (2023-09-19) + + +### Bug Fixes + +* add post-run warnings ([#180](https://github.com/currents-dev/cypress-cloud/issues/180)) [CSR-601] ([e8d9354](https://github.com/currents-dev/cypress-cloud/commit/e8d93540d1aefb1235c0d478f4b39920d3267b9e)) + ## [1.9.5](https://github.com/currents-dev/cypress-cloud/compare/v1.9.4...v1.9.5) (2023-09-13) diff --git a/examples/webapp/cypress.config.ts b/examples/webapp/cypress.config.ts index f50a4805..39e2aada 100644 --- a/examples/webapp/cypress.config.ts +++ b/examples/webapp/cypress.config.ts @@ -1,5 +1,7 @@ +import grepPlugin from "@cypress/grep/src/plugin"; import { defineConfig } from "cypress"; -import currents from "cypress-cloud/plugin"; +import { cloudPlugin } from "cypress-cloud/plugin"; +import patchCypressOn from "cypress-on-fix"; module.exports = defineConfig({ e2e: { @@ -10,17 +12,19 @@ module.exports = defineConfig({ videoUploadOnPasses: false, supportFile: "cypress/support/e2e.ts", specPattern: "cypress/*/**/*.spec.js", - setupNodeEvents(on, config) { - require("@cypress/grep/src/plugin")(config); - require("cypress-terminal-report/src/installLogsPrinter")(on); - return currents(on, config); + async setupNodeEvents(cyOn, config) { + const on = patchCypressOn(cyOn); + grepPlugin(config); + const result = await cloudPlugin(on, config); + return result; }, }, component: { specPattern: ["pages/__tests__/*.spec.tsx"], - setupNodeEvents(on, config) { - return currents(on, config); + async setupNodeEvents(on, config) { + const result = await cloudPlugin(on, config); + return result; }, devServer: { framework: "next", diff --git a/examples/webapp/cypress/support/e2e.ts b/examples/webapp/cypress/support/e2e.ts index 32bd2596..36c49908 100644 --- a/examples/webapp/cypress/support/e2e.ts +++ b/examples/webapp/cypress/support/e2e.ts @@ -1,5 +1,4 @@ import registerCypressGrep from "@cypress/grep/src/support"; -require("cypress-terminal-report/src/installLogsCollector")(); require("cypress-cloud/support"); require("./commands"); diff --git a/examples/webapp/package.json b/examples/webapp/package.json index 7fcee08f..ddc938f7 100644 --- a/examples/webapp/package.json +++ b/examples/webapp/package.json @@ -12,6 +12,7 @@ "dependencies": { "cypress": "^12.6.0", "cypress-cloud": "*", + "cypress-on-fix": "^1.0.2", "cypress-terminal-report": "^5.3.3", "next": "^13.2.1", "react": "^18.2.0", diff --git a/package-lock.json b/package-lock.json index 4cc6a584..69c5abd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,6 +113,7 @@ "dependencies": { "cypress": "^12.6.0", "cypress-cloud": "*", + "cypress-on-fix": "^1.0.2", "cypress-terminal-report": "^5.3.3", "next": "^13.2.1", "react": "^18.2.0", @@ -7005,6 +7006,11 @@ "resolved": "packages/cypress-cloud", "link": true }, + "node_modules/cypress-on-fix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cypress-on-fix/-/cypress-on-fix-1.0.2.tgz", + "integrity": "sha512-oN/PW7FsC3Y7xC9Z76KzAVXWH5VNPrt+FWjMlSYDPQtEOYedVNBLu7dz9CEU6Ntt5kD0MHuXAyO8Ov2YNBf9lg==" + }, "node_modules/cypress-terminal-report": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/cypress-terminal-report/-/cypress-terminal-report-5.3.3.tgz", @@ -9591,6 +9597,14 @@ "dev": true, "license": "MIT" }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-absolute": { "version": "1.0.0", "license": "MIT", @@ -15716,7 +15730,7 @@ } }, "packages/cypress-cloud": { - "version": "1.9.5", + "version": "1.9.6", "license": "GPL-3.0-or-later", "dependencies": { "@cypress/commit-info": "^2.2.0", @@ -15735,6 +15749,7 @@ "lil-http-terminator": "^1.2.3", "lodash": "^4.17.21", "nanoid": "^3.3.4", + "plur": "^4.0.0", "pretty-ms": "^7.0.1", "source-map-support": "^0.5.21", "table": "^6.8.1", @@ -16729,6 +16744,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/cypress-cloud/node_modules/plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/cypress-cloud/node_modules/proxy-agent": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", @@ -21453,6 +21482,7 @@ "lodash": "^4.17.21", "nanoid": "^3.3.4", "nock": "^13.2.9", + "plur": "^4.0.0", "pretty-ms": "^7.0.1", "release-it": "^16.1.5", "rimraf": "^3.0.2", @@ -22149,6 +22179,14 @@ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true }, + "plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "requires": { + "irregular-plurals": "^3.2.0" + } + }, "proxy-agent": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", @@ -22364,6 +22402,11 @@ } } }, + "cypress-on-fix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cypress-on-fix/-/cypress-on-fix-1.0.2.tgz", + "integrity": "sha512-oN/PW7FsC3Y7xC9Z76KzAVXWH5VNPrt+FWjMlSYDPQtEOYedVNBLu7dz9CEU6Ntt5kD0MHuXAyO8Ov2YNBf9lg==" + }, "cypress-terminal-report": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/cypress-terminal-report/-/cypress-terminal-report-5.3.3.tgz", @@ -23950,6 +23993,11 @@ "version": "1.1.8", "dev": true }, + "irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==" + }, "is-absolute": { "version": "1.0.0", "requires": { @@ -27552,6 +27600,7 @@ "@types/react-dom": "^18.0.11", "cypress": "^12.6.0", "cypress-cloud": "*", + "cypress-on-fix": "^1.0.2", "cypress-terminal-report": "^5.3.3", "eslint": "7.32.0", "eslint-config-custom": "*", diff --git a/packages/cypress-cloud/bin/lib/program.ts b/packages/cypress-cloud/bin/lib/program.ts index e694397f..0d976bc6 100644 --- a/packages/cypress-cloud/bin/lib/program.ts +++ b/packages/cypress-cloud/bin/lib/program.ts @@ -107,7 +107,7 @@ ${getLegalNotice()} .addOption( new Option( "--cloud-config-file ", - "Specify the config file for cypress-cloud, defaults to 'currents.config.js' and will be searched in the project root, unless an aboslue path is provided" + "Specify the config file for cypress-cloud, defaults to 'currents.config.js' and will be searched in the project root, unless an absolute path is provided" ).default(undefined) ) .addOption( diff --git a/packages/cypress-cloud/lib/artifacts.ts b/packages/cypress-cloud/lib/artifacts.ts index ed6accc5..ca3dc478 100644 --- a/packages/cypress-cloud/lib/artifacts.ts +++ b/packages/cypress-cloud/lib/artifacts.ts @@ -2,10 +2,12 @@ import Debug from "debug"; import { ScreenshotArtifact, ScreenshotUploadInstruction } from "../types"; import { updateInstanceStdout } from "./api"; import { safe } from "./lang"; -import { warn } from "./log"; +import { dim } from "./log"; +import { ExecutionState } from "./state"; import { uploadImage, uploadJson, uploadVideo } from "./upload"; const debug = Debug("currents:artifacts"); interface UploadArtifacts { + executionState: ExecutionState; videoPath: string | null; videoUploadUrl?: string | null; screenshots: ScreenshotArtifact[]; @@ -14,6 +16,7 @@ interface UploadArtifacts { coverageFilePath?: string | null; } export async function uploadArtifacts({ + executionState, videoPath, videoUploadUrl, screenshots, @@ -21,8 +24,6 @@ export async function uploadArtifacts({ coverageFilePath, coverageUploadUrl, }: UploadArtifacts) { - // title("blue", "Uploading Results"); - debug("uploading artifacts: %o", { videoPath, videoUploadUrl, @@ -33,9 +34,8 @@ export async function uploadArtifacts({ }); const totalUploads = - (videoPath ? 1 : 0) + screenshots.length + (coverageFilePath ? 1 : 0); + (videoPath ? 1 : 0) + screenshots.length + (coverageUploadUrl ? 1 : 0); if (totalUploads === 0) { - // info("Nothing to upload"); return; } @@ -43,7 +43,12 @@ export async function uploadArtifacts({ if (videoUploadUrl && videoPath) { await safe( uploadVideo, - (e) => debug("failed uploading video %s. Error: %o", videoPath, e), + (e) => { + debug("failed uploading video %s. Error: %o", videoPath, e); + executionState.addWarning( + `Failed uploading video ${videoPath}.\n${dim(e)}` + ); + }, () => debug("success uploading", videoPath) )(videoPath, videoUploadUrl); } @@ -60,17 +65,23 @@ export async function uploadArtifacts({ screenshot, screenshotUploadUrls ); - warn("Cannot find upload url for screenshot: %s", screenshot.path); + executionState.addWarning( + `No upload URL for screenshot ${screenshot.path}` + ); return Promise.resolve(); } return safe( uploadImage, - (e) => + (e) => { debug( "failed uploading screenshot %s. Error: %o", screenshot.path, e - ), + ); + executionState.addWarning( + `Failed uploading screenshot ${screenshot.path}.\n${dim(e)}` + ); + }, () => debug("success uploading", screenshot.path) )(screenshot.path, url); }) @@ -80,12 +91,18 @@ export async function uploadArtifacts({ if (coverageUploadUrl && coverageFilePath) { await safe( uploadJson, - (e) => + (e) => { debug( "failed uploading coverage file %s. Error: %o", coverageFilePath, e - ), + ); + + executionState.addWarning( + `Failed uploading coverage file ${coverageFilePath}.\n${dim(e)}` + ); + }, + () => debug("success uploading", coverageFilePath) )(coverageFilePath, coverageUploadUrl); } diff --git a/packages/cypress-cloud/lib/coverage/index.ts b/packages/cypress-cloud/lib/coverage/index.ts index 5ad7cc86..be881ae6 100644 --- a/packages/cypress-cloud/lib/coverage/index.ts +++ b/packages/cypress-cloud/lib/coverage/index.ts @@ -1,6 +1,5 @@ -import { join } from "path"; import fs from "fs/promises"; -import { warn } from "../log"; +import { join } from "path"; export const getCoverageFilePath = async ( coverageFile = "./.nyc_output/out.json" @@ -9,12 +8,14 @@ export const getCoverageFilePath = async ( try { await fs.access(path); - return path; + return { + path, + error: false, + }; } catch (error) { - warn( - 'Coverage file was not found at "%s". Coverage recording will be skipped.', - path - ); - return null; + return { + path, + error, + }; } }; diff --git a/packages/cypress-cloud/lib/log.ts b/packages/cypress-cloud/lib/log.ts index 6f0d6ac3..87d1f70e 100644 --- a/packages/cypress-cloud/lib/log.ts +++ b/packages/cypress-cloud/lib/log.ts @@ -4,6 +4,7 @@ import util from "util"; const log = (...args: unknown[]) => console.log(util.format(...args)); export const info = log; +export const format = util.format; export const withError = (msg: string) => chalk.bgRed.white(" ERROR ") + " " + msg; @@ -37,3 +38,5 @@ export const gray = chalk.gray; export const white = chalk.white; export const magenta = chalk.magenta; export const bold = chalk.bold; +export const yellow = chalk.yellow; +export const dim = chalk.dim; diff --git a/packages/cypress-cloud/lib/results/uploadResults.ts b/packages/cypress-cloud/lib/results/uploadResults.ts index 65955d7c..61443eac 100644 --- a/packages/cypress-cloud/lib/results/uploadResults.ts +++ b/packages/cypress-cloud/lib/results/uploadResults.ts @@ -10,15 +10,18 @@ import { uploadArtifacts, uploadStdoutSafe } from "../artifacts"; import { setCancellationReason } from "../cancellation"; import { getInitialOutput } from "../capture"; import { isCurrents } from "../env"; +import { ConfigState, ExecutionState } from "../state"; import { getInstanceResultPayload, getInstanceTestsPayload } from "./results"; const debug = Debug("currents:results"); export async function getReportResultsTask( instanceId: string, - results: CypressCommandLine.CypressRunResult, + configState: ConfigState, + executionState: ExecutionState, stdout: string, coverageFilePath?: string ) { + const results = executionState.getInstanceResults(configState, instanceId); const run = results.runs[0]; if (!run) { throw new Error("No run found in Cypress results"); @@ -41,6 +44,7 @@ export async function getReportResultsTask( return Promise.all([ uploadArtifacts({ + executionState, videoUploadUrl, videoPath: run.video, screenshotUploadUrls, diff --git a/packages/cypress-cloud/lib/run.ts b/packages/cypress-cloud/lib/run.ts index 5e7cf209..1610f160 100644 --- a/packages/cypress-cloud/lib/run.ts +++ b/packages/cypress-cloud/lib/run.ts @@ -1,6 +1,7 @@ import "./init"; import Debug from "debug"; +import plur from "plur"; import { getLegalNotice } from "../legal"; import { CurrentsRunParameters } from "../types"; import { createRun } from "./api"; @@ -12,12 +13,13 @@ import { preprocessParams, validateParams, } from "./config"; +import { getCoverageFilePath } from "./coverage"; import { runBareCypress } from "./cypress"; import { activateDebug } from "./debug"; import { isCurrents } from "./env"; import { getGitInfo } from "./git"; import { setAPIBaseUrl, setRunId } from "./httpClient"; -import { bold, divider, info, spacer, title } from "./log"; +import { bold, dim, divider, info, spacer, title, warn, yellow } from "./log"; import { getPlatform } from "./platform"; import { pubsub } from "./pubsub"; import { summarizeTestResults, summaryTable } from "./results"; @@ -30,7 +32,6 @@ import { shutdown } from "./shutdown"; import { getSpecFiles } from "./specMatcher"; import { ConfigState, ExecutionState } from "./state"; import { startWSS } from "./ws"; -import { getCoverageFilePath } from "./coverage"; const debug = Debug("currents:run"); @@ -144,7 +145,10 @@ export async function run(params: CurrentsRunParameters = {}) { title("white", "Cloud Run Finished"); console.log(summaryTable(_summary)); - info("šŸ Recorded Run:", bold(run.runUrl)); + + printWarnings(executionState); + + info("\nšŸ Recorded Run:", bold(run.runUrl)); await shutdown(); @@ -176,15 +180,34 @@ function listenToSpecEvents( debug("after:spec %o %o", spec, results); executionState.setSpecAfter(spec.relative, results); executionState.setSpecOutput(spec.relative, getCapturedOutput()); + if (experimentalCoverageRecording) { - const coverageFilePath = await getCoverageFilePath( + const { path, error } = await getCoverageFilePath( config?.env?.coverageFile ); - if (coverageFilePath) { - executionState.setSpecCoverage(spec.relative, coverageFilePath); + if (!error) { + executionState.setSpecCoverage(spec.relative, path); + } else { + executionState.addWarning( + `Could not process coverage file "${path}"\n${dim(error)}` + ); } } createReportTaskSpec(configState, executionState, spec.relative); } ); } + +function printWarnings(executionState: ExecutionState) { + const warnings = Array.from(executionState.getWarnings()); + if (warnings.length > 0) { + warn( + `${warnings.length} ${plur( + "Warning", + warnings.length + )} encountered during the execution:\n${warnings + .map((w, i) => `\n${yellow(`[${i + 1}/${warnings.length}]`)} ${w}`) + .join("\n")}` + ); + } +} diff --git a/packages/cypress-cloud/lib/runner/reportTask.ts b/packages/cypress-cloud/lib/runner/reportTask.ts index 5f7dcc0d..6be3bae8 100644 --- a/packages/cypress-cloud/lib/runner/reportTask.ts +++ b/packages/cypress-cloud/lib/runner/reportTask.ts @@ -29,7 +29,8 @@ export const createReportTask = ( reportTasks.push( getReportResultsTask( instanceId, - executionState.getInstanceResults(configState, instanceId), + configState, + executionState, instance.output ?? "no output captured", instance.coverageFilePath ).catch(error) diff --git a/packages/cypress-cloud/lib/state/execution.ts b/packages/cypress-cloud/lib/state/execution.ts index 50a8a083..e149112b 100644 --- a/packages/cypress-cloud/lib/state/execution.ts +++ b/packages/cypress-cloud/lib/state/execution.ts @@ -26,8 +26,17 @@ type InstanceExecutionState = { }; export class ExecutionState { + private warnings = new Set(); private state: Record = {}; + public getWarnings() { + return this.warnings; + } + + public addWarning(warning: string) { + this.warnings.add(warning); + } + public getResults(configState: ConfigState) { return Object.values(this.state).map((i) => this.getInstanceResults(configState, i.instanceId) diff --git a/packages/cypress-cloud/package.json b/packages/cypress-cloud/package.json index e357709f..77327e15 100644 --- a/packages/cypress-cloud/package.json +++ b/packages/cypress-cloud/package.json @@ -1,6 +1,6 @@ { "name": "cypress-cloud", - "version": "1.9.5", + "version": "1.9.6", "main": "./dist/index.js", "author": "Currents Software Inc", "homepage": "https://github.com/currents-dev/cypress-cloud", @@ -69,6 +69,7 @@ "lil-http-terminator": "^1.2.3", "lodash": "^4.17.21", "nanoid": "^3.3.4", + "plur": "^4.0.0", "pretty-ms": "^7.0.1", "source-map-support": "^0.5.21", "table": "^6.8.1",