├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── add-issue-to-triage-board.yml │ ├── snyk_sca_scan.yaml │ ├── snyk_static_analysis_scan.yaml │ └── triage_closed_issue_comment.yml ├── .gitignore ├── .prettierrc.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── cypress.config.js ├── cypress └── e2e │ └── spec.js ├── images ├── beta.png └── netlify-to-cy-gh-app-checks.png ├── manifest.yml ├── netlify.toml ├── package-lock.json ├── package.json ├── public └── index.html ├── renovate.json ├── src ├── constants.js ├── index.js ├── onPostBuild.js ├── onPostBuild.test.js ├── onPreBuild.js ├── onSuccess.js ├── ping-cli.js ├── ping.js ├── serve-cli.js └── utils.js └── tests ├── basic ├── README.md ├── cypress.config.js ├── cypress │ └── e2e │ │ └── spec.cy.js ├── netlify.toml └── public │ └── index.html ├── config-file ├── README.md ├── cypress.config.js ├── cypress.netlify.config.js ├── cypress │ ├── e2e │ │ └── spec.cy.js │ ├── fixtures │ │ └── example.json │ └── support │ │ ├── commands.js │ │ └── e2e.js ├── netlify.toml ├── package.json └── public │ └── index.html ├── html-pages ├── README.md ├── cypress.config.js ├── cypress │ └── e2e │ │ └── spec.cy.js ├── netlify.toml └── public │ ├── commands │ └── about.html │ └── index.html ├── recommended ├── README.md ├── cypress.config.js ├── cypress │ └── e2e │ │ └── spec.cy.js ├── netlify.toml └── public │ └── index.html ├── recording ├── README.md ├── cypress.config.js ├── cypress │ └── e2e │ │ └── spec.js ├── netlify.toml └── public │ └── index.html ├── routing ├── .env ├── README.md ├── cypress.config.js ├── cypress │ └── e2e │ │ └── spec.cy.js ├── images │ └── routing.png ├── netlify.toml ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── index.css │ ├── index.js │ └── serviceWorker.js ├── test-netlify-dev ├── README.md ├── cypress.config.js ├── cypress │ └── e2e │ │ └── spec.cy.js ├── netlify.toml ├── package.json ├── public │ └── index.html └── src │ └── functions │ └── hello.js ├── test-postbuild-start ├── README.md ├── cypress.config.js ├── cypress │ └── e2e │ │ └── spec.js ├── netlify.toml ├── package.json └── public │ └── index.html ├── test-prebuild-only ├── README.md ├── cypress.config.js ├── cypress │ └── e2e │ │ └── spec.js ├── netlify.toml ├── package.json └── public │ └── index.html ├── test-twice ├── README.md ├── cypress.config.js ├── cypress │ └── e2e │ │ └── spec.js ├── netlify.toml └── public │ └── index.html └── use-chromium ├── README.md ├── cypress.config.js ├── cypress └── e2e │ └── spec.cy.js ├── netlify.toml └── public └── index.html /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # The default executor for cypress-io/cypress@3.0.0 orb is 16.16.0 2 | # this executor node version should also be in parity with the node version 3 | # in netlify: https://app.netlify.com/sites/netlify-plugin-cypress 4 | # To update, be sure to set the NODE_VERSION inside netlify when updating this 5 | # executor. 6 | # @see https://docs.netlify.com/configure-builds/environment-variables/#netlify-configuration-variables 7 | defaultCypressOrbConfig: &defaultCypressOrbConfig 8 | executor: 9 | name: cypress/default 10 | node-version: '18.17.1' 11 | 12 | version: 2.1 13 | orbs: 14 | node: circleci/node@5.1.0 15 | cypress: cypress-io/cypress@4.1.0 16 | jobs: 17 | build: 18 | executor: 19 | name: node/default 20 | tag: '18.17.1' 21 | steps: 22 | - checkout 23 | - node/install-packages: 24 | cache-path: ~/project/node_modules 25 | override-ci-command: npm install 26 | - run: 27 | name: show package contents 📦 28 | command: npm pack --dry 29 | - run: 30 | name: unit tests 🧪 31 | command: npm run test:unit 32 | install_and_persist: 33 | <<: *defaultCypressOrbConfig 34 | steps: 35 | - cypress/install: 36 | # --force is needed to avoid unsupported platform issues with netlify cli dependencies on @esbuild/android-arm 37 | install-command: npm ci --force 38 | - persist_to_workspace: 39 | paths: 40 | - .cache/Cypress 41 | - project 42 | root: ~/ 43 | 44 | release: 45 | executor: 46 | name: node/default 47 | tag: '18.17.1' 48 | environment: 49 | # since we do not need Cypress to publish the NPM package 50 | # we can skip the binary download 51 | CYPRESS_INSTALL_BINARY: 0 52 | # we also skip Puppeteer Chromium download 53 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true 54 | 55 | steps: 56 | - checkout 57 | - node/install-packages: 58 | cache-path: ~/project/node_modules 59 | override-ci-command: npm install 60 | # allow CircleCI to release beta versions 61 | # from pull request build jobs 62 | - run: 63 | name: Semantic release 🚀 64 | command: npx semantic-release 65 | # by tricking Circle and removing the environment variables 66 | environment: 67 | CIRCLE_PR_NUMBER: 68 | CIRCLE_PULL_REQUEST: 69 | CI_PULL_REQUEST: 70 | 71 | 'basic test': 72 | <<: *defaultCypressOrbConfig 73 | steps: 74 | # all dependencies were installed in previous job 75 | - attach_workspace: 76 | at: ~/ 77 | - run: 78 | name: Netlify Build 🏗 79 | command: BUILD_ID=$CIRCLE_BUILD_NUM npx netlify build 80 | working_directory: tests/basic 81 | environment: 82 | DEBUG: netlify-plugin-cypress 83 | 84 | 'recommended test': 85 | <<: *defaultCypressOrbConfig 86 | steps: 87 | # all dependencies were installed in previous job 88 | - attach_workspace: 89 | at: ~/ 90 | - run: 91 | name: Netlify Build 🏗 92 | command: BUILD_ID=$CIRCLE_BUILD_NUM npx netlify build 93 | working_directory: tests/recommended 94 | environment: 95 | DEBUG: netlify-plugin-cypress 96 | 97 | 'recording test': 98 | <<: *defaultCypressOrbConfig 99 | steps: 100 | # all dependencies were installed in previous job 101 | - attach_workspace: 102 | at: ~/ 103 | - run: 104 | name: Netlify Build 🏗 105 | command: BUILD_ID=$CIRCLE_BUILD_NUM npx netlify build 106 | working_directory: tests/recording 107 | environment: 108 | DEBUG: netlify-plugin-cypress 109 | 110 | 'test-twice': 111 | <<: *defaultCypressOrbConfig 112 | steps: 113 | # all dependencies were installed in previous job 114 | - attach_workspace: 115 | at: ~/ 116 | - run: 117 | name: Netlify Build 🏗 118 | command: BUILD_ID=$CIRCLE_BUILD_NUM npx netlify build 119 | working_directory: tests/test-twice 120 | environment: 121 | DEBUG: netlify-plugin-cypress 122 | 123 | 'test-prebuild-only': 124 | <<: *defaultCypressOrbConfig 125 | steps: 126 | # all dependencies were installed in previous job 127 | - attach_workspace: 128 | at: ~/ 129 | - run: 130 | name: Netlify Build 🏗 131 | command: npm run netlify:build 132 | working_directory: tests/test-prebuild-only 133 | environment: 134 | DEBUG: netlify-plugin-cypress 135 | 136 | 'test-postbuild-start': 137 | <<: *defaultCypressOrbConfig 138 | steps: 139 | # all dependencies were installed in previous job 140 | - attach_workspace: 141 | at: ~/ 142 | - run: 143 | name: Netlify Build 🏗 144 | command: npm run netlify:build 145 | working_directory: tests/test-postbuild-start 146 | environment: 147 | DEBUG: netlify-plugin-cypress 148 | 149 | 'test-using-chromium': 150 | <<: *defaultCypressOrbConfig 151 | steps: 152 | # all dependencies were installed in previous job 153 | - attach_workspace: 154 | at: ~/ 155 | - run: 156 | name: Netlify Build 🏗 157 | command: BUILD_ID=$CIRCLE_BUILD_NUM npx netlify build 158 | working_directory: tests/use-chromium 159 | environment: 160 | DEBUG: netlify-plugin-cypress 161 | 162 | 'test-netlify-dev': 163 | <<: *defaultCypressOrbConfig 164 | steps: 165 | # all dependencies were installed in previous job 166 | - attach_workspace: 167 | at: ~/ 168 | - run: 169 | name: Netlify Build 🏗 170 | command: npm run netlify:build 171 | working_directory: tests/test-netlify-dev 172 | environment: 173 | DEBUG: netlify-plugin-cypress 174 | 175 | 'html-pages': 176 | <<: *defaultCypressOrbConfig 177 | steps: 178 | # all dependencies were installed in previous job 179 | - attach_workspace: 180 | at: ~/ 181 | - run: 182 | name: Netlify Build 🏗 183 | command: BUILD_ID=$CIRCLE_BUILD_NUM npx netlify build 184 | working_directory: tests/html-pages 185 | environment: 186 | DEBUG: netlify-plugin-cypress 187 | 188 | routing: 189 | <<: *defaultCypressOrbConfig 190 | steps: 191 | # all dependencies were installed in previous job 192 | - attach_workspace: 193 | at: ~/ 194 | - run: 195 | name: Netlify Build 🏗 196 | command: npm run netlify:build 197 | working_directory: tests/routing 198 | environment: 199 | DEBUG: netlify-plugin-cypress 200 | 201 | config-file: 202 | <<: *defaultCypressOrbConfig 203 | steps: 204 | # all dependencies were installed in previous job 205 | - attach_workspace: 206 | at: ~/ 207 | - run: 208 | name: Netlify Build 🏗 209 | command: npm run netlify:build 210 | working_directory: tests/config-file 211 | environment: 212 | DEBUG: netlify-plugin-cypress 213 | 214 | workflows: 215 | version: 2 216 | test_and_release: 217 | jobs: 218 | - build 219 | - install_and_persist 220 | - 'basic test': 221 | requires: 222 | - install_and_persist 223 | - 'html-pages': 224 | requires: 225 | - install_and_persist 226 | - 'recommended test': 227 | requires: 228 | - install_and_persist 229 | - 'recording test': 230 | requires: 231 | - install_and_persist 232 | - 'test-twice': 233 | requires: 234 | - install_and_persist 235 | - 'test-prebuild-only': 236 | requires: 237 | - install_and_persist 238 | - 'test-postbuild-start': 239 | requires: 240 | - install_and_persist 241 | - test-using-chromium: 242 | requires: 243 | - install_and_persist 244 | - test-netlify-dev: 245 | requires: 246 | - install_and_persist 247 | - routing: 248 | requires: 249 | - install_and_persist 250 | - config-file: 251 | requires: 252 | - install_and_persist 253 | - release: 254 | # run the release job on all branches 255 | # since we might want to release a beta version 256 | requires: 257 | - build 258 | - 'basic test' 259 | - 'html-pages' 260 | - 'recommended test' 261 | - 'recording test' 262 | - 'test-twice' 263 | - 'test-prebuild-only' 264 | - 'test-postbuild-start' 265 | - test-using-chromium 266 | - test-netlify-dev 267 | - 'routing' 268 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | **Versions** 9 | 10 | - What is this plugin's version? 11 | - What is Cypress version? 12 | - What Netlify build image are you using? This setting is available under "Deploy settings / Build image selection". Probably either "Ubuntu Trusty 14.04" or "Ubuntu Xenial 16.04" 13 | - What is the Node version if you know it? 14 | - What is the NPM version if you know it? 15 | 16 | **Describe the bug** 17 | A clear and concise description of what the bug is. 18 | 19 | **Logs and screenshots** 20 | If possible, add the log from the terminal. You can turn on debugging logging, see [Debugging](https://github.com/cypress-io/netlify-plugin-cypress#debugging) section of the README file. 21 | 22 | **Link to the repo** 23 | Bugs with a reproducible example, like an open source repo showing the bug, are the most likely to be resolved. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/add-issue-to-triage-board.yml: -------------------------------------------------------------------------------- 1 | name: 'Add issue/PR to Triage Board' 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | pull_request_target: 7 | types: 8 | - opened 9 | jobs: 10 | add-to-triage-project-board: 11 | uses: cypress-io/cypress/.github/workflows/triage_add_to_project.yml@develop 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/snyk_sca_scan.yaml: -------------------------------------------------------------------------------- 1 | name: Snyk Software Composition Analysis Scan 2 | # This git workflow leverages Snyk actions to perform a Software Composition 3 | # Analysis scan on our Opensource libraries upon Pull Requests to Master & 4 | # Develop branches. We use this as a control to prevent vulnerable packages 5 | # from being introduced into the codebase. 6 | on: 7 | pull_request_target: 8 | types: 9 | - opened 10 | branches: 11 | - master 12 | jobs: 13 | Snyk_SCA_Scan: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [18.x] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setting up Node 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Installing snyk-delta and dependencies 25 | run: npm i -g snyk-delta 26 | - uses: snyk/actions/setup@master 27 | - name: Perform SCA Scan 28 | continue-on-error: false 29 | run: | 30 | snyk test --all-projects --detection-depth=4 --exclude=docker,Dockerfile --severity-threshold=critical 31 | env: 32 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/snyk_static_analysis_scan.yaml: -------------------------------------------------------------------------------- 1 | name: Snyk Static Analysis Scan 2 | # This git workflow leverages Snyk actions to perform a Static Application 3 | # Testing scan (SAST) on our first-party code upon Pull Requests to Master & 4 | # Develop branches. We use this as a control to prevent vulnerabilities 5 | # from being introduced into the codebase. 6 | on: 7 | pull_request_target: 8 | types: 9 | - opened 10 | branches: 11 | - master 12 | jobs: 13 | Snyk_SAST_Scan : 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: snyk/actions/setup@master 18 | - name: Perform Static Analysis Test 19 | continue-on-error: true 20 | run: | 21 | snyk code test --all-projects --detection-depth=4 --exclude=docker,Dockerfile --severity-threshold=high 22 | env: 23 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 24 | # The Following Requires Advanced Security License 25 | # - name: Upload results to Github Code Scanning 26 | # uses: github/codeql-action/upload-sarif@v1 27 | # with: 28 | # sarif_file: snyk_sarif 29 | -------------------------------------------------------------------------------- /.github/workflows/triage_closed_issue_comment.yml: -------------------------------------------------------------------------------- 1 | name: 'Handle Comment Workflow' 2 | on: 3 | issue_comment: 4 | types: 5 | - created 6 | jobs: 7 | closed-issue-comment: 8 | uses: cypress-io/cypress/.github/workflows/triage_handle_new_comments.yml@develop 9 | secrets: inherit 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | **/cypress/screenshots 3 | **/cypress/videos 4 | build 5 | .DS_Store 6 | 7 | # Local Netlify folder 8 | .netlify 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to [code a plugin](https://github.com/netlify/build/blob/master/docs/creating-a-plugin.md) 2 | 3 | ## Testing 4 | 5 | End-to-end tests are in folder [tests](tests) and they use this [plugin locally](https://github.com/netlify/build/blob/master/README.md#using-a-local-plugin) and build each subfolder using Netlify CLI on CircleCI. You can find the test recordings at [Cypress Dashboard](https://dashboard.cypress.io/projects/ixroqc/) 6 | 7 | To run locally: 8 | 9 | - `npx netlify link` once 10 | - from every subfolder of `tests` execute `npm run build` 11 | 12 | ### Authentication (local) 13 | 14 | - use `netlify-cli` to authenticate locally. It will also create a local site and store its ID in `.netlify` folder (ignored by `git`) 15 | 16 | ### Authentication (ci) 17 | 18 | To authenticate `netlify` on CircleCI: 19 | - grab the Settings > Site information > APP ID and set it as environment variable `NETLIFY_SITE_ID` 20 | - generate new authentication token for CI from User Settings > Applications > New Access Token and set it as environment variable `NETLIFY_AUTH_TOKEN` 21 | 22 | ## Releasing a new version 23 | 24 | Before releasing check that each CircleCI job has actually run the tests correctly. We don't have a way to check if Cypress _executed all tests_, other than looking at the CircleCI terminal output. 25 | 26 | Try using `beta` branch to release new pre-release versions of the plugin by following [the semantic release guide](https://github.com/semantic-release/semantic-release/blob/master/docs/recipes/pre-releases.md). You can fork and test out new versions published to NPM using the [netlify-plugin-cypress-example](https://github.com/cypress-io/netlify-plugin-cypress-example) repository. Hope the `master` branch merged into `beta` does not bring features and fixes *already released*. Thus I suggest using `beta` branch for new features. 27 | 28 | **Do not open pull request on CircleCI** while adding new features to the `beta` branch. If there is a pull request, semantic-release refuses to publish new versions. Instead just watch the [builds on CircleCI](https://circleci.com/gh/cypress-io/netlify-plugin-cypress/tree/beta) directly and open the PR after the new version has been tested. 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | Copyright (c) 2018 Cypress.io https://cypress.io 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netlify-plugin-cypress 2 | [![CircleCI](https://circleci.com/gh/cypress-io/netlify-plugin-cypress.svg?style=shield)](https://circleci.com/gh/cypress-io/netlify-plugin-cypress/tree/master) [![renovate-app badge][renovate-badge]][renovate-app] [![netlify-plugin-cypress](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ixroqc/master&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/ixroqc/runs) [![Netlify Status](https://api.netlify.com/api/v1/badges/76892baf-2ad8-4642-b283-f2135963ff51/deploy-status)](https://app.netlify.com/sites/sad-lumiere-6a00a5/deploys) 3 | > Runs Cypress end-to-end tests on Netlify Build 4 | 5 | ## Install and use 6 | 7 | You can install this plugin in the Netlify UI from this [direct in-app installation link](https://app.netlify.com/plugins/netlify-plugin-cypress/install) or from the [Plugins directory](https://app.netlify.com/plugins). 8 | 9 | For file based installation, add `netlify-plugin-cypress` NPM package as a dev dependency to your repository. 10 | 11 | ```shell 12 | npm install --save-dev netlify-plugin-cypress 13 | # or 14 | yarn add -D netlify-plugin-cypress 15 | ``` 16 | 17 | And then add the plugin's name to the list of build plugins in `netlify.toml` file as shown in the examples below. 18 | 19 | *note:* this plugin assumes you have already installed Cypress as a dev NPM dependency. 20 | 21 | ### Chromium install 22 | 23 | This plugin installs [via Puppeteer](https://github.com/puppeteer/puppeteer) Chromium browser, which is also cached inside `./node_modules` folder. 24 | 25 | ## How does it work 26 | 27 | ### Build steps 28 | 29 | When Netlify Build system runs it performs 2 steps essentially: 30 | 31 | 1. builds the site 32 | 2. deploys the site 33 | 34 | Every plugin that wants to perform some actions can do so before the build, after the build (but before the deploy), and after the deploy. The Netlify uses the following names for these events 35 | 36 | ``` 37 | "preBuild" 38 | 1. builds the site 39 | "postBuild" 40 | 2. deploys the site 41 | "onSuccess" 42 | "onFailure" 43 | ``` 44 | 45 | Thus every plugin can register itself to be executed before a site is built using "preBuild" event, or after a successful deploy using "onSuccess" event name, etc. 46 | 47 | ### This plugin 48 | 49 | This plugin `netlify-plugin-cypress` by default runs during the "onSuccess" event, testing the deployed site. The Netlify Build system gives the URL to the plugin and it runs Cypress against that URL using the [Cypress NPM module API](https://on.cypress.io/module-api). 50 | 51 | Optionally, you can also run tests during "preBuild" and "postBuild" steps. This is useful if you want to ensure the site is working even before deploying it to Netlify servers. Finally, this plugin does not use "onFailure" event which happens only if Netlify fails to deploy the site. 52 | 53 | ### Failing the deploy 54 | 55 | Running Cypress tests by default uses the "onSuccess" step of the build pipeline. By this point Netlify has already deployed the site. Even if the tests fail now, the Netlify shows the successful deployment - the site is live! To really prevent the broken deploys, we suggest using [Cypress GitHub / GitLab / Bitbucket integration](https://on.cypress.io/github-integration) to fail the _status checks_ on a pull request. 56 | 57 | We also suggest running tests during the "preBuild" and/or "postBuild" steps. If the tests fail during these steps, the Netlify fails the entire build and does not deploy the broken site. 58 | 59 | Finally, you can set up [Slack notifications](https://on.cypress.io/slack-integration) on failed tests against the deployed site. At least you will quickly find out if the deployed site fails the E2E tests and would be able to roll back the deploy. 60 | 61 | ## Examples 62 | 63 | ### basic 64 | 65 | Here is the most basic [Netlify config file](https://docs.netlify.com/configure-builds/file-based-configuration/) `netlify.toml` with just the Cypress plugin 66 | 67 | ```toml 68 | [[plugins]] 69 | # runs Cypress tests against the deployed URL 70 | package = "netlify-plugin-cypress" 71 | ``` 72 | 73 | The example file above should be enough to run Cypress tests in any existing Netlify project. 74 | 75 | ### recommended 76 | 77 | We strongly recommend setting `CYPRESS_CACHE_FOLDER` to place the Cypress binary _inside the node_modules_ folder to [cache it between builds](https://on.cypress.io/caching) 78 | 79 | ```toml 80 | # explicit commands for building the site 81 | # and the folder to publish 82 | [build] 83 | command = "npm run build" 84 | publish = "build" 85 | 86 | [build.environment] 87 | # cache Cypress binary in local "node_modules" folder 88 | # so Netlify caches it 89 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 90 | # set TERM variable for terminal output 91 | TERM = "xterm" 92 | 93 | [[plugins]] 94 | # runs Cypress tests against the deployed URL 95 | package = "netlify-plugin-cypress" 96 | ``` 97 | 98 | See [netlify-plugin-cypress-example](https://github.com/cypress-io/netlify-plugin-cypress-example) repo. 99 | 100 | Typescript users may need to add a `install` before the build command. For a yarn user with a typescript app, the build section of the Netlify configuration might look like this: 101 | 102 | ```toml 103 | [build] 104 | command = "yarn install && yarn build" 105 | publish = "build" 106 | 107 | # ...remaining configuration... 108 | ``` 109 | 110 | ### tutorial 111 | 112 | Read the full tutorial at [Test Sites Deployed To Netlify Using netlify-plugin-cypress](https://glebbahmutov.com/blog/test-netlify/). 113 | 114 | **Note:** if any tests against the deployed URL fail, the Netlify build still considers it a success. Thus if you want to have a test check against the deploy, install [Cypress GitHub App](https://on.cypress.io/github-integration). The app will provide its own failing status check in this case. 115 | 116 | ### options 117 | 118 | You can control the browser, the specs to run, record tests on Cypress Dashboard, etc, see [manifest.yml](./manifest.yml) file. 119 | 120 | ### recording 121 | 122 | To record test results and artifacts on Cypress Dashboard, set `record: true` plugin input and set `CYPRESS_RECORD_KEY` as an environment variable via Netlify Deploy settings. 123 | 124 | ```yaml 125 | [build] 126 | command = "npm run build" 127 | publish = "build" 128 | [build.environment] 129 | # cache Cypress binary in local "node_modules" folder 130 | # so Netlify caches it 131 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 132 | # set TERM variable for terminal output 133 | TERM = "xterm" 134 | 135 | [[plugins]] 136 | # runs Cypress tests against the deployed URL 137 | package = "netlify-plugin-cypress" 138 | [plugins.inputs] 139 | record = true 140 | ``` 141 | 142 | See [cypress-example-kitchensink](https://github.com/cypress-io/cypress-example-kitchensink) and recorded results at Cypress Dashboard [![netlify-plugin-cypress](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ixroqc/master&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/ixroqc/runs) 143 | 144 | **Security note 🔐:** you should keep your `CYPRESS_RECORD_KEY` secret. You can control how Netlify builds external pull requests, see [the doc](https://docs.netlify.com/configure-builds/environment-variables/) - you never want to expose sensitive environment variables to outside builds. 145 | 146 | #### status checks 147 | 148 | If you are recording test results to Cypress Dashboard, you should also install [Cypress GitHub Integration App](https://on.cypress.io/github-integration) to see status checks from individual groups or from individual specs per commit. See [netlify-plugin-prebuild-example PR #8](https://github.com/cypress-io/netlify-plugin-prebuild-example/pull/8) pull request for an example. 149 | 150 | ![Netlify to Cypress Dashboard to GH Integration checks](./images/netlify-to-cy-gh-app-checks.png) 151 | 152 | #### group 153 | 154 | You can change the group name for the recorded run using `group` parameter 155 | 156 | ```toml 157 | [[plugins]] 158 | # runs Cypress tests against the deployed URL 159 | package = "netlify-plugin-cypress" 160 | [plugins.inputs] 161 | record = true 162 | group = "built site" 163 | ``` 164 | 165 | #### tag 166 | 167 | You can give recorded run [tags](https://on.cypress.io/module-api#cypress-run) using a comma-separated string. If the tag is not specified, Netlify context will be used (`production`, `deploy-preview` or `branch-deploy`) 168 | 169 | ```toml 170 | [[plugins]] 171 | # runs Cypress tests against the deployed URL 172 | package = "netlify-plugin-cypress" 173 | [plugins.inputs] 174 | record = true 175 | group = "built site" 176 | tag = "nightly,production" 177 | ``` 178 | 179 | ### spec 180 | 181 | Run only a single spec or specs matching a wildcard 182 | 183 | ```toml 184 | [build] 185 | command = "npm run build" 186 | publish = "build" 187 | [build.environment] 188 | # cache Cypress binary in local "node_modules" folder 189 | # so Netlify caches it 190 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 191 | # set TERM variable for terminal output 192 | TERM = "xterm" 193 | 194 | [[plugins]] 195 | # runs Cypress tests against the deployed URL 196 | package = "netlify-plugin-cypress" 197 | [plugins.inputs] 198 | spec = "cypress/integration/smoke*.js" 199 | ``` 200 | 201 | See [cypress-example-kitchensink](https://github.com/cypress-io/cypress-example-kitchensink) for instance. 202 | 203 | ### browser 204 | 205 | By default all tests run using the Chromium browser. If you want to use Electron: 206 | 207 | ```toml 208 | [build] 209 | command = "npm run build" 210 | publish = "build" 211 | [build.environment] 212 | # cache Cypress binary in local "node_modules" folder 213 | # so Netlify caches it 214 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 215 | # set TERM variable for terminal output 216 | TERM = "xterm" 217 | 218 | [[plugins]] 219 | package = "netlify-plugin-cypress" 220 | [plugins.inputs] 221 | # allowed values: electron, chromium 222 | browser = "electron" 223 | ``` 224 | 225 | ### configFile 226 | 227 | If you would like to use a different Cypress config file instead of `cypress.json`, specify it using the `configFile` option 228 | 229 | ```toml 230 | [build] 231 | command = "npm run build" 232 | publish = "build" 233 | [build.environment] 234 | # cache Cypress binary in local "node_modules" folder 235 | # so Netlify caches it 236 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 237 | # set TERM variable for terminal output 238 | TERM = "xterm" 239 | 240 | [[plugins]] 241 | package = "netlify-plugin-cypress" 242 | [plugins.inputs] 243 | configFile = "cypress.netlify.config.js" 244 | ``` 245 | 246 | ### testing SPA routes 247 | 248 | SPAs need catch-all redirect setup to make non-root paths accessible by tests. You can enable this with `spa` parameter. 249 | 250 | ``` 251 | [[plugins]] 252 | # local Cypress plugin will test our site after it is built 253 | package = "netlify-plugin-cypress" 254 | [plugins.inputs] 255 | # can also use "spa = true" to use "index.html" by default 256 | spa = "index.html" 257 | ``` 258 | 259 | See [lws-spa](https://github.com/lwsjs/spa) for more options and [tests/routing](tests/routing) example. 260 | 261 | ### testing the site before build 262 | 263 | By default this plugin tests static site _after deploy_. But maybe you want to run end-to-end tests against the _local development server_. You can start the local server, wait for it to respond and then run Cypress tests by passing parameters to this plugin. Here is a sample config file 264 | 265 | ```toml 266 | [[plugins]] 267 | package = "netlify-plugin-cypress" 268 | # let's run tests against development server 269 | # before building it (and testing the built site) 270 | [plugins.inputs.preBuild] 271 | enable = true 272 | start = 'npm start' 273 | wait-on = 'http://localhost:3000' 274 | wait-on-timeout = '30' # seconds 275 | ``` 276 | 277 | Parameters you can place into `preBuild` inputs: `start`, `wait-on`, `wait-on-timeout`, `spec`, `record`, `group`, and `tag`. 278 | 279 | See [netlify-plugin-prebuild-example](https://github.com/cypress-io/netlify-plugin-prebuild-example) repo 280 | 281 | ### testing the site after build 282 | 283 | By default this plugin tests static site _after deploy_. But maybe you want to run end-to-end tests locally after building the static site. Cypress includes a local static server for this case but you can specify your own command if needed by using the `start` argument. Here is a sample config file 284 | 285 | ```toml 286 | [[plugins]] 287 | package = "netlify-plugin-cypress" 288 | # let's run tests against the built site 289 | [plugins.inputs.postBuild] 290 | enable = true 291 | ``` 292 | 293 | Parameters you can place into `postBuild` inputs: `spec`, `record`, `group`, `tag`, `start` and `spa`. 294 | 295 | #### The SPA parameter 296 | 297 | If your site requires all unknown URLs to redirect back to the index page, use the `spa` parameter 298 | 299 | ```toml 300 | [[plugins]] 301 | package = "netlify-plugin-cypress" 302 | # let's run tests against the built site 303 | [plugins.inputs.postBuild] 304 | enable = true 305 | # must allow our test server to redirect unknown routes to "/" 306 | # so that client-side routing can correctly route them 307 | # can be set to true or "index.html" (or similar fallback filename in the built folder) 308 | spa = true 309 | start = 'npm start' 310 | ``` 311 | 312 | See [the routing example](./tests/routing/netlify.toml). 313 | 314 | ### using Netlify CLI 315 | 316 | Even better when testing the prebuilt site is to run the [Netlify CLI](https://cli.netlify.com/) to make sure the local API redirects and Netlify functions work in addition to the web site. Add `netlify-cli` as a dev dependency and start it during testing. 317 | 318 | ```shell 319 | $ npm i -D netlify-cli 320 | ``` 321 | 322 | ```toml 323 | [[plugins]] 324 | package = "netlify-plugin-cypress" 325 | # start Netlify server 326 | [plugins.inputs.preBuild] 327 | start = 'npx netlify dev' 328 | wait-on = 'http://localhost:8888' 329 | ``` 330 | 331 | For more, see [tests/test-netlify-dev](./tests/test-netlify-dev) example and read [Testing Netlify Function](https://glebbahmutov.com/blog/test-netlify/#testing-netlify-functions) section. 332 | 333 | ### skipping tests 334 | 335 | If you are testing the site before building it and want to skip testing the deployed URL 336 | 337 | ```toml 338 | [[plugins]] 339 | package = "netlify-plugin-cypress" 340 | # do not test the deployed URL 341 | [plugins.inputs] 342 | enable = false 343 | # test the local site 344 | [plugins.inputs.preBuild] 345 | enable = true 346 | ``` 347 | 348 | ### parallelization 349 | 350 | Running tests in parallel **is not supported** because Netlify plugin system runs on a single machine. Thus you can record the tests on Cypress Dashboard, but not run tests in parallel. If Netlify expands its build offering by allowing multiple build machines, we could take advantage of it and run tests in parallel. 351 | 352 | ### HTML files 353 | 354 | When serving the built folder, we automatically serve `.html` files. For example, if your folder has the following structure: 355 | 356 | ``` 357 | public/ 358 | index.html 359 | pages/ 360 | about.html 361 | ``` 362 | 363 | The `public` folder is served automatically and the following test successfully visits both the root and the `about.html` pages: 364 | 365 | ```js 366 | cy.visit('/') 367 | cy.visit('/pages/about') // visits the about.html 368 | ``` 369 | 370 | ## Example repos 371 | 372 | Name | Description 373 | --- | --- 374 | [netlify-plugin-cypress-example](https://github.com/cypress-io/netlify-plugin-cypress-example) | Runs Cypress tests on Netlify and records their results to Cypress Dashboard 375 | [netlify-plugin-prebuild-example](https://github.com/cypress-io/netlify-plugin-prebuild-example) | Runs tests twice, first using the development version of the site, then after Netlify builds the production bundles, runs the tests again 376 | [cypress-example-kitchensink](https://github.com/cypress-io/cypress-example-kitchensink) | Runs only a subset of all tests before publishing the folder to Netlify 377 | [bahmutov/eleventyone](https://github.com/bahmutov/eleventyone) | Example used in [Test Sites Deployed To Netlify Using netlify-plugin-cypress](https://glebbahmutov.com/blog/test-netlify/) tutorial 378 | [gatsby-starter-portfolio-cara](https://github.com/bahmutov/gatsby-starter-portfolio-cara) | A Gatsby site example 379 | 380 | ## Major upgrades 381 | 382 | ### v1 to v2 383 | 384 | - The default browser has been switched to Chromium. If you want to use the built-in Electron use an explicit option [browser](#browser) 385 | - We have changed the default testing phase. In v1 the tests executed after building the site by default. In v2 the tests run against the deployed URL by default, and you need to enable the testing during `preBuild` or `postBuild` steps. 386 | 387 | ## Debugging 388 | 389 | Set environment variable `DEBUG=netlify-plugin-cypress` to see the debug logs. To see even more information, set `DEBUG=netlify-plugin-cypress,netlify-plugin-cypress:verbose` 390 | 391 | **Warning:** be careful with verbose logging, since it can print all environment variables passed to the plugin, including tokens, API keys, and other secrets. 392 | 393 | ## Common problems 394 | 395 |
396 | Too many progress messages while installing Cypress 397 | If you see A LOT of progress messages during "npm install" step, set an environment 398 | variable during build CI = 1 to remove them. 399 |
400 | 401 |
402 | Cypress binary is installed on every build 403 | By default Cypress binary is installed in the home folder, see caching. 404 | Netlify build does NOT cache this folder, but it DOES cache the local "node_modules" folder. 405 | Tell Cypress to install its binary in the "node_modules" folder by setting build environment 406 | variable CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary". 407 |
408 | 409 |
410 | Several versions of Cypress are installed according to the build logs 411 | From the Netlify UI under Deploys, pick "Trigger Deploy" and select "Clear cache and deploy site". This should cleanly install new "node_modules" and remove old Cypress versions. 412 |
413 | 414 |
415 | Term message warnings in the Cypress output 416 | If you see messages like tput: No value for $TERM and no -T specified during 417 | Cypress run, add an environment variable TERM = xterm. 418 |
419 | 420 |
421 | Electron browser crashes while running tests 422 | Switch to using Chromium browser that seems to be a bit more reliable. Use browser = "chromium" setting. 423 |
424 | 425 |
426 | You want to skip Puppeteer download 427 | If you do not plan on using Chromium to run the tests, if you want to use the built-in Electron browser, you can save time by skipping the Puppeteer download. Set the environment variable PUPPETEER_SKIP_DOWNLOAD = 1 on your CI. 428 |
429 | 430 | ## License 431 | 432 | This project is licensed under the terms of the [MIT license](LICENSE.md). 433 | 434 | ## Contributing 435 | 436 | Read the [contributing guide](CONTRIBUTING.md) 437 | 438 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 439 | [renovate-app]: https://renovateapp.com/ 440 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | projectId: 'ixroqc', 6 | e2e: { 7 | setupNodeEvents(on, config) {}, 8 | supportFile: false, 9 | baseUrl: 'http://localhost:3000', 10 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | it('loads page', () => { 3 | cy.visit('/') 4 | cy.contains('Placeholder').should('be.visible') 5 | }) 6 | 7 | it.skip('skips this test on purpose', () => { 8 | expect(false).to.be.true 9 | }) 10 | 11 | it('has not test yet') 12 | -------------------------------------------------------------------------------- /images/beta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/netlify-plugin-cypress/1a4eb26c984c76c6579713468e5290a7d115c6b3/images/beta.png -------------------------------------------------------------------------------- /images/netlify-to-cy-gh-app-checks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/netlify-plugin-cypress/1a4eb26c984c76c6579713468e5290a7d115c6b3/images/netlify-to-cy-gh-app-checks.png -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | name: netlify-plugin-cypress 2 | inputs: 3 | # by default the Cypress tests run against the deployed URL 4 | # and these settings apply during the "onSuccess" step 5 | - name: enable 6 | description: Run tests against the preview or production deploy 7 | default: true 8 | 9 | # Cypress comes with built-in Electron browser 10 | # and this NPM package installs Chromium browser 11 | - name: browser 12 | description: Allowed values are chromium, electron 13 | default: chromium 14 | 15 | - name: configFile 16 | description: Path to the Cypress config file to use 17 | 18 | - name: record 19 | description: Record test results to Cypress Dashboard 20 | default: false 21 | 22 | - name: spec 23 | description: | 24 | Run just the given spec or spec pattern, 25 | equivalent to "cypress run --spec ..." 26 | 27 | - name: group 28 | description: | 29 | If recording to Cypress Dashboard, 30 | pass the group name with "cypress run --record --group ..." 31 | 32 | - name: tag 33 | description: | 34 | If recording to Cypress Dashboard, 35 | pass the tag with "cypress run --record --tag ..." 36 | 37 | # tells the plugin how to start the server using custom command 38 | # and waiting for an url, record to the dashboard, tag, etc 39 | # see README "testing the site before build" 40 | - name: preBuild 41 | description: Run tests before building the site 42 | 43 | # tells the plugin to start a static server during postBuild 44 | # and test just the built static site. 45 | # see README "testing the site after build" 46 | # NOTE: does not execute Netlify API redirects or functions 47 | - name: postBuild 48 | description: Run tests against the built static site 49 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # WARNING ⚠️: this is a Netlify file with local Cypress plugin 2 | # so it might look quite different from the user's Netlify configuration 3 | [build] 4 | command = "echo 'Netlify build command ...'" 5 | publish = "public" 6 | [build.environment] 7 | TERM = "xterm" 8 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 9 | 10 | [[plugins]] 11 | # local Cypress plugin will test our site after it is built 12 | package = "." 13 | 14 | # run tests after deploying to Netlify 15 | [plugins.inputs] 16 | record = true 17 | group = 'deployed' 18 | 19 | # run tests after building the site 20 | [plugins.inputs.postBuild] 21 | enable = true 22 | record = true 23 | group = 'postBuild' 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netlify-plugin-cypress", 3 | "private": false, 4 | "version": "0.0.0-development", 5 | "description": "Cypress Netlify build plugin", 6 | "main": "src", 7 | "scripts": { 8 | "test": "cypress run", 9 | "test:unit": "jest", 10 | "semantic-release": "semantic-release", 11 | "start": "serve public" 12 | }, 13 | "keywords": [ 14 | "netlify", 15 | "netlify-plugin" 16 | ], 17 | "author": "Cypress-io", 18 | "files": [ 19 | "manifest.yml", 20 | "src", 21 | "!src/**/*.test.js" 22 | ], 23 | "license": "ISC", 24 | "dependencies": { 25 | "common-tags": "1.8.0", 26 | "debug": "4.1.1", 27 | "got": "11.8.6", 28 | "local-web-server": "^4.2.1", 29 | "puppeteer": "18.1.0", 30 | "ramda": "0.28.0" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/cypress-io/netlify-plugin-cypress.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/cypress-io/netlify-plugin-cypress/issues" 38 | }, 39 | "publishConfig": { 40 | "access": "public" 41 | }, 42 | "engines": { 43 | "node": ">=18" 44 | }, 45 | "devDependencies": { 46 | "jest": "^24.9.0", 47 | "cypress": "14.4.0", 48 | "netlify-cli": "18.1.0", 49 | "prettier": "2.2.1", 50 | "react": "16.13.1", 51 | "react-dom": "16.13.1", 52 | "react-router-dom": "5.2.0", 53 | "react-scripts": "5.0.1", 54 | "semantic-release": "^20.0.2", 55 | "serve": "14.2.0" 56 | }, 57 | "release": { 58 | "branches": [ 59 | { 60 | "name": "master" 61 | }, 62 | { 63 | "name": "prepare-v2", 64 | "prerelease": true 65 | } 66 | ] 67 | }, 68 | "jest": { 69 | "rootDir": "./src" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | Placeholder 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "major": { 7 | "automerge": false 8 | }, 9 | "masterIssue": true, 10 | "labels": [ 11 | "type: dependencies", 12 | "renovate" 13 | ], 14 | "packageRules": [ 15 | { 16 | "packagePatterns": ["*"], 17 | "excludePackagePatterns": [ 18 | "ecstatic", 19 | "cypress", 20 | "netlify-cli", 21 | "got" 22 | ], 23 | "enabled": false 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PLUGIN_NAME: 'netlify-plugin-cypress', 3 | DEFAULT_BROWSER: 'chromium', 4 | } 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const onPreBuild = require('./onPreBuild') 3 | const onPostBuild = require('./onPostBuild') 4 | const onSuccess = require('./onSuccess') 5 | 6 | module.exports = { 7 | onPreBuild, 8 | onPostBuild, 9 | onSuccess, 10 | } 11 | -------------------------------------------------------------------------------- /src/onPostBuild.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const debug = require('debug')('netlify-plugin-cypress') 3 | const debugVerbose = require('debug')('netlify-plugin-cypress:verbose') 4 | const { 5 | startServerMaybe, 6 | serveFolder, 7 | runCypressTests, 8 | processCypressResults, 9 | hasRecordKey, 10 | waitOnMaybe, 11 | } = require('./utils') 12 | const { DEFAULT_BROWSER } = require('./constants') 13 | 14 | async function postBuild({ 15 | utils, 16 | fullPublishFolder, 17 | record, 18 | spec, 19 | start, 20 | waitOn, 21 | waitOnTimeout, 22 | group, 23 | tag, 24 | spa, 25 | browser, 26 | configFile, 27 | errorCallback, 28 | summaryCallback, 29 | }) { 30 | const port = 8080 31 | let server 32 | let closeServer 33 | 34 | if (start) { 35 | closeServer = startServerMaybe(utils.run, { start }) 36 | await waitOnMaybe(utils.build, { 37 | 'wait-on': waitOn, 38 | 'wait-on-timeout': waitOnTimeout, 39 | }) 40 | } else { 41 | try { 42 | server = serveFolder(fullPublishFolder, port, spa) 43 | debug('local server listening on port %d', port) 44 | } catch (err) { 45 | return errorCallback(`Could not serve folder ${fullPublishFolder}`, { 46 | error: err, 47 | }) 48 | } 49 | } 50 | 51 | const baseUrl = waitOn || `http://localhost:${port}` 52 | 53 | const results = await runCypressTests( 54 | baseUrl, 55 | record, 56 | spec, 57 | group, 58 | tag, 59 | browser, 60 | configFile, 61 | ) 62 | 63 | if (closeServer) { 64 | debug('closing server') 65 | await closeServer() 66 | } 67 | 68 | if (server) { 69 | await new Promise((resolve, reject) => { 70 | server.close((err) => { 71 | if (err) { 72 | return reject(err) 73 | } 74 | debug('closed local server on port %d', port) 75 | resolve() 76 | }) 77 | }) 78 | } 79 | // =============================================== 80 | 81 | processCypressResults(results, errorCallback, summaryCallback) 82 | } 83 | 84 | module.exports = async ({ inputs, constants, utils }) => { 85 | debugVerbose('===postBuild===') 86 | 87 | const postBuildInputs = inputs.postBuild || {} 88 | debug('cypress plugin postBuild inputs %o', postBuildInputs) 89 | 90 | const enablePostBuildTests = Boolean(postBuildInputs.enable) 91 | if (!enablePostBuildTests) { 92 | debug('Skipping postBuild tests') 93 | return 94 | } 95 | 96 | const fullPublishFolder = constants.PUBLISH_DIR 97 | debug('folder to publish is "%s"', fullPublishFolder) 98 | 99 | const browser = postBuildInputs.browser || DEFAULT_BROWSER 100 | 101 | // only if the user wants to record the tests and has set the record key 102 | // then we should attempt recording 103 | const record = Boolean(postBuildInputs.record) && hasRecordKey() 104 | 105 | let group 106 | let tag 107 | if (record) { 108 | group = postBuildInputs.group || 'postBuild' 109 | 110 | if (postBuildInputs.tag) { 111 | tag = postBuildInputs.tag 112 | } else { 113 | tag = process.env.CONTEXT 114 | } 115 | } 116 | 117 | const spa = postBuildInputs.spa 118 | const configFile = postBuildInputs.configFile 119 | 120 | const errorCallback = utils.build.failBuild.bind(utils.build) 121 | const summaryCallback = utils.status.show.bind(utils.status) 122 | 123 | await postBuild({ 124 | utils, 125 | fullPublishFolder, 126 | record, 127 | spec: postBuildInputs.spec, 128 | start: postBuildInputs.start, 129 | waitOn: postBuildInputs['wait-on'], 130 | waitOnTimeout: postBuildInputs['wait-on-timeout'], 131 | group, 132 | tag, 133 | spa, 134 | browser, 135 | configFile, 136 | errorCallback, 137 | summaryCallback, 138 | }) 139 | } 140 | -------------------------------------------------------------------------------- /src/onPostBuild.test.js: -------------------------------------------------------------------------------- 1 | const onPostBuild = require('./onPostBuild.js') 2 | const { 3 | startServerMaybe, 4 | serveFolder, 5 | runCypressTests, 6 | processCypressResults, 7 | waitOnMaybe, 8 | } = require('./utils') 9 | 10 | jest.mock('./utils') 11 | 12 | const setup = ({ postBuildInputs = {} } = {}) => { 13 | const inputs = { 14 | postBuild: { 15 | spa: false, 16 | record: false, 17 | ...postBuildInputs, 18 | }, 19 | } 20 | const constants = { 21 | PUBLISH_DIR: 'PUBLISH_DIR', 22 | } 23 | const utils = { 24 | run: jest.fn(), 25 | build: { 26 | failBuild: jest.fn(), 27 | }, 28 | status: { 29 | show: jest.fn(), 30 | }, 31 | } 32 | 33 | return { 34 | inputs, 35 | constants, 36 | utils, 37 | testFunction: () => onPostBuild({ inputs, constants, utils }), 38 | } 39 | } 40 | 41 | describe('onPostBuild', () => { 42 | beforeEach(() => { 43 | startServerMaybe.mockReset() 44 | serveFolder.mockReset() 45 | runCypressTests.mockReset() 46 | processCypressResults.mockReset() 47 | }) 48 | 49 | it('skips when post build tests is disabled', async () => { 50 | const { testFunction } = setup({ 51 | postBuildInputs: { enable: false }, 52 | }) 53 | 54 | await expect(testFunction()).resolves.toBe(undefined) 55 | expect(startServerMaybe).not.toHaveBeenCalled() 56 | expect(serveFolder).not.toHaveBeenCalled() 57 | }) 58 | 59 | describe('start option', () => { 60 | it('runs the specified command', async () => { 61 | const startCommand = 'a start command' 62 | const { testFunction, utils } = setup({ 63 | postBuildInputs: { 64 | enable: true, 65 | start: startCommand, 66 | }, 67 | }) 68 | 69 | await expect(testFunction()).resolves.toBe(undefined) 70 | expect(startServerMaybe).toHaveBeenCalledWith(utils.run, { 71 | start: startCommand, 72 | }) 73 | }) 74 | 75 | it('waits for the specified url before continuing', async () => { 76 | const startCommand = 'a start command' 77 | const { testFunction, utils, inputs } = setup({ 78 | postBuildInputs: { 79 | enable: true, 80 | start: startCommand, 81 | 'wait-on': 'URL', 82 | 'wait-on-timeout': 10, 83 | }, 84 | }) 85 | 86 | await expect(testFunction()).resolves.toBe(undefined) 87 | expect(waitOnMaybe).toHaveBeenCalledWith(utils.build, { 88 | 'wait-on': inputs.postBuild['wait-on'], 89 | 'wait-on-timeout': inputs.postBuild['wait-on-timeout'], 90 | }) 91 | }) 92 | 93 | it('kills the process when tests are complete', async () => { 94 | const { testFunction } = setup({ 95 | postBuildInputs: { 96 | enable: true, 97 | start: 'a start command', 98 | }, 99 | }) 100 | 101 | const stopMock = jest.fn() 102 | startServerMaybe.mockReturnValue(stopMock) 103 | 104 | await expect(testFunction()).resolves.toBe(undefined) 105 | expect(stopMock).toHaveBeenCalled() 106 | }) 107 | 108 | it('does not try to serve the publish folder', async () => { 109 | const { testFunction } = setup({ 110 | postBuildInputs: { 111 | enable: true, 112 | start: 'a start command', 113 | }, 114 | }) 115 | 116 | await expect(testFunction()).resolves.toBe(undefined) 117 | expect(serveFolder).not.toHaveBeenCalled() 118 | }) 119 | 120 | it('runs the cypress tests', async () => { 121 | const { testFunction, inputs } = setup({ 122 | postBuildInputs: { 123 | enable: true, 124 | start: 'a start command', 125 | }, 126 | }) 127 | 128 | await expect(testFunction()).resolves.toBe(undefined) 129 | // TODO: Improve this assertion 130 | expect(runCypressTests).toHaveBeenCalledWith( 131 | 'http://localhost:8080', 132 | inputs.postBuild.record, 133 | inputs.postBuild.spec, 134 | undefined, 135 | undefined, 136 | 'chromium', 137 | undefined, 138 | ) 139 | }) 140 | 141 | it('processes the cypress test results', async () => { 142 | const { testFunction, inputs, utils } = setup({ 143 | postBuildInputs: { 144 | enable: true, 145 | start: 'a start command', 146 | }, 147 | }) 148 | 149 | const testResults = 'RESULTS' 150 | runCypressTests.mockReturnValue(testResults) 151 | 152 | await expect(testFunction()).resolves.toBe(undefined) 153 | // TODO: Improve assertion 154 | expect(processCypressResults).toHaveBeenCalledWith( 155 | testResults, 156 | expect.any(Function), 157 | expect.any(Function), 158 | ) 159 | }) 160 | }) 161 | 162 | describe('serve folder', () => { 163 | it('serves the publish folder when a start command is not specified', async () => { 164 | const { testFunction, constants } = setup({ 165 | postBuildInputs: { 166 | enable: true, 167 | }, 168 | }) 169 | 170 | await expect(testFunction()).resolves.toBe(undefined) 171 | expect(serveFolder).toHaveBeenCalledWith( 172 | constants.PUBLISH_DIR, 173 | 8080, 174 | false, 175 | ) 176 | }) 177 | 178 | it('kills the process when tests are complete', async () => { 179 | const { testFunction } = setup({ 180 | postBuildInputs: { 181 | enable: true, 182 | }, 183 | }) 184 | 185 | const serverCloseMock = jest.fn().mockImplementation((cb) => cb()) 186 | serveFolder.mockReturnValue({ close: serverCloseMock }) 187 | 188 | await expect(testFunction()).resolves.toBe(undefined) 189 | expect(serverCloseMock).toHaveBeenCalled() 190 | }) 191 | 192 | it('calls the error callback when it fails to serve the folder', async () => { 193 | const { testFunction, utils, constants } = setup({ 194 | postBuildInputs: { 195 | enable: true, 196 | }, 197 | }) 198 | 199 | const error = new Error('Broken') 200 | serveFolder.mockImplementation(() => { 201 | throw error 202 | }) 203 | 204 | await expect(testFunction()).resolves.toBe(undefined) 205 | expect( 206 | utils.build.failBuild, 207 | ).toHaveBeenCalledWith( 208 | `Could not serve folder ${constants.PUBLISH_DIR}`, 209 | { error }, 210 | ) 211 | }) 212 | 213 | it('rejects when it fails to close the server', async () => { 214 | const { testFunction, utils, constants } = setup({ 215 | postBuildInputs: { 216 | enable: true, 217 | }, 218 | }) 219 | 220 | const error = new Error('Broken') 221 | serveFolder.mockReturnValue({ 222 | close: () => { 223 | throw error 224 | }, 225 | }) 226 | 227 | await expect(testFunction()).rejects.toBe(error) 228 | }) 229 | 230 | it('runs the cypress tests', async () => { 231 | const { testFunction, inputs } = setup({ 232 | postBuildInputs: { 233 | enable: true, 234 | }, 235 | }) 236 | 237 | await expect(testFunction()).resolves.toBe(undefined) 238 | // TODO: Improve this assertion 239 | expect(runCypressTests).toHaveBeenCalledWith( 240 | 'http://localhost:8080', 241 | inputs.postBuild.record, 242 | inputs.postBuild.spec, 243 | undefined, 244 | undefined, 245 | 'chromium', 246 | undefined, 247 | ) 248 | }) 249 | 250 | it('processes the cypress test results', async () => { 251 | const { testFunction, inputs, utils } = setup({ 252 | postBuildInputs: { 253 | enable: true, 254 | start: 'a start command', 255 | }, 256 | }) 257 | 258 | const testResults = 'RESULTS' 259 | runCypressTests.mockReturnValue(testResults) 260 | 261 | await expect(testFunction()).resolves.toBe(undefined) 262 | // TODO: Improve assertion 263 | expect(processCypressResults).toHaveBeenCalledWith( 264 | testResults, 265 | expect.any(Function), 266 | expect.any(Function), 267 | ) 268 | }) 269 | }) 270 | }) 271 | -------------------------------------------------------------------------------- /src/onPreBuild.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const debug = require('debug')('netlify-plugin-cypress') 3 | const { 4 | ping, 5 | startServerMaybe, 6 | runCypressTests, 7 | processCypressResults, 8 | hasRecordKey, 9 | waitOnMaybe, 10 | } = require('./utils') 11 | const { DEFAULT_BROWSER } = require('./constants') 12 | 13 | async function install(arg) { 14 | debug('installing Cypress binary just in case') 15 | const runOptions = debug.enabled ? {} : { stdio: 'ignore' } 16 | try { 17 | await arg.utils.run('cypress', ['install'], runOptions) 18 | } catch (error) { 19 | debug('error installing Cypress: %s', error.message) 20 | const buildUtils = arg.utils.build 21 | console.error('') 22 | console.error('Failed to install Cypress') 23 | console.error('Did you forget to add Cypress as a dev dependency?') 24 | console.error(' npm i -D cypress') 25 | console.error('or') 26 | console.error(' yarn add -D cypress') 27 | console.error('') 28 | console.error( 29 | 'See https://github.com/cypress-io/netlify-plugin-cypress#readme', 30 | ) 31 | console.error('') 32 | buildUtils.failBuild( 33 | 'Failed to install Cypress. Did you forget to add Cypress as a dev dependency?', 34 | { error }, 35 | ) 36 | } 37 | } 38 | 39 | async function cypressVerify(arg) { 40 | debug('verifying Cypress can run') 41 | try { 42 | await arg.utils.run('cypress', ['verify']) 43 | } catch (error) { 44 | debug('error verifying Cypress: %s', error.message) 45 | const buildUtils = arg.utils.build 46 | console.error('') 47 | console.error('Failed to verify Cypress') 48 | console.error('') 49 | buildUtils.failBuild('Failed to verify Cypress', { error }) 50 | } 51 | } 52 | 53 | async function cypressInfo(arg) { 54 | debug('Cypress info') 55 | try { 56 | await arg.utils.run('cypress', ['info']) 57 | } catch (error) { 58 | debug('error in Cypress info command: %s', error.message) 59 | const buildUtils = arg.utils.build 60 | console.error('') 61 | console.error('Failed to run Cypress info') 62 | console.error('') 63 | buildUtils.failBuild('Failed Cypress info', { error }) 64 | } 65 | } 66 | 67 | module.exports = async (arg) => { 68 | // we need to install everything to be ready 69 | await install(arg) 70 | await cypressVerify(arg) 71 | await cypressInfo(arg) 72 | 73 | const { inputs, utils } = arg 74 | 75 | const preBuildInputs = inputs.preBuild || {} 76 | debug('preBuild inputs %o', preBuildInputs) 77 | 78 | const enablePreBuildTests = Boolean(preBuildInputs.enable) 79 | if (!enablePreBuildTests) { 80 | debug('Skipping preBuild tests') 81 | return 82 | } 83 | 84 | const browser = preBuildInputs.browser || DEFAULT_BROWSER 85 | 86 | const closeServer = startServerMaybe(utils.run, preBuildInputs) 87 | await waitOnMaybe(utils.build, preBuildInputs) 88 | 89 | const baseUrl = preBuildInputs['wait-on'] 90 | const record = hasRecordKey() && Boolean(preBuildInputs.record) 91 | const spec = preBuildInputs.spec 92 | let group 93 | let tag 94 | if (record) { 95 | group = preBuildInputs.group || 'preBuild' 96 | 97 | if (preBuildInputs.tag) { 98 | tag = preBuildInputs.tag 99 | } else { 100 | tag = process.env.CONTEXT 101 | } 102 | } 103 | 104 | const configFile = preBuildInputs.configFile 105 | 106 | const results = await runCypressTests( 107 | baseUrl, 108 | record, 109 | spec, 110 | group, 111 | tag, 112 | browser, 113 | configFile, 114 | ) 115 | 116 | if (closeServer) { 117 | debug('closing server') 118 | closeServer() 119 | } 120 | 121 | const errorCallback = utils.build.failBuild.bind(utils.build) 122 | const summaryCallback = utils.status.show.bind(utils.status) 123 | 124 | processCypressResults(results, errorCallback, summaryCallback) 125 | } 126 | -------------------------------------------------------------------------------- /src/onSuccess.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const R = require('ramda') 3 | const debug = require('debug')('netlify-plugin-cypress') 4 | const debugVerbose = require('debug')('netlify-plugin-cypress:verbose') 5 | const { 6 | runCypressTests, 7 | processCypressResults, 8 | hasRecordKey, 9 | } = require('./utils') 10 | const { DEFAULT_BROWSER } = require('./constants') 11 | 12 | module.exports = async ({ utils, inputs, constants }) => { 13 | debugVerbose('onSuccess arg %o', { utils, inputs, constants }) 14 | 15 | // extract test run parameters 16 | const onSuccessInputs = R.omit(['preBuild', 'postBuild'], inputs || {}) 17 | debug('onSuccess inputs %o', onSuccessInputs) 18 | 19 | const isLocal = constants.IS_LOCAL 20 | const siteName = process.env.SITE_NAME 21 | const deployPrimeUrl = process.env.DEPLOY_PRIME_URL 22 | debug('onSuccess against %o', { 23 | siteName, 24 | deployPrimeUrl, 25 | isLocal, 26 | }) 27 | 28 | const enableOnSuccessTests = Boolean(onSuccessInputs.enable) 29 | if (!enableOnSuccessTests) { 30 | debug('Skipping onSuccess tests') 31 | return 32 | } 33 | 34 | debug('onSuccessInputs %s %o', typeof onSuccessInputs, onSuccessInputs) 35 | 36 | const errorCallback = utils.build.failPlugin.bind(utils.build) 37 | const summaryCallback = utils.status.show.bind(utils.status) 38 | 39 | if (!deployPrimeUrl) { 40 | return errorCallback('Missing DEPLOY_PRIME_URL') 41 | } 42 | 43 | const browser = onSuccessInputs.browser || DEFAULT_BROWSER 44 | 45 | // only if the user wants to record the tests and has set the record key 46 | // then we should attempt recording 47 | const hasKey = hasRecordKey() 48 | const record = hasKey && Boolean(onSuccessInputs.record) 49 | 50 | const spec = onSuccessInputs.spec 51 | let group 52 | let tag 53 | if (record) { 54 | group = onSuccessInputs.group || 'onSuccess' 55 | 56 | if (onSuccessInputs.tag) { 57 | tag = onSuccessInputs.tag 58 | } else { 59 | tag = process.env.CONTEXT 60 | } 61 | } 62 | debug('deployed url test parameters %o', { 63 | hasRecordKey: hasKey, 64 | record, 65 | spec, 66 | group, 67 | tag, 68 | }) 69 | 70 | const configFile = onSuccessInputs.configFile 71 | 72 | console.log('testing deployed url %s', deployPrimeUrl) 73 | const results = await runCypressTests( 74 | deployPrimeUrl, 75 | record, 76 | spec, 77 | group, 78 | tag, 79 | browser, 80 | configFile, 81 | ) 82 | processCypressResults(results, errorCallback, summaryCallback) 83 | } 84 | -------------------------------------------------------------------------------- /src/ping-cli.js: -------------------------------------------------------------------------------- 1 | // a little CLI utility for testing pinging websites 2 | // node ./src/ping-cli 3 | 4 | const { ping } = require('./utils') 5 | const timeoutSeconds = 60 6 | const url = process.argv[2] 7 | console.log('pinging url %s for %d seconds', url, timeoutSeconds) 8 | if (!url) { 9 | console.error('Missing url to ping') 10 | process.exit(1) 11 | } 12 | ping(url, timeoutSeconds * 1000).then( 13 | () => { 14 | console.log('all good') 15 | }, 16 | (err) => { 17 | console.error('a problem', err) 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /src/ping.js: -------------------------------------------------------------------------------- 1 | const got = require('got') 2 | const debug = require('debug')('netlify-plugin-cypress') 3 | const debugVerbose = require('debug')('netlify-plugin-cypress:verbose') 4 | 5 | /** 6 | * A small utility for checking when an URL responds, kind of 7 | * a poor man's https://www.npmjs.com/package/wait-on. This version 8 | * is implemented using https://github.com/sindresorhus/got 9 | */ 10 | const ping = (url, timeout) => { 11 | if (!timeout) { 12 | throw new Error('Expected timeout in ms') 13 | } 14 | 15 | debug('pinging "%s" for %d ms max', url, timeout) 16 | 17 | // make copy of the error codes that "got" retries on 18 | const errorCodes = [...got.defaults.options.retry.errorCodes] 19 | errorCodes.push('ESOCKETTIMEDOUT') 20 | 21 | // we expect the server to respond within a time limit 22 | // and if it does not - retry up to total "timeout" duration 23 | const individualPingTimeout = Math.min(timeout, 30000) 24 | const limit = Math.ceil(timeout / individualPingTimeout) 25 | 26 | debug(`total ping timeout ${timeout}`) 27 | debug(`individual ping timeout ${individualPingTimeout}ms`) 28 | debug(`retries limit ${limit}`) 29 | 30 | const start = +new Date() 31 | return got(url, { 32 | headers: { 33 | Accept: 'text/html, application/json, text/plain, */*', 34 | }, 35 | timeout: individualPingTimeout, 36 | errorCodes, 37 | retry: { 38 | limit, 39 | calculateDelay({ error, attemptCount }) { 40 | if (error) { 41 | debugVerbose(`got error ${JSON.stringify(error)}`) 42 | } 43 | const now = +new Date() 44 | const elapsed = now - start 45 | debugVerbose( 46 | `${elapsed}ms ${error.method} ${error.host} ${error.code} attempt ${attemptCount}`, 47 | ) 48 | if (elapsed > timeout) { 49 | console.error( 50 | '%s timed out on retry %d of %d', 51 | url, 52 | attemptCount, 53 | limit, 54 | ) 55 | return 0 56 | } 57 | 58 | // if the error code is ECONNREFUSED use shorter timeout 59 | // because the server is probably starting 60 | if (error.code === 'ECONNREFUSED') { 61 | return 1000 62 | } 63 | 64 | // default "long" timeout 65 | return individualPingTimeout 66 | }, 67 | }, 68 | }).then(() => { 69 | const now = +new Date() 70 | const elapsed = now - start 71 | debug(`pinging ${url} has finished ok after ${elapsed}ms`) 72 | }) 73 | } 74 | 75 | module.exports = { ping } 76 | -------------------------------------------------------------------------------- /src/serve-cli.js: -------------------------------------------------------------------------------- 1 | // a little utility to debug our static file server 2 | const { serveFolder } = require('./utils') 3 | 4 | serveFolder('./public', '8080', false) 5 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const puppeteer = require('puppeteer') 3 | const debug = require('debug')('netlify-plugin-cypress') 4 | const LocalWebServer = require('local-web-server') 5 | const fs = require('fs') 6 | const { stripIndent } = require('common-tags') 7 | const { ping } = require('./ping') 8 | const { PLUGIN_NAME } = require('./constants') 9 | 10 | const getBrowserPath = async () => { 11 | const browserFetcher = puppeteer.createBrowserFetcher() 12 | const revisions = await browserFetcher.localRevisions() 13 | debug('local Chromium revisions %o', revisions) 14 | if (revisions.length <= 0) { 15 | throw new Error('Could not find local browser') 16 | } 17 | const info = await browserFetcher.revisionInfo(revisions[0]) 18 | debug('found Chromium %o', info) 19 | return info.executablePath 20 | } 21 | 22 | /** 23 | * Servers local folder 24 | * @see https://github.com/lwsjs/local-web-server 25 | * @param {string} directory 26 | * @param {number} port 27 | * @param {boolean|'index.html'} spa 28 | */ 29 | function serveFolder(directory, port, spa) { 30 | if (typeof spa === 'boolean') { 31 | if (spa) { 32 | // spa parameter should be the name of the 33 | // fallback file in the directory to serve 34 | // typically it is "index.html" 35 | spa = 'index.html' 36 | } else { 37 | // do not use fallback mechanism for routing 38 | spa = undefined 39 | } 40 | } 41 | debug( 42 | 'serving local folder %o from working directory %s', 43 | { 44 | directory, 45 | port, 46 | spa, 47 | }, 48 | process.cwd(), 49 | ) 50 | 51 | if (!fs.existsSync(directory)) { 52 | throw new Error(`Cannot find folder "${directory}" to serve`) 53 | } 54 | 55 | return LocalWebServer.create({ 56 | // @ts-ignore 57 | directory, 58 | port, 59 | spa, 60 | // to debug use 61 | // DEBUG=koa-send 62 | staticExtensions: ['html'], 63 | }).server 64 | } 65 | 66 | function startServerMaybe(run, options = {}) { 67 | const startCommand = options.start 68 | if (!startCommand) { 69 | debug('No start command found') 70 | return 71 | } 72 | 73 | const serverProcess = run.command(startCommand, { 74 | detached: true, 75 | shell: true, 76 | }) 77 | 78 | debug('detached the process and returning stop function') 79 | return async () => { 80 | console.log('stopping server process opened with:', startCommand) 81 | await serverProcess.kill() 82 | } 83 | } 84 | 85 | async function waitOnMaybe(buildUtils, options = {}) { 86 | const waitOnUrl = options['wait-on'] 87 | if (!waitOnUrl) { 88 | debug('no wait-on defined') 89 | return 90 | } 91 | 92 | const waitOnTimeout = options['wait-on-timeout'] || '60' 93 | 94 | console.log( 95 | 'waiting on "%s" with timeout of %s seconds', 96 | waitOnUrl, 97 | waitOnTimeout, 98 | ) 99 | 100 | const waitTimeoutMs = parseFloat(waitOnTimeout) * 1000 101 | 102 | try { 103 | await ping(waitOnUrl, waitTimeoutMs) 104 | debug('url %s responds', waitOnUrl) 105 | } catch (err) { 106 | debug('pinging %s for %d ms failed', waitOnUrl, waitTimeoutMs) 107 | debug(err) 108 | return buildUtils.failBuild( 109 | `Pinging ${waitOnUrl} for ${waitTimeoutMs} failed`, 110 | { error: err }, 111 | ) 112 | } 113 | } 114 | 115 | const isValidBrowser = (name) => name === 'electron' || name === 'chromium' 116 | 117 | async function runCypressTests( 118 | baseUrl, 119 | record, 120 | spec, 121 | group, 122 | tag, 123 | browser, 124 | configFile, 125 | ) { 126 | if (!isValidBrowser(browser)) { 127 | throw new Error(`Invalid browser name "${browser}"`) 128 | } 129 | 130 | // we will use Cypress via its NPM module API 131 | // https://on.cypress.io/module-api 132 | const cypress = require('cypress') 133 | 134 | let ciBuildId 135 | if (record) { 136 | // https://docs.netlify.com/configure-builds/environment-variables/#build-metadata 137 | // unique build id we can use to link preBuild and postBuild recordings 138 | ciBuildId = process.env.BUILD_ID 139 | } 140 | 141 | const browserPath = 142 | browser === 'electron' ? 'electron' : await getBrowserPath() 143 | 144 | debug('run cypress params %o', { 145 | baseUrl, 146 | record, 147 | spec, 148 | group, 149 | tag, 150 | ciBuildId, 151 | browser: browserPath, 152 | }) 153 | 154 | return await cypress.run({ 155 | config: { 156 | baseUrl, 157 | }, 158 | spec, 159 | record, 160 | group, 161 | tag, 162 | ciBuildId, 163 | browser: browserPath, 164 | headless: true, 165 | configFile, 166 | }) 167 | } 168 | 169 | /** 170 | * Reports the number of successful and failed tests. 171 | * If there are failed tests, uses the `errorCallback` to 172 | * fail the build step. 173 | * @param {*} results 174 | * @param {function} errorCallback 175 | * @param {function} summaryCallback 176 | */ 177 | const processCypressResults = (results, errorCallback, summaryCallback) => { 178 | if (typeof errorCallback !== 'function') { 179 | debug('Typeof of error callback %s', errorCallback) 180 | throw new Error( 181 | `Expected error callback to be a function, it was ${typeof errorCallback}`, 182 | ) 183 | } 184 | if (typeof summaryCallback !== 'function') { 185 | debug('Typeof of summary callback %s', summaryCallback) 186 | throw new Error( 187 | `Expected summary callback to be a function, it was ${typeof summaryCallback}`, 188 | ) 189 | } 190 | 191 | if (results.failures) { 192 | // Cypress failed without even running the tests 193 | console.error('Problem running Cypress') 194 | console.error(results.message) 195 | 196 | return errorCallback('Problem running Cypress', { 197 | error: new Error(results.message), 198 | }) 199 | } 200 | 201 | debug('Cypress run results') 202 | Object.keys(results).forEach((key) => { 203 | if (key.startsWith('total')) { 204 | debug('%s:', key, results[key]) 205 | } 206 | }) 207 | 208 | // Note: text looks nice with double space after the emoji 209 | const summary = [ 210 | 'tests:', 211 | `✅ ${results.totalPassed}`, 212 | `🔥 ${results.totalFailed}`, 213 | `⭕️ ${results.totalPending}`, 214 | `🚫 ${results.totalSkipped}`, 215 | ] 216 | 217 | let text = stripIndent` 218 | ✅ Passed tests: ${results.totalPassed} 219 | 🔥 Failed tests: ${results.totalFailed} 220 | ⭕️ Pending tests: ${results.totalPending} 221 | 🚫 Skipped tests: ${results.totalSkipped} 222 | ` 223 | if (results.runUrl) { 224 | summary.push(`🔗 [dashboard run](${results.runUrl})`) 225 | text += `\n🔗 Cypress Dashboard url: [${results.runUrl}](${results.runUrl})` 226 | } 227 | summaryCallback({ 228 | title: PLUGIN_NAME, 229 | summary: summary.join(' '), 230 | text, 231 | }) 232 | 233 | // results.totalFailed gives total number of failed tests 234 | if (results.totalFailed) { 235 | return errorCallback('Failed Cypress tests', { 236 | error: new Error(`${results.totalFailed} test(s) failed`), 237 | }) 238 | } 239 | } 240 | 241 | const hasRecordKey = () => typeof process.env.CYPRESS_RECORD_KEY === 'string' 242 | 243 | module.exports = { 244 | ping, 245 | getBrowserPath, 246 | serveFolder, 247 | startServerMaybe, 248 | runCypressTests, 249 | processCypressResults, 250 | hasRecordKey, 251 | waitOnMaybe, 252 | } 253 | -------------------------------------------------------------------------------- /tests/basic/README.md: -------------------------------------------------------------------------------- 1 | # basic test 2 | 3 | These tests run: 4 | - [ ] before the build 5 | - [x] after the build 6 | - [ ] on deploy success 7 | -------------------------------------------------------------------------------- /tests/basic/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | e2e: { 6 | setupNodeEvents(on, config) {}, 7 | supportFile: false, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /tests/basic/cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | describe('basic', () => { 3 | it('runs unit test', () => { 4 | expect(2 + 3).to.equal(5) 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /tests/basic/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "echo 'Netlify build command ...'" 3 | publish = "public" 4 | base = "tests/basic" 5 | 6 | [[plugins]] 7 | # local Cypress plugin will test our site after it is built 8 | # in production, please use: package = "netlify-plugin-cypress" 9 | package = "../../" 10 | 11 | # do not run tests after deploy (testing) 12 | [plugins.inputs] 13 | enable = false 14 | 15 | # run tests postBuild 16 | [plugins.inputs.postBuild] 17 | enable = true 18 | -------------------------------------------------------------------------------- /tests/basic/public/index.html: -------------------------------------------------------------------------------- 1 |

Basic

2 | -------------------------------------------------------------------------------- /tests/config-file/README.md: -------------------------------------------------------------------------------- 1 | # using specific config file 2 | 3 | These tests specify non-default Cypress configuration file to use 4 | 5 | These tests run: 6 | - [ ] before the build 7 | - [x] after the build 8 | - [ ] on deploy success 9 | 10 | ## Local testing 11 | 12 | ``` 13 | DEBUG=netlify-plugin-cypress npm run netlify:build 14 | ``` 15 | -------------------------------------------------------------------------------- /tests/config-file/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | e2e: { 6 | setupNodeEvents(on, config) {}, 7 | supportFile: false, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /tests/config-file/cypress.netlify.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress"); 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | env: { 6 | CUSTOM_FILE: 42, 7 | }, 8 | e2e: { 9 | setupNodeEvents(on, config) { 10 | // implement node event listeners here 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /tests/config-file/cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | describe('custom config file', () => { 3 | it('uses cypress.netlify.config.js', () => { 4 | // this property is set in the cypress.netlify.config.js file 5 | expect(Cypress.env()).to.have.property('CUSTOM_FILE', 42) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /tests/config-file/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /tests/config-file/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -------------------------------------------------------------------------------- /tests/config-file/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /tests/config-file/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "echo 'Netlify build command ...'" 3 | publish = "public" 4 | base = "tests/config-file" 5 | 6 | [[plugins]] 7 | # local Cypress plugin will test our site after it is built 8 | # in production, please use: package = "netlify-plugin-cypress" 9 | package = "../../" 10 | 11 | # do not run tests after deploy (testing) 12 | [plugins.inputs] 13 | enable = false 14 | configFile = "cypress.netlify.config.js" 15 | 16 | # run tests postBuild 17 | [plugins.inputs.postBuild] 18 | enable = true 19 | configFile = "cypress.netlify.config.js" 20 | -------------------------------------------------------------------------------- /tests/config-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config-file", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "../../node_modules/.bin/react-scripts start", 7 | "build": "../../node_modules/.bin/react-scripts build", 8 | "cypress:open": "../../node_modules/.bin/cypress open", 9 | "cypress:run": "../../node_modules/.bin/cypress run", 10 | "netlify:build": "../../node_modules/.bin/netlify build" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/config-file/public/index.html: -------------------------------------------------------------------------------- 1 |

Basic

2 | -------------------------------------------------------------------------------- /tests/html-pages/README.md: -------------------------------------------------------------------------------- 1 | # HTML pages 2 | 3 | See [#116](https://github.com/cypress-io/netlify-plugin-cypress/issues/116) 4 | 5 | In this example the public folder has both `index.html` and pages like `public/commands/about.html` which we want to visit using `cy.visit('/commands/about')` without using `.html` extension 6 | 7 | Test locally with 8 | 9 | ``` 10 | $ DEBUG=netlify-plugin-cypress ../../node_modules/.bin/netlify build 11 | ``` 12 | 13 | These tests run: 14 | - [ ] before the build 15 | - [x] after the build 16 | - [ ] on deploy success 17 | -------------------------------------------------------------------------------- /tests/html-pages/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | e2e: { 6 | setupNodeEvents(on, config) {}, 7 | supportFile: false, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /tests/html-pages/cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | describe('html-pages', () => { 3 | it('loads the index page', () => { 4 | cy.visit('/') 5 | cy.contains('Index page') 6 | }) 7 | 8 | it('loads the about page', () => { 9 | cy.visit('/commands/about') 10 | cy.contains('About') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/html-pages/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "echo 'Netlify build command ...'" 3 | publish = "public" 4 | base = "tests/html-pages" 5 | 6 | [[plugins]] 7 | # local Cypress plugin will test our site after it is built 8 | # in production, please use: package = "netlify-plugin-cypress" 9 | package = "../../" 10 | 11 | # do not run tests after deploy 12 | [plugins.inputs] 13 | enable = false 14 | 15 | # run and record tests post build 16 | [plugins.inputs.postBuild] 17 | enable = true 18 | -------------------------------------------------------------------------------- /tests/html-pages/public/commands/about.html: -------------------------------------------------------------------------------- 1 |

About

2 | -------------------------------------------------------------------------------- /tests/html-pages/public/index.html: -------------------------------------------------------------------------------- 1 |

Index page

2 | -------------------------------------------------------------------------------- /tests/recommended/README.md: -------------------------------------------------------------------------------- 1 | # recommended test 2 | 3 | These tests run: 4 | - [ ] before the build 5 | - [x] after the build 6 | - [ ] on deploy success 7 | -------------------------------------------------------------------------------- /tests/recommended/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | e2e: { 6 | setupNodeEvents(on, config) {}, 7 | supportFile: false, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /tests/recommended/cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | describe('recommended', () => { 3 | it('runs unit test', () => { 4 | expect(2 + 3).to.equal(5) 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /tests/recommended/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "echo 'Netlify build command ...'" 3 | publish = "public" 4 | base = "tests/recommended" 5 | 6 | [build.environment] 7 | # cache Cypress binary in local "node_modules" folder 8 | # so Netlify caches it 9 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 10 | # set TERM variable for terminal output 11 | TERM = "xterm" 12 | 13 | [[plugins]] 14 | # local Cypress plugin will test our site after it is built 15 | # in production, please use: package = "netlify-plugin-cypress" 16 | package = "../../" 17 | 18 | # this is a testing situation 19 | # in the real system we recommend running tests post deploy 20 | # by default and enable preBuild and postBuild if needed 21 | 22 | # do not run tests after deploy (testing) 23 | [plugins.inputs] 24 | enable = false 25 | 26 | # run tests postBuild 27 | [plugins.inputs.postBuild] 28 | enable = true 29 | -------------------------------------------------------------------------------- /tests/recommended/public/index.html: -------------------------------------------------------------------------------- 1 |

Basic

2 | -------------------------------------------------------------------------------- /tests/recording/README.md: -------------------------------------------------------------------------------- 1 | # recommended test 2 | 3 | These tests run: 4 | - [ ] before the build 5 | - [x] after the build 6 | - [ ] on deploy success 7 | -------------------------------------------------------------------------------- /tests/recording/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | projectId: 'ixroqc', 6 | e2e: { 7 | setupNodeEvents(on, config) {}, 8 | supportFile: false, 9 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /tests/recording/cypress/e2e/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | describe('recommended', () => { 3 | it('runs unit test', () => { 4 | expect(2 + 3).to.equal(5) 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /tests/recording/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "echo 'Netlify build command ...'" 3 | publish = "public" 4 | base = "tests/recording" 5 | 6 | [build.environment] 7 | # cache Cypress binary in local "node_modules" folder 8 | # so Netlify caches it 9 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 10 | # set TERM variable for terminal output 11 | TERM = "xterm" 12 | 13 | [[plugins]] 14 | # local Cypress plugin will test our site after it is built 15 | # in production, please use: package = "netlify-plugin-cypress" 16 | package = "../../" 17 | 18 | # do not run tests after deploy 19 | [plugins.inputs] 20 | enable = false 21 | 22 | # run and record tests post build 23 | [plugins.inputs.postBuild] 24 | enable = true 25 | record = true 26 | group = "test once after build" 27 | tag = "recommended" 28 | -------------------------------------------------------------------------------- /tests/recording/public/index.html: -------------------------------------------------------------------------------- 1 |

Basic

2 | -------------------------------------------------------------------------------- /tests/routing/.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | -------------------------------------------------------------------------------- /tests/routing/README.md: -------------------------------------------------------------------------------- 1 | # routing example 2 | > Client-side routing with fallback 3 | 4 | This example tests the redirect fallback that makes client-side routing defined in [src/App.js](src/App.js) work. See [netlify.toml](netlify.toml) 5 | 6 | ![Test screenshot](images/routing.png) 7 | 8 | These tests run: 9 | - [x] before the build 10 | - [x] after the build 11 | - [ ] on deploy success 12 | 13 | ## Local build 14 | 15 | Use command `DEBUG=netlify-plugin-cypress npm run netlify:build` to try the plugin locally 16 | -------------------------------------------------------------------------------- /tests/routing/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | e2e: { 6 | setupNodeEvents(on, config) {}, 7 | supportFile: false, 8 | baseUrl: 'http://localhost:3000', 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /tests/routing/cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | describe('Client-side routing', () => { 3 | it('goes from page to page', () => { 4 | cy.visit('/') 5 | 6 | // navigate to About page by clicking link 7 | cy.contains('a', 'About').click() 8 | // equivalent checks 9 | cy.url().should('match', /\/about$/) 10 | cy.location('pathname').should('equal', '/about') 11 | cy.contains('h2', 'About').should('be.visible') 12 | 13 | // navigate to Users page by clicking link 14 | cy.contains('a', 'Users').click() 15 | cy.url().should('match', /\/users$/) 16 | cy.location('pathname').should('equal', '/users') 17 | cy.contains('h2', 'Users').should('be.visible') 18 | 19 | // back to Home page 20 | cy.contains('a', 'Home').click() 21 | cy.location('pathname').should('equal', '/') 22 | }) 23 | 24 | it('can visit directly the About page', () => { 25 | // the server redirects all unknown paths back to "/" 26 | // where the client-side routing correctly sends it 27 | cy.visit('/about') 28 | cy.url().should('match', /\/about$/) 29 | cy.location('pathname').should('equal', '/about') 30 | cy.contains('h2', 'About').should('be.visible') 31 | }) 32 | 33 | it('can visit directly the Users page', () => { 34 | // the server redirects all unknown paths back to "/" 35 | // where the client-side routing correctly sends it 36 | cy.visit('/users') 37 | cy.url().should('match', /\/users$/) 38 | cy.location('pathname').should('equal', '/users') 39 | cy.contains('h2', 'Users').should('be.visible') 40 | }) 41 | }) 42 | 43 | -------------------------------------------------------------------------------- /tests/routing/images/routing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/netlify-plugin-cypress/1a4eb26c984c76c6579713468e5290a7d115c6b3/tests/routing/images/routing.png -------------------------------------------------------------------------------- /tests/routing/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "build" 4 | base = "tests/routing" 5 | 6 | [build.environment] 7 | # cache Cypress binary in local "node_modules" folder 8 | # so Netlify caches it 9 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 10 | # set TERM variable for terminal output 11 | TERM = "xterm" 12 | # do not print too many progress messages 13 | CI = "1" 14 | 15 | [[plugins]] 16 | # local Cypress plugin will test our site after it is built 17 | # in production, please use: package = "netlify-plugin-cypress" 18 | package = "../../" 19 | 20 | # run Cypress tests once on the site before it is built 21 | # and then after building the static folder 22 | 23 | [plugins.inputs] 24 | # these settings apply to the default step onSuccess 25 | # in this case, we do not want to run any tests after deploy 26 | enable = false 27 | 28 | # let's run tests against development server 29 | # before building it (and testing the built site) 30 | [plugins.inputs.preBuild] 31 | enable = true 32 | start = 'npm start' 33 | wait-on = 'http://localhost:3000' 34 | wait-on-timeout = '45' # seconds 35 | 36 | # and test the built site after 37 | [plugins.inputs.postBuild] 38 | enable = true 39 | # must allow our test server to redirect unknown routes to "/" 40 | # so that client-side routing can correctly route them 41 | # can be set to true or "index.html" (or similar fallback filename in the built folder) 42 | spa = true 43 | -------------------------------------------------------------------------------- /tests/routing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "routing-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "../../node_modules/.bin/react-scripts start", 7 | "build": "../../node_modules/.bin/react-scripts build", 8 | "cypress:open": "../../node_modules/.bin/cypress open", 9 | "cypress:run": "../../node_modules/.bin/cypress run", 10 | "netlify:build": "../../node_modules/.bin/netlify build", 11 | "serve:built": "../../node_modules/.bin/ws --directory build --spa index.html" 12 | }, 13 | "eslintConfig": { 14 | "extends": "react-app" 15 | }, 16 | "browserslist": { 17 | "production": [ 18 | ">0.2%", 19 | "not dead", 20 | "not op_mini all" 21 | ], 22 | "development": [ 23 | "last 1 chrome version", 24 | "last 1 firefox version", 25 | "last 1 safari version" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/routing/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/netlify-plugin-cypress/1a4eb26c984c76c6579713468e5290a7d115c6b3/tests/routing/public/favicon.ico -------------------------------------------------------------------------------- /tests/routing/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/routing/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /tests/routing/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | Link 7 | } from "react-router-dom"; 8 | 9 | export default function App() { 10 | return ( 11 | 12 |
13 | 26 | 27 | {/* A looks through its children s and 28 | renders the first one that matches the current URL. */} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 | ); 43 | } 44 | 45 | function Home() { 46 | return

Home

; 47 | } 48 | 49 | function About() { 50 | return

About

; 51 | } 52 | 53 | function Users() { 54 | return

Users

; 55 | } 56 | -------------------------------------------------------------------------------- /tests/routing/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /tests/routing/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /tests/routing/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ) 22 | 23 | export function register (config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href) 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config) 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ) 48 | }) 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | function registerValidSW (swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing 63 | if (installingWorker == null) { 64 | return 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ) 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration) 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.') 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error) 98 | }) 99 | } 100 | 101 | function checkValidServiceWorker (swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type') 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload() 115 | }) 116 | }) 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config) 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ) 126 | }) 127 | } 128 | 129 | export function unregister () { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister() 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/test-netlify-dev/README.md: -------------------------------------------------------------------------------- 1 | # use Netlify dev command 2 | 3 | These tests run: 4 | - [x] before the build 5 | - [ ] after the build 6 | - [ ] on deploy success 7 | 8 | ## Local build 9 | 10 | Use command `DEBUG=netlify-plugin-cypress npm run netlify:build` to try the plugin locally 11 | -------------------------------------------------------------------------------- /tests/test-netlify-dev/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | e2e: { 6 | setupNodeEvents(on, config) {}, 7 | supportFile: false, 8 | baseUrl: 'http://localhost:8888', 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /tests/test-netlify-dev/cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | it('loads page', () => { 3 | cy.visit('/') 4 | cy.contains('Placeholder').should('be.visible') 5 | }) 6 | 7 | it('greets me', () => { 8 | cy.visit('/') 9 | cy.request('/api/hello') 10 | .its('body') 11 | .should('equal', 'Hello from a serverless function!') 12 | }) 13 | -------------------------------------------------------------------------------- /tests/test-netlify-dev/netlify.toml: -------------------------------------------------------------------------------- 1 | # this Netlify project uses both functions 2 | # and API redirects 3 | 4 | [build] 5 | command = "echo 'Netlify build command ...'" 6 | publish = "public" 7 | functions = "src/functions" 8 | base = "tests/test-netlify-dev" 9 | 10 | [build.environment] 11 | # cache Cypress binary in local "node_modules" folder 12 | # so Netlify caches it 13 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 14 | # set TERM variable for terminal output 15 | TERM = "xterm" 16 | # do not print too many progress messages 17 | CI = "1" 18 | 19 | [dev] 20 | command = "npx serve public" 21 | targetPort = 3000 22 | framework = "#custom" 23 | 24 | [[redirects]] 25 | from = "/api/*" 26 | to = "/.netlify/functions/:splat" 27 | status = 200 28 | 29 | [[plugins]] 30 | # local Cypress plugin will test our site after it is built 31 | # in production, please use: package = "netlify-plugin-cypress" 32 | package = "../../" 33 | 34 | # the local site uses Netlify API redirects 35 | # and Netlify functions 36 | # let's run tests against Netlify Dev server 37 | [plugins.inputs.preBuild] 38 | enable = true 39 | start = 'npx netlify dev' 40 | wait-on = 'http://localhost:8888' 41 | 42 | # do no run tests after deploy 43 | [plugins.inputs] 44 | enable = false 45 | -------------------------------------------------------------------------------- /tests/test-netlify-dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prebuild-only-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "netlify:build": "../../node_modules/.bin/netlify build" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/test-netlify-dev/public/index.html: -------------------------------------------------------------------------------- 1 | Placeholder 2 | -------------------------------------------------------------------------------- /tests/test-netlify-dev/src/functions/hello.js: -------------------------------------------------------------------------------- 1 | // Netlify function 2 | exports.handler = async (event, context) => { 3 | try { 4 | return { statusCode: 200, body: `Hello from a serverless function!` } 5 | } catch (err) { 6 | return { statusCode: 500, body: err.toString() } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/test-postbuild-start/README.md: -------------------------------------------------------------------------------- 1 | # run post-build tests only with start command 2 | 3 | These tests run: 4 | - [ ] before the build 5 | - [x] after the build 6 | - [ ] on deploy success 7 | 8 | ## Local build 9 | 10 | Use command `DEBUG=netlify-plugin-cypress npm run netlify:build` to try the plugin locally 11 | -------------------------------------------------------------------------------- /tests/test-postbuild-start/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | projectId: 'ixroqc', 6 | e2e: { 7 | setupNodeEvents(on, config) {}, 8 | supportFile: false, 9 | baseUrl: 'http://localhost:3000', 10 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /tests/test-postbuild-start/cypress/e2e/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | it('loads page', () => { 3 | cy.visit('/') 4 | cy.contains('Placeholder').should('be.visible') 5 | }) 6 | -------------------------------------------------------------------------------- /tests/test-postbuild-start/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "echo 'Netlify build command ...'" 3 | publish = "public" 4 | base = "tests/test-postbuild-start" 5 | 6 | [build.environment] 7 | # cache Cypress binary in local "node_modules" folder 8 | # so Netlify caches it 9 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 10 | # set TERM variable for terminal output 11 | TERM = "xterm" 12 | # do not print too many progress messages 13 | CI = "1" 14 | 15 | [[plugins]] 16 | # local Cypress plugin will test our site after it is built 17 | # in production, please use: package = "netlify-plugin-cypress" 18 | package = "../../" 19 | 20 | # run Cypress tests once on the site before it is built 21 | # and do not run the tests after it was built 22 | 23 | [plugins.inputs] 24 | # these settings apply to the default step onSuccess 25 | # in this case, we do not want to run any tests after deploy 26 | enable = false 27 | 28 | # let's run tests against development server 29 | [plugins.inputs.postBuild] 30 | enable = true 31 | start = 'npx serve public' 32 | wait-on = 'http://localhost:3000' 33 | wait-on-timeout = '30' # seconds 34 | -------------------------------------------------------------------------------- /tests/test-postbuild-start/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postbuild-start-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "netlify:build": "../../node_modules/.bin/netlify build" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/test-postbuild-start/public/index.html: -------------------------------------------------------------------------------- 1 | Placeholder 2 | -------------------------------------------------------------------------------- /tests/test-prebuild-only/README.md: -------------------------------------------------------------------------------- 1 | # run pre-build tests only 2 | 3 | These tests run: 4 | - [x] before the build 5 | - [ ] after the build 6 | - [ ] on deploy success 7 | 8 | ## Local build 9 | 10 | Use command `DEBUG=netlify-plugin-cypress npm run netlify:build` to try the plugin locally 11 | -------------------------------------------------------------------------------- /tests/test-prebuild-only/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | projectId: 'ixroqc', 6 | e2e: { 7 | setupNodeEvents(on, config) {}, 8 | supportFile: false, 9 | baseUrl: 'http://localhost:3000', 10 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /tests/test-prebuild-only/cypress/e2e/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | it('loads page', () => { 3 | cy.visit('/') 4 | cy.contains('Placeholder').should('be.visible') 5 | }) 6 | -------------------------------------------------------------------------------- /tests/test-prebuild-only/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "echo 'Netlify build command ...'" 3 | publish = "public" 4 | base = "tests/test-prebuild-only" 5 | 6 | [build.environment] 7 | # cache Cypress binary in local "node_modules" folder 8 | # so Netlify caches it 9 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 10 | # set TERM variable for terminal output 11 | TERM = "xterm" 12 | # do not print too many progress messages 13 | CI = "1" 14 | 15 | [[plugins]] 16 | # local Cypress plugin will test our site after it is built 17 | # in production, please use: package = "netlify-plugin-cypress" 18 | package = "../../" 19 | 20 | # run Cypress tests once on the site before it is built 21 | # and do not run the tests after it was built 22 | 23 | [plugins.inputs] 24 | # these settings apply to the default step onSuccess 25 | # in this case, we do not want to run any tests after deploy 26 | enable = false 27 | 28 | # let's run tests against development server 29 | [plugins.inputs.preBuild] 30 | enable = true 31 | start = 'npx serve public' 32 | wait-on = 'http://localhost:3000' 33 | wait-on-timeout = '30' # seconds 34 | -------------------------------------------------------------------------------- /tests/test-prebuild-only/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prebuild-only-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "netlify:build": "../../node_modules/.bin/netlify build" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/test-prebuild-only/public/index.html: -------------------------------------------------------------------------------- 1 | Placeholder 2 | -------------------------------------------------------------------------------- /tests/test-twice/README.md: -------------------------------------------------------------------------------- 1 | # routing example 2 | > Client-side routing with fallback 3 | 4 | This example tests the redirect fallback that makes client-side routing defined in [src/App.js](src/App.js) work. See [netlify.toml](netlify.toml) 5 | 6 | ![Test screenshot](images/routing.png) 7 | 8 | These tests run: 9 | - [x] before the build 10 | - [x] after the build 11 | - [ ] on deploy success 12 | 13 | ## Local build 14 | 15 | Use command `DEBUG=netlify-plugin-cypress npm run netlify:build` to try the plugin locally 16 | -------------------------------------------------------------------------------- /tests/test-twice/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | projectId: 'ixroqc', 6 | e2e: { 7 | setupNodeEvents(on, config) {}, 8 | supportFile: false, 9 | baseUrl: 'http://localhost:3000', 10 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /tests/test-twice/cypress/e2e/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | it('loads page', () => { 3 | cy.visit('/') 4 | cy.contains('Placeholder').should('be.visible') 5 | }) 6 | -------------------------------------------------------------------------------- /tests/test-twice/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "echo 'Netlify build command ...'" 3 | publish = "public" 4 | base = "tests/test-twice" 5 | 6 | [build.environment] 7 | # cache Cypress binary in local "node_modules" folder 8 | # so Netlify caches it 9 | CYPRESS_CACHE_FOLDER = "./node_modules/CypressBinary" 10 | # set TERM variable for terminal output 11 | TERM = "xterm" 12 | 13 | [[plugins]] 14 | # local Cypress plugin will test our site after it is built 15 | # in production, please use: package = "netlify-plugin-cypress" 16 | package = "../../" 17 | 18 | # run Cypress tests once on the site before it is built 19 | # and then after building the static folder 20 | 21 | # do not run tests after deploy 22 | [plugins.inputs] 23 | enable = false 24 | 25 | # let's run tests against development server 26 | # before building it (and testing the built site) 27 | [plugins.inputs.preBuild] 28 | enable = true 29 | start = 'npx serve public' 30 | wait-on = 'http://localhost:3000' 31 | wait-on-timeout = '30' # seconds 32 | record = true 33 | group = "test twice: 1 - before build" 34 | tag = "test twice" 35 | 36 | # and test after build 37 | [plugins.inputs.postBuild] 38 | record = true 39 | group = "test twice: 2 - after build" 40 | tag = "test twice" 41 | -------------------------------------------------------------------------------- /tests/test-twice/public/index.html: -------------------------------------------------------------------------------- 1 | Placeholder 2 | -------------------------------------------------------------------------------- /tests/use-chromium/README.md: -------------------------------------------------------------------------------- 1 | # use Chromium browser 2 | 3 | These tests run: 4 | - [ ] before the build 5 | - [x] after the build 6 | - [ ] on deploy success 7 | -------------------------------------------------------------------------------- /tests/use-chromium/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | e2e: { 6 | setupNodeEvents(on, config) {}, 7 | supportFile: false, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /tests/use-chromium/cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | describe('basic', () => { 3 | it('runs unit test', () => { 4 | expect(2 + 3).to.equal(5) 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /tests/use-chromium/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "echo 'Netlify build command ...'" 3 | publish = "public" 4 | base = "tests/use-chromium" 5 | 6 | [[plugins]] 7 | # local Cypress plugin will test our site after it is built 8 | # in production, please use: package = "netlify-plugin-cypress" 9 | package = "../../" 10 | 11 | [plugins.inputs] 12 | # do not run tests after deploy 13 | enable = false 14 | 15 | # only run tests postBuild before deploy 16 | [plugins.inputs.postBuild] 17 | enable = true 18 | browser = "chromium" 19 | -------------------------------------------------------------------------------- /tests/use-chromium/public/index.html: -------------------------------------------------------------------------------- 1 |

Basic

2 | --------------------------------------------------------------------------------