├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ ├── check-package-version.yml │ ├── publish-to-github-registry.yml │ ├── publish-to-npm.yml │ └── test-mutation.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── assets │ └── cypress-fail-fast-screenshot.png ├── index.d.ts ├── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── plugin.d.ts ├── plugin.js ├── renovate.json ├── sonar-project.properties ├── src ├── helpers │ ├── config.js │ ├── constants.js │ └── cypress.js ├── plugin.js └── support.js ├── stryker.conf.js ├── test-e2e ├── .gitignore ├── commands │ ├── build.js │ └── support │ │ ├── copy.js │ │ └── variants.js ├── cypress-src │ ├── .eslintrc.json │ ├── babel.config.js │ ├── cypress.config.js │ ├── cypress.config.ts │ ├── cypress.json │ ├── integration │ │ ├── all-tests-passing │ │ │ ├── a-file.js │ │ │ ├── b-file.js │ │ │ └── c-file.js │ │ ├── before-failing │ │ │ ├── a-file.js │ │ │ ├── b-file.js │ │ │ ├── c-file.js │ │ │ └── d-file.js │ │ ├── describe-disabled-test-enabled │ │ │ ├── a-file.js │ │ │ ├── b-file.js │ │ │ └── c-file.js │ │ ├── describe-enabled-test-disabled │ │ │ ├── a-file.js │ │ │ ├── b-file.js │ │ │ └── c-file.js │ │ ├── environment-config-only │ │ │ ├── a-file.js │ │ │ ├── b-file.js │ │ │ └── c-file.js │ │ ├── grandparent-describe-enabled │ │ │ ├── a-file.js │ │ │ ├── b-file.js │ │ │ └── c-file.js │ │ ├── many-failing-tests │ │ │ ├── a-file.js │ │ │ ├── b-file.js │ │ │ └── c-file.js │ │ └── parallel-failing │ │ │ ├── a-file.js │ │ │ ├── b-file.js │ │ │ └── c-file.js │ ├── parallel-preprocessor-babel-config.config.js │ ├── parallel-ts.config.js │ ├── plugins-custom │ │ ├── parallel-preprocessor-babel-config │ │ │ ├── index.js │ │ │ └── index.ts │ │ ├── parallel │ │ │ ├── index.js │ │ │ └── index.ts │ │ └── preprocessor-babel-config │ │ │ ├── index.js │ │ │ └── index.ts │ ├── plugins │ │ ├── index.js │ │ └── index.ts │ ├── scripts │ │ └── reports.js │ └── support │ │ ├── e2e.js │ │ ├── e2e.ts │ │ ├── index.js │ │ └── index.ts ├── cypress-variants │ ├── cypress-10 │ │ ├── .gitignore │ │ ├── package-lock.json │ │ └── package.json │ ├── cypress-11 │ │ ├── .gitignore │ │ ├── package-lock.json │ │ └── package.json │ ├── cypress-12 │ │ ├── .gitignore │ │ ├── package-lock.json │ │ └── package.json │ ├── cypress-13 │ │ ├── .gitignore │ │ ├── package-lock.json │ │ └── package.json │ ├── cypress-9 │ │ ├── .gitignore │ │ ├── package-lock.json │ │ └── package.json │ └── typescript │ │ ├── .gitignore │ │ ├── package-lock.json │ │ ├── package.json │ │ └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── parallel-storage │ └── .gitignore ├── public │ └── index.html ├── serve.json └── test │ ├── .eslintrc.json │ ├── before.spec.js │ ├── environment-config.spec.js │ ├── parallel.spec.js │ ├── strategy.spec.js │ ├── support │ ├── logs.js │ ├── npmCommandRunner.js │ └── testsRunner.js │ └── test-config.spec.js ├── test ├── .eslintrc.json ├── config.spec.js ├── plugin.spec.js └── support.spec.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 2022 8 | }, 9 | "plugins": ["prettier"], 10 | "rules": { 11 | "prettier/prettier": [ 12 | "error", 13 | { 14 | "printWidth": 99, 15 | "parser": "flow" 16 | } 17 | ], 18 | "no-undef": "error", 19 | "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }] 20 | }, 21 | "extends": ["prettier"], 22 | "root": true 23 | } 24 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thanks for taking the time to contribute! 4 | 5 | The following is a set of guidelines for contributing to this project. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | #### Table Of Contents 8 | 9 | * [Code of Conduct](#code-of-conduct) 10 | * [Project governance](#project-governance) 11 | * [Rules](#rules) 12 | * [Releases](#releases) 13 | * [Changes to this arrangement](#changes-to-this-arrangement) 14 | * [Pull Requests](#pull-requests) 15 | * [Styleguides](#styleguides) 16 | * [Git Commit Messages](#git-commit-messages) 17 | * [JavaScript Styleguide](#javascript-styleguide) 18 | * [Tests Styleguide](#tests-styleguide) 19 | * [Developer's certificate of origin](#developers-certificate-of-origin) 20 | 21 | ## Code of Conduct 22 | 23 | This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 24 | 25 | ## Project Governance 26 | 27 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. 28 | 29 | ### Rules 30 | 31 | There are a few basic ground-rules for contributors: 32 | 33 | 1. **No `--force` pushes** or modifying the Git history in any way. 34 | 2. **All modifications** should be subject to a **pull request** to solicit feedback from other contributors. The base branch of the pull request should be the `release` branch. 35 | 3. **All changes** to this project should be documented in the GHANGELOG.md file in the `unreleased` chapter until a formal release is declared. 36 | 37 | ### Releases 38 | 39 | Declaring formal releases remains the prerogative of the project maintainer. 40 | 41 | ### Changes to this arrangement 42 | 43 | This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. 44 | 45 | ## Pull Requests 46 | 47 | * Fill in [the required template](PULL_REQUEST_TEMPLATE.md). 48 | * Do not include issue numbers in the PR title. 49 | * Follow the [JavaScript styleguide](#javascript-styleguide). 50 | * Follow the [Tests styleguide](#tests-styleguide). 51 | * All enhancements and bug fixes must be accompanied with all needed new related regression test. 52 | * Coverage of unit tests must remain 100%. 53 | * Run tests often. Tests are ran automatically when a PR is opened, but you still need to run them locally before creating it. 54 | * Document new features, or update documentation if changes affect to it. 55 | * End all files with a newline. 56 | * Place requires in the following order: 57 | * Built in Node Modules (such as `path`) 58 | * NPM Modules (such as `lodash`) 59 | * Local Modules (using relative paths) 60 | 61 | ## Styleguides 62 | 63 | ### Git Commit Messages 64 | 65 | * Use [semmantic commit](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) 66 | 67 | ### JavaScript Styleguide 68 | 69 | All JavaScript must adhere to the style defined in the `.eslintrc.json` file. 70 | 71 | ### Tests Styleguide 72 | 73 | - Fail tests first. How do you know if it is actually testing anything if the assert never failed? 74 | - Treat `describe` as a noun or situation (Situations usually start with "when"). 75 | - Treat `it` as a statement about state or how an operation changes state. Usually, all `it` should start with "should". 76 | - Prefer fewer asserts per `it`. 77 | 78 | #### Example 79 | 80 | ```js 81 | describe("a dog", () => { 82 | describe("when is happy", () => { 83 | it("should wags its tail", () => { 84 | expect(dog.tail.moving).to.be.true(); 85 | }); 86 | }); 87 | }); 88 | ``` 89 | 90 | ## Developer's Certificate of Origin 91 | 92 | By making a contribution to this project, I certify that: 93 | 94 | - (a) The contribution was created in whole or in part by me and I have the right to 95 | submit it under the open source license indicated in the file; or 96 | 97 | - (b) The contribution is based upon previous work that, to the best of my knowledge, is 98 | covered under an appropriate open source license and I have the right under that license 99 | to submit that work with modifications, whether created in whole or in part by me, under 100 | the same open source license (unless I am permitted to submit under a different 101 | license), as indicated in the file; or 102 | 103 | - (c) The contribution was provided directly to me by some other person who certified 104 | (a), (b) or (c) and I have not modified it. 105 | 106 | - (d) I understand and agree that this project and the contribution are public and that a 107 | record of the contribution (including all personal information I submit with it, 108 | including my sign-off) is maintained indefinitely and may be redistributed consistent 109 | with this project or the open source license(s) involved. 110 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Logs** 17 | If applicable, add logs to help explain your problem. 18 | 19 | ** Operating system, Node.js an npm versions, or browser version (please complete the following information):** 20 | - OS: [e.g. Ubuntu 18.04] 21 | - Node.js: [e.g. 8.11.1] 22 | - npm: [e.g. 5.6.0] 23 | - Browser: [e.g. Chrome 73.0.3683] 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or examples about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **IMPORTANT: Please do not create a Pull Request without creating an issue first.** 2 | 3 | *Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of the pull request.* 4 | 5 | Please provide enough information so that others can review your pull request: 6 | 7 | Explain the **details** for making this change. What existing problem does the pull request solve? 8 | 9 | **Closing issues** 10 | 11 | Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes. 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - release 7 | pull_request: 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | max-parallel: 2 16 | matrix: 17 | node: ["18.20.2", "20.13.1", "22.2.0"] 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Use Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node }} 25 | - name: Cache node modules 26 | uses: actions/cache@v4 27 | env: 28 | cache-name: cache-node-modules 29 | with: 30 | # npm cache files are stored in `~/.npm` on Linux/macOS 31 | path: ~/.npm 32 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 33 | restore-keys: | 34 | ${{ runner.os }}-build-${{ env.cache-name }}- 35 | ${{ runner.os }}-build- 36 | ${{ runner.os }}- 37 | - name: Install dependencies 38 | run: npm ci 39 | - name: Lint 40 | run: npm run lint 41 | - name: Check types 42 | run: npm run tsc 43 | - name: Test unit 44 | run: npm run test:unit 45 | - name: Test E2E 46 | run: npm run test:e2e:ci 47 | id: test-e2e 48 | - name: Upload test results 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: coverage-${{ matrix.node }} 52 | path: coverage 53 | retention-days: 1 54 | quality: 55 | runs-on: ubuntu-latest 56 | needs: test 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | with: 61 | fetch-depth: 0 62 | - name: Download test results 63 | uses: actions/download-artifact@v4 64 | with: 65 | name: coverage-20.13.1 66 | path: coverage 67 | - name: Coveralls 68 | uses: coverallsapp/github-action@master 69 | with: 70 | github-token: ${{ secrets.GITHUB_TOKEN }} 71 | - name: SonarCloud Scan 72 | uses: sonarsource/sonarcloud-github-action@master 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 76 | -------------------------------------------------------------------------------- /.github/workflows/check-package-version.yml: -------------------------------------------------------------------------------- 1 | name: check-package-version 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | jobs: 10 | check-package-version: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Get NPM version is new 16 | id: check 17 | uses: EndBug/version-check@v2.1.4 18 | with: 19 | diff-search: true 20 | file-name: ./package.json 21 | file-url: https://unpkg.com/cypress-fail-fast@latest/package.json 22 | static-checking: localIsNew 23 | - name: Check version is new 24 | if: steps.check.outputs.changed != 'true' 25 | run: | 26 | echo "Version not changed" 27 | exit 1 28 | - name: Get NPM version 29 | id: package-version 30 | uses: martinbeentjes/npm-get-version-action@v1.3.1 31 | - name: Check Changelog version 32 | id: changelog_reader 33 | uses: mindsers/changelog-reader-action@v2.2.3 34 | with: 35 | version: ${{ steps.package-version.outputs.current-version }} 36 | path: ./CHANGELOG.md 37 | - name: Read version from Sonar config 38 | id: sonar-version 39 | uses: christian-draeger/read-properties@1.1.1 40 | with: 41 | path: './sonar-project.properties' 42 | properties: 'sonar.projectVersion' 43 | - name: Check Sonar version 44 | if: steps.sonar-version.outputs.sonar-projectVersion != steps.package-version.outputs.current-version 45 | run: | 46 | echo "Version not changed" 47 | exit 1 48 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-github-registry.yml: -------------------------------------------------------------------------------- 1 | name: publish-to-github-registry 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - run: npm ci 11 | # Setup .npmrc file to publish to GitHub Packages 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: '22.x' 15 | registry-url: 'https://npm.pkg.github.com' 16 | # Defaults to the user or organization that owns the workflow file 17 | scope: '@javierbrea' 18 | - uses: MerthinTechnologies/edit-json-action@v1 19 | with: 20 | filename: './package.json' 21 | key: 'name' 22 | value: '@javierbrea/cypress-fail-fast' 23 | - run: npm publish 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-npm.yml: -------------------------------------------------------------------------------- 1 | name: publish-to-npm 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: '22.x' 13 | registry-url: 'https://registry.npmjs.org/' 14 | - run: npm ci 15 | - run: npm publish 16 | env: 17 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/test-mutation.yml: -------------------------------------------------------------------------------- 1 | name: test-mutation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - release 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | jobs: 11 | test-mutation: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Extract branch name 16 | shell: bash 17 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})" 18 | id: extract-branch 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: '22.x' 22 | registry-url: 'https://registry.npmjs.org/' 23 | - name: Cache node modules 24 | uses: actions/cache@v4 25 | env: 26 | cache-name: cache-node-modules 27 | with: 28 | # npm cache files are stored in `~/.npm` on Linux/macOS 29 | path: ~/.npm 30 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-build-${{ env.cache-name }}- 33 | ${{ runner.os }}-build- 34 | ${{ runner.os }}- 35 | - name: Install dependencies 36 | run: npm ci 37 | - name: Test mutation 38 | run: npm run test:mutation 39 | env: 40 | BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }} 41 | STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | #environment variables 4 | /.env 5 | 6 | # dependencies 7 | /node_modules 8 | 9 | # tests 10 | /coverage 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # ides 25 | .idea 26 | .vs 27 | 28 | # stryker temp files 29 | /reports 30 | .stryker-tmp 31 | stryker.conf.local.js 32 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "sonarlint.connectedMode.project": { 3 | "connectionId": "javierbrea", 4 | "projectKey": "javierbrea_cypress-fail-fast" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [unreleased] 8 | ### Added 9 | ### Changed 10 | ### Fixed 11 | ### Removed 12 | ### BREAKING CHANGES 13 | 14 | ## [7.1.1] - 2024-08-04 15 | 16 | ### Fixed 17 | 18 | - fix(#296): Force to set the `shouldSkip` flag to `true` when a before hook fails in Cypress 13. 19 | 20 | ### Changed 21 | - docs(#295): Change installation example to ES6 import syntax 22 | - chore(deps): Update devDependencies 23 | 24 | ## [7.1.0] - 2023-11-21 25 | 26 | ### Added 27 | - test: Add Cypress v13 tests. Use Cypress 13 in TypeScript tests 28 | 29 | ### Removed 30 | - test: Drop Cypress 12 tests 31 | 32 | ### Changed 33 | - chore(deps): Update devDependencies 34 | 35 | ## [7.0.3] - 2023-08-26 36 | 37 | ### Fixed 38 | - fix(#290): Do not use optional chaining. It was producing an error in Cypress. See https://github.com/cypress-io/cypress/issues/9298 39 | 40 | ### Changed 41 | - test(deps): Update Cypress to latest version in TypeScript E2E tests 42 | 43 | ## [7.0.2] - 2023-08-16 [YANKED] 44 | 45 | ### Changed 46 | - chore(deps): Update devDependencies 47 | - chore(deps): Use NodeJs 16.x, 18.x and 20.x in pipelines 48 | - refactor: Remove redundant double negation 49 | - refactor: Use optional chain expressions 50 | 51 | ### Added 52 | - chore: Handle concurrency in pipelines 53 | 54 | ## [7.0.1] - 2023-04-04 55 | 56 | ### Changed 57 | - chore(deps): Update devDependencies 58 | 59 | ## [7.0.0] - 2022-12-08 60 | 61 | ### Added 62 | - test: Add Cypress v12 tests 63 | 64 | ### Removed 65 | - test: Drop support for Cypress 7 66 | 67 | ## [6.0.0] - 2022-12-07 68 | 69 | ### Added 70 | - feat(#275): Extend suite configuration definitions for Cypress 11.x 71 | - test: Add Cypress v11 tests. Use Cypress 11 in TypeScript tests 72 | 73 | ### Changed 74 | - chore(deps): Update devDependencies 75 | 76 | ### Removed 77 | - test: Drop support for Cypress 6 78 | 79 | ### Fixed 80 | - docs(#245): Fix parallel configuration example 81 | 82 | ## [5.0.1] - 2022-08-30 83 | 84 | ### Changed 85 | - chore(deps): Update devDependencies 86 | 87 | ## [5.0.0] - 2022-06-02 88 | 89 | ### Added 90 | - feat(#213): Add support for Cypress 10. 91 | - test(#213): Run E2E tests using also Cypress 10. Adapt config. 92 | - docs(#213): Add docs about how to install the plugin in Cypress 10 93 | 94 | ### Removed 95 | - chore: Drop support for Cypress 5 96 | 97 | ## [4.0.0] - 2022-05-30 98 | 99 | ### Removed 100 | - chore: Drop support for Node.js versions lower than v14 101 | 102 | ### Changed 103 | - chore(deps): Update devDependencies 104 | 105 | ## [3.4.1] - 2022-03-02 106 | 107 | ### Fixed 108 | - fix(#193): Do not log "Enabling skip mode" in every failed test. When a test fails, log "Failed tests: x/y", where `y` is the bail option. 109 | 110 | ### Changed 111 | - chore(deps): Update devDependencies 112 | 113 | ## [3.4.0] - 2022-02-24 114 | 115 | ### Added 116 | - feat(#186): Add CYPRESS_FAIL_FAST_BAIL option 117 | 118 | ### Changed 119 | - chore(deps): Update package-lock files to NPM v8 120 | - test(e2e): Increase tests stability. Fix flaky test in parallel specs 121 | - test(e2e): Turn tsc check into an assertion 122 | - refactor: Reduce cognitive complexity 123 | - chore(deps): Update devDependencies 124 | 125 | ## [3.3.0] - 2021-11-13 126 | 127 | ### Added 128 | - feat(#169): Support Cypress 9.x 129 | - test(#169): Run E2E tests also with Cypress 9.x 130 | - chore: Do not run pipelines with Node.js v12.x in order to make tests lighter 131 | 132 | ### Changed 133 | - chore(deps): Support any Cypress version greater than 5.x in peerDependencies. 134 | - chore(deps): Update devDependencies 135 | - chore(deps): Configure renovate to not upgrade Cypress major versions in E2E tests of versions 7.x and 8.x 136 | 137 | ### Removed 138 | - docs: Remove npm dependencies broken badge 139 | 140 | ## [3.2.0] - 2021-11-01 141 | 142 | ### Changed 143 | - chore(deps): Update devDependencies 144 | - chore(deps): Support any NodeJs version greater than 10.x. 145 | 146 | ### Fixed 147 | - fix(#124): Skip nested before hooks when one fails (#164) 148 | 149 | ## [3.1.1] - 2021-08-21 150 | 151 | ### Added 152 | - test(#151): Add TypeScript types check. Check types also in E2E tests 153 | - docs: Add Cypress v8.x support to docs 154 | 155 | ### Changed 156 | - chore(deps): Update dependencies 157 | 158 | ### Fixed 159 | - fix(#151): Fix TypeScript declarations. Remove `TestConfigOverrides` recursively references 160 | 161 | ## [3.1.0] - 2021-07-22 162 | 163 | ### Added 164 | - chore(#129): Support Cypress v8.x in peerDependencies. Add E2E tests using Cypress v8 165 | 166 | ### Changed 167 | - chore(deps): Update dependencies 168 | 169 | ## [3.0.0] - 2021-06-23 170 | 171 | ### Added 172 | - feat(#119): Force the next test to fail when a "before" or "beforeEach" hook fails, so the execution is marked as "failed", and fail fast mode can be enabled. 173 | - feat: Add logs when skip mode is enabled, and when Cypress runner is stopped. 174 | 175 | ### Changed 176 | - refactor: Improve code readability 177 | - chore(deps): Update dependencies 178 | 179 | ### Removed 180 | - feat: Do not apply fail fast on other hooks apart from "before" and "beforeEach" 181 | 182 | ### BREAKING CHANGES 183 | - Fail fast is only applied on "before" and "beforeEach" hooks failures. Other hooks are ignored. 184 | 185 | ## [2.4.0] - 2021-06-10 186 | 187 | ### Added 188 | - feat(#91): Enter skip mode if any hook fails 189 | 190 | ### Changed 191 | - chore(deps): Update devDependencies 192 | 193 | ## [2.3.3] - 2021-05-29 194 | 195 | ### Changed 196 | - chore(deps): Update devDependencies 197 | - chore: Migrate Sonar project 198 | 199 | ## [2.3.2] - 2021-04-28 200 | 201 | ### Added 202 | - chore(deps): Support Node v16.x in engines. Run tests also in node 16.0.0 203 | 204 | ### Changed 205 | - chore(deps): Update devDependencies 206 | 207 | ## [2.3.1] - 2021-04-07 208 | 209 | ### Added 210 | - chore(deps): Support Cypress v7.x in peerDependencies 211 | - test(e2e): Run e2e tests also in Cypress v7.x 212 | 213 | ### Changed 214 | - chore(pipelines): Update node versions 215 | - chore(pipelines): Do not run tests in Node 10, because it is not supported by Cypress v7.x 216 | - chore(deps): Update devDependencies 217 | - chore(renovate): Configure renovate to not update Cypress to a version higher than 6.x in Cypress 6.x e2e tests folder 218 | - test(e2e): Do not trace npm commands logs until DEBUG environment variable is set to true 219 | 220 | ## [2.3.0] - 2021-04-04 221 | 222 | ### Added 223 | - feat: Add FAIL_FAST_STRATEGY environment variable, allowing to skip tests only in current spec file, in current run or in parallel runs (#29) 224 | - feat: Add configuration allowing to implement fail-fast in parallel runs (#33). 225 | 226 | ### Changed 227 | - chore(ci): Separate test mutation job to a new workflow 228 | - chore(deps): Update devDependencies 229 | 230 | ## [2.2.2] - 2021-03-30 231 | 232 | ### Fixed 233 | - fix: Fix configuration by test in Cypress versions higher than 6.6 (#73) 234 | 235 | ### Changed 236 | - chore(deps): Update devDependencies 237 | 238 | ## [2.2.1] - 2021-03-24 239 | 240 | ### Fixed 241 | - fix(ts): Make failFast property optional in test configuration (#69) 242 | - docs: Fix typo in readme 243 | 244 | ### Changed 245 | - chore(deps): Update devDependencies 246 | 247 | ## [2.2.0] - 2021-02-21 248 | 249 | ### Added 250 | - test(e2e): Check that `test:after:run` event is executed in failed tests using mochawesome reporter (closes #61) 251 | - feat: Stop Cypress runner in before hook in headless mode when tests should be skipped (closes #52) 252 | 253 | ### Fixed 254 | - fix: Mark current test as pending when it has to be skipped (related to #61) 255 | 256 | ## [2.1.1] - 2021-02-21 257 | 258 | ### Changed 259 | - chore(deps): Configure renovate to no upgrade Cypress version in v5 e2e tests 260 | 261 | ### Fixed 262 | - fix: Revert change producing unstability (#61). 263 | 264 | ### Removed 265 | - chore(deps): Remove unused devDependency 266 | 267 | ## [2.1.0] - 2021-02-21 268 | 269 | ### Added 270 | - test(e2e): Check that `test:after:run` event is executed in failed tests. 271 | 272 | ### Changed 273 | - feat: Do not stop runner from failed test hook and execute flag task "parallely" in order to let execute test:after:run events. (closes #61) 274 | - test(e2e): Update Cypress 6 to latest version. 275 | - chore(deps): Update devDependencies 276 | 277 | ### Removed 278 | - test(unit): Remove duplicated test 279 | 280 | ## [2.0.0] - 2021-01-17 281 | 282 | ### Added 283 | - feat: Add FAIL_FAST_ENABLED environment variable (#53) 284 | - feat: Allow environment variables to be enabled with 1, and disabled with 0 285 | 286 | ### Changed 287 | - feat: Rename FAIL_FAST environment variable to FAIL_FAST_PLUGIN (#53) 288 | - test(e2e): Allow some tests to be executed only in last Cypress version in order to reduce timings 289 | - chore(deps): Update devDependencies 290 | 291 | ### BREAKING CHANGES 292 | - feat: Plugin is now enabled by default (#44). To disable it, FAIL_FAST_PLUGIN environment variable has to be explicitly set as "false". Removed FAIL_FAST environment variable, which now has not any effect. 293 | 294 | ## [1.4.0] - 2021-01-02 295 | 296 | ### Added 297 | - feat: Add suite and tests plugin custom configuration. Enable or disable plugin for suites or tests using the enabled property from custom config 298 | - test(e2e): Add helper to run E2E tests with different specs files and configurations 299 | 300 | ### Changed 301 | - feat: Do not log plugin tasks, except when setting shouldSkip flag to true 302 | - docs: Change TypeScript example 303 | - refactor: Do not check plugin configuration inside Node.js plugin 304 | - refactor: Rename plugin tasks. Start all with same namespace 305 | 306 | ### Removed 307 | - chore: Remove unused eslint settings from src folder 308 | 309 | ## [1.3.1] - 2020-12-31 310 | ### Fixed 311 | - docs: Fix E2E tests versions links 312 | 313 | ## [1.3.0] - 2020-12-31 314 | ### Added 315 | - feat: Add TypeScript declarations (#37) 316 | - test(e2e): Add E2E tests using TypeScript in Cypress 317 | 318 | ### Changed 319 | - test(e2e): Refactor E2E tests to avoid code duplications. Now there is a common tests runner and code is generated for each different Cypress variant (except package.json files in order to allow renovate continue updating dependencies) 320 | - docs: Update contributing guidelines 321 | - chore(deps): Update dependencies 322 | 323 | ## [1.2.1] - 2020-12-10 324 | ### Fixed 325 | - docs(badge): Fix build badge 326 | 327 | ## [1.2.0] - 2020-12-10 328 | ### Added 329 | - chore(deps): Add node 10.x support 330 | 331 | ### Changed 332 | - docs(readme): Improve docs 333 | - chore(deps): Update Cypress 6.x version used in E2E 334 | - chore(pipeline): Migrate pipelines to Github actions 335 | 336 | ## [1.1.0] - 2020-11-28 337 | ### Added 338 | - test(deps): Add support for Cypress v6.x 339 | 340 | ### Fixed 341 | - docs(readme): Fix installation instructions 342 | 343 | ## [1.0.0] - 2020-11-28 344 | ### Added 345 | - First release 346 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 Javier Brea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status][build-image]][build-url] [![Coverage Status][coveralls-image]][coveralls-url] [![Quality Gate][quality-gate-image]][quality-gate-url] [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fjavierbrea%2Fcypress-fail-fast%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/javierbrea/cypress-fail-fast/main) 2 | 3 | [![Renovate](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com) [![Last commit][last-commit-image]][last-commit-url] [![Last release][release-image]][release-url] 4 | 5 | [![NPM downloads][npm-downloads-image]][npm-downloads-url] [![License][license-image]][license-url] 6 | 7 | # Cypress Fail Fast 8 | 9 | Enables fail fast in Cypress, skipping the rest of tests on first failure. 10 | 11 | It can be configured to skip all remaining tests in current spec file, in current run, or even in parallel runs. 12 | 13 | ## Table of Contents 14 | 15 | - [Installation](#installation) 16 | - [Limitations and notes](#limitations-and-notes) 17 | - [Configuration](#configuration) 18 | * [Environment variables](#environment-variables) 19 | * [Configuration by test](#configuration-by-test) 20 | * [Configuration examples for usual scenarios](#configuration-examples-for-usual-scenarios) 21 | * [Configuration for parallel runs](#configuration-for-parallel-runs) 22 | - [Usage with TypeScript](#usage-with-typescript) 23 | 24 | ## Installation 25 | 26 | Add the plugin to `devDependencies` 27 | 28 | ```bash 29 | npm i --save-dev cypress-fail-fast 30 | ``` 31 | 32 | Now, depending on your Cypress version, use one of the next methods: 33 | 34 | ### Installation on Cypress 10 and higher 35 | 36 | Inside `cypress.config.ts` file: 37 | 38 | ```javascript 39 | import cypressFailFast from "cypress-fail-fast/plugin"; 40 | 41 | export default defineConfig({ 42 | e2e: { 43 | setupNodeEvents(on, config) { 44 | cypressFailFast(on, config); 45 | }, 46 | }, 47 | }); 48 | ``` 49 | 50 | In case you are using JavaScript, you may explicit the file extension in some cases: 51 | 52 | ```javascript 53 | import cypressFailFast from "cypress-fail-fast/plugin.js" 54 | ``` 55 | 56 | Note: This example shows how to install the plugin for `e2e` testing type. Read [Cypress configuration docs](https://docs.cypress.io/guides/references/configuration) for further info. 57 | 58 | At the top of your support file (usually `cypress/support/e2e.js` for `e2e` testing type) 59 | 60 | ```javascript 61 | import "cypress-fail-fast"; 62 | ``` 63 | 64 | ### Installation on Cypress versions lower than 10 65 | 66 | Inside `cypress/plugins/index.js`: 67 | 68 | ```javascript 69 | module.exports = (on, config) => { 70 | require("cypress-fail-fast/plugin")(on, config); 71 | return config; 72 | }; 73 | ``` 74 | 75 | At the top of `cypress/support/index.js`: 76 | 77 | ```javascript 78 | import "cypress-fail-fast"; 79 | ``` 80 | 81 | From now, if one test fail after its last retry, the rest of tests will be skipped: 82 | 83 | ![Cypress results screenshot](docs/assets/cypress-fail-fast-screenshot.png) 84 | 85 | ## Limitations and notes 86 | 87 | * All spec files will be loaded, even after entering "skip mode", but every tests and hooks inside them will be skipped. 88 | * The `spec` strategy does not work in headed mode, because for Cypress events it is like running a single spec, so all remaining tests will be skipped. 89 | 90 | ## Configuration 91 | 92 | ### Environment variables 93 | 94 | * __`FAIL_FAST_STRATEGY`__: `'spec'|'run'|'parallel'` 95 | * If `spec`, only remaining tests in current spec file are skipped. This mode only works in "headless" mode. 96 | * If `run`, all remaining tests in all spec files are skipped (default value). 97 | * Use `parallel` to [provide your own callbacks](#configuration-for-parallel-runs) allowing to notify from one run to the others when remaining tests should be skipped. 98 | * __`FAIL_FAST_ENABLED`__: `boolean = true` Allows disabling the "fail-fast" feature globally, but it could be still enabled for specific tests or describes using [configuration by test](#configuration-by-test). 99 | * __`FAIL_FAST_PLUGIN`__: `boolean = true` If `false`, it disables the "fail-fast" feature totally, ignoring even plugin [configurations by test](#configuration-by-test). 100 | * __`FAIL_FAST_BAIL`__: `Number = 1` Enable the skip mode immediately upon n number of failing test suite. Defaults to 1. 101 | 102 | #### Examples 103 | 104 | ```bash 105 | CYPRESS_FAIL_FAST_PLUGIN=false npm run cypress 106 | ``` 107 | 108 | or set the "env" key in the `cypress.json` configuration file: 109 | 110 | ```json 111 | { 112 | "env": 113 | { 114 | "FAIL_FAST_STRATEGY": "run", 115 | "FAIL_FAST_ENABLED": true, 116 | "FAIL_FAST_BAIL": 2, 117 | } 118 | } 119 | ``` 120 | 121 | ### Configuration by test 122 | 123 | If you want to configure the plugin on a specific test, you can set this by using the `failFast` property in [test configuration](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Test-Configuration). The plugin allows next config values: 124 | 125 | * __`failFast`__: Configuration for the plugin, containing any of next properties: 126 | * __`enabled`__ : Indicates wheter a failure of the current test or children tests _(if configuration is [applied to a suite](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Suite-configuration))_ should produce to skip the rest of tests or not. Note that the value defined in this property has priority over the value of the environment variable `CYPRESS_FAIL_FAST_ENABLED` _(but not over `CYPRESS_FAIL_FAST_PLUGIN`, which disables the plugin totally)_. 127 | 128 | #### Example 129 | 130 | In the next example, tests are configured to "fail-fast" only in case the test with the "sanity test" description fails. If any of the other tests fails, "fail-fast" will not be applied. 131 | 132 | ```js 133 | describe("All tests", { 134 | failFast: { 135 | enabled: false, // Children tests and describes will inherit this configuration 136 | }, 137 | }, () => { 138 | it("sanity test", { 139 | failFast: { 140 | enabled: true, // Overwrite configuration defined in parents 141 | }, 142 | }, () => { 143 | // Will skip the rest of tests if this one fails 144 | expect(true).to.be.true; 145 | }); 146 | 147 | it("second test",() => { 148 | // Will continue executing tests if this one fails 149 | expect(true).to.be.true; 150 | }); 151 | }); 152 | ``` 153 | 154 | ### Configuration examples for usual scenarios 155 | 156 | ##### You want to disable "fail-fast" in all specs except one: 157 | 158 | Set the `FAIL_FAST_ENABLED` key in the `cypress.json` configuration file: 159 | 160 | ```json 161 | { 162 | "env": 163 | { 164 | "FAIL_FAST_ENABLED": false 165 | } 166 | } 167 | ``` 168 | 169 | Enable "fail-fast" in those specs you want using [configurations by test](#configuration-by-test): 170 | 171 | ```js 172 | describe("All tests", { failFast: { enabled: true } }, () => { 173 | // If any test in this describe fails, the rest of tests and specs will be skipped 174 | }); 175 | ``` 176 | 177 | ##### You want to totally disable "fail-fast" in your local environment: 178 | 179 | Set the `FAIL_FAST_PLUGIN` key in your local `cypress.env.json` configuration file: 180 | 181 | ```json 182 | { 183 | "env": 184 | { 185 | "FAIL_FAST_PLUGIN": false 186 | } 187 | } 188 | ``` 189 | 190 | ### Configuration for parallel runs 191 | 192 | The plugin configuration supports defining two callbacks that, used in combination, allow to skip tests in one run when other run starts skipping tests also. Where, or how do you store the "flag" that allows to communicate your runs is in your hands, the plugin does not care about it. 193 | 194 | To implement it, the plugin can receive an object with extra configuration as third argument when it is registered in the `cypress/plugins/index.js` file: 195 | 196 | * __`parallelCallbacks`__: Object containing next properties: 197 | * __`onCancel`__: `function()` This callback is executed on first test failure that produces the plugin starts skipping tests. 198 | * __`isCancelled`__: `function(): boolean` If this callback returns `true`, the plugin skips remaining tests. 199 | 200 | These callbacks are executed only when the environment variable `FAIL_FAST_STRATEGY` is set to `parallel`. 201 | 202 | Here is an example of configuration that would skip tests on many parallel runs when one of them starts skipping tests. It would only work if all parallel runs have access to the folder where the `isCancelled` flag is being stored as a file (easy to achieve if all of your parallel runs are being executed on Docker images on a same machine, for example). _Note that this is only an example, you could also implement it storing the flag in a REST API, etc._ 203 | 204 | ```js 205 | // Example valid for Cypress versions lower than 10. Use config file on Cypress 10 206 | 207 | const fs = require("fs"); 208 | const path = require("path"); 209 | 210 | // Flag file is stored in the /cypress folder 211 | const isCancelledFlagFile = path.resolve(__dirname, "..", ".run-is-cancelled"); 212 | 213 | module.exports = (on, config) => { 214 | require("cypress-fail-fast/plugin")(on, config, { 215 | parallelCallbacks: { 216 | onCancel: () => { 217 | // Create flag file when the plugin starts skipping tests 218 | fs.writeFileSync(isCancelledFlagFile, ""); 219 | }, 220 | isCancelled: () => { 221 | // If any other run has created the file, start skipping tests 222 | return fs.existsSync(isCancelledFlagFile); 223 | }, 224 | }, 225 | }); 226 | 227 | return config; 228 | }; 229 | ``` 230 | 231 | Note that this example requires to remove the created file when all of the runs have finished, or tests will always be skipped whenever any run starts again. So, the `FAIL_FAST_STRATEGY` environment variable should be set to `parallel` only in CI pipelines where the workspace is cleaned on finish, for example. 232 | 233 | ## Usage with TypeScript 234 | 235 | If you are using [TypeScript in the Cypress plugins file][cypress-typescript], this plugin includes TypeScript declarations and can be imported like the following: 236 | 237 | ```ts 238 | import cypressFailFast = require("cypress-fail-fast/plugin"); 239 | 240 | export default (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): Cypress.PluginConfigOptions => { 241 | cypressFailFast(on, config); 242 | return config; 243 | }; 244 | ``` 245 | 246 | Note: The example above is only valid for Cypress versions lower than 10. Use the configuration file in Cypress 10. 247 | 248 | ## Tests 249 | 250 | To ensure the plugin stability, the current major version is being tested with Cypress major versions 9.x, 10.x, 11.x, 12.x and 13.x, and new releases will be published for each new Cypress minor or major releases, updating the E2E tests. 251 | 252 | Minor versions used in the E2E tests can be checked in the `devDependencies` of the `package.json` files of the E2E tests: 253 | * [Cypress v9.x](https://github.com/javierbrea/cypress-fail-fast/blob/main/test-e2e/cypress-variants/cypress-9/package.json) 254 | * [Cypress v10.x](https://github.com/javierbrea/cypress-fail-fast/blob/main/test-e2e/cypress-variants/cypress-10/package.json) 255 | * [Cypress v11.x](https://github.com/javierbrea/cypress-fail-fast/blob/main/test-e2e/cypress-variants/cypress-11/package.json) 256 | * [Cypress v12.x](https://github.com/javierbrea/cypress-fail-fast/blob/main/test-e2e/cypress-variants/cypress-12/package.json) 257 | * [Cypress v13.x](https://github.com/javierbrea/cypress-fail-fast/blob/main/test-e2e/cypress-variants/cypress-13/package.json) 258 | 259 | Even when current major version may work with previous Cypress versions, it is not currently tested, so, to be sure it works you should use: 260 | 261 | * Cypress 8.x may work, but it was tested until `cypress-fail-fast` 7.0.x 262 | * If you need Cypress 7 support, use `cypress-fail-fast` 6.x 263 | * If you need Cypress 6 support, use `cypress-fail-fast` 5.x 264 | * If you need Cypress 5 or lower, use `cypress-fail-fast` <= 4.x 265 | 266 | If you find any issue for a specific Cypress version, please report it at https://github.com/javierbrea/cypress-fail-fast/issues. 267 | 268 | ## Acknowledgements 269 | 270 | This plugin has been developed based on the solutions proposed by the community on this [Cypress issue](https://github.com/cypress-io/cypress/issues/518), so thanks to all! I hope this plugin can be deprecated soon, as soon as the Cypress team adds native support for this feature. 😃 271 | 272 | ## Contributing 273 | 274 | Contributors are welcome. 275 | Please read the [contributing guidelines](.github/CONTRIBUTING.md) and [code of conduct](.github/CODE_OF_CONDUCT.md). 276 | 277 | ## License 278 | 279 | MIT, see [LICENSE](./LICENSE) for details. 280 | 281 | [coveralls-image]: https://coveralls.io/repos/github/javierbrea/cypress-fail-fast/badge.svg 282 | [coveralls-url]: https://coveralls.io/github/javierbrea/cypress-fail-fast 283 | [build-image]: https://github.com/javierbrea/cypress-fail-fast/workflows/build/badge.svg?branch=main 284 | [build-url]: https://github.com/javierbrea/cypress-fail-fast/actions?query=workflow%3Abuild+branch%3Amain 285 | [last-commit-image]: https://img.shields.io/github/last-commit/javierbrea/cypress-fail-fast.svg 286 | [last-commit-url]: https://github.com/javierbrea/cypress-fail-fast/commits 287 | [license-image]: https://img.shields.io/npm/l/cypress-fail-fast.svg 288 | [license-url]: https://github.com/javierbrea/cypress-fail-fast/blob/main/LICENSE 289 | [npm-downloads-image]: https://img.shields.io/npm/dm/cypress-fail-fast.svg 290 | [npm-downloads-url]: https://www.npmjs.com/package/cypress-fail-fast 291 | [quality-gate-image]: https://sonarcloud.io/api/project_badges/measure?project=javierbrea_cypress-fail-fast&metric=alert_status 292 | [quality-gate-url]: https://sonarcloud.io/dashboard?id=javierbrea_cypress-fail-fast 293 | [release-image]: https://img.shields.io/github/release-date/javierbrea/cypress-fail-fast.svg 294 | [release-url]: https://github.com/javierbrea/cypress-fail-fast/releases 295 | 296 | [cypress-typescript]: https://docs.cypress.io/guides/tooling/typescript-support.html 297 | -------------------------------------------------------------------------------- /docs/assets/cypress-fail-fast-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbrea/cypress-fail-fast/0c88df331b03dda28c10e8b34188583ecb88a2c6/docs/assets/cypress-fail-fast-screenshot.png -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface FailFastConfigOptions { 5 | /** 6 | * Disables fail-fast plugin 7 | * If the test fails, the rest of tests won't be skipped 8 | */ 9 | enabled?: boolean 10 | } 11 | 12 | interface TestConfigOverrides { 13 | /** 14 | * Configuration for fail-fast plugin 15 | */ 16 | failFast?: Partial 17 | } 18 | 19 | interface SuiteConfigOverrides { 20 | /** 21 | * Configuration for fail-fast plugin 22 | */ 23 | failFast?: Partial 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global Cypress, cy, beforeEach, afterEach, before */ 2 | 3 | module.exports = require("./src/support")(Cypress, cy, beforeEach, afterEach, before); 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // Automatically clear mock calls and instances between every test 6 | clearMocks: true, 7 | 8 | // Indicates whether the coverage information should be collected while executing the test 9 | collectCoverage: true, 10 | 11 | // An array of glob patterns indicating a set of files for which coverage information should be collected 12 | collectCoverageFrom: ["src/**/*.js"], 13 | 14 | // The directory where Jest should output its coverage files 15 | coverageDirectory: "coverage", 16 | 17 | // An object that configures minimum threshold enforcement for coverage results 18 | coverageThreshold: { 19 | global: { 20 | branches: 100, 21 | functions: 100, 22 | lines: 100, 23 | statements: 100, 24 | }, 25 | }, 26 | 27 | // The test environment that will be used for testing 28 | testEnvironment: "node", 29 | 30 | // The glob patterns Jest uses to detect test files 31 | testMatch: ["/test/**/*.spec.js"], 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-fail-fast", 3 | "version": "7.1.1", 4 | "description": "Skip the rest of Cypress tests on first failure", 5 | "keywords": [ 6 | "cypress", 7 | "cypress-plugin", 8 | "fail-fast", 9 | "failure", 10 | "skip", 11 | "cancel", 12 | "tests", 13 | "testing-tools", 14 | "testing", 15 | "parallel" 16 | ], 17 | "author": "Javier Brea", 18 | "license": "MIT", 19 | "repository": "https://github.com/javierbrea/cypress-fail-fast", 20 | "files": [ 21 | "src", 22 | "index.d.ts", 23 | "plugin.js", 24 | "plugin.d.ts" 25 | ], 26 | "main": "index.js", 27 | "types": "index.d.ts", 28 | "scripts": { 29 | "lint": "eslint src test *.js test-e2e/*.js test-e2e/commands/**/*.js test-e2e/cypress-src/**/*.js test-e2e/test/**/*.js", 30 | "lint-staged": "lint-staged", 31 | "test": "jest", 32 | "test:unit": "npm run test", 33 | "test:e2e:install": "cd test-e2e && npm i", 34 | "test:e2e:run": "cd test-e2e && npm run test:ci", 35 | "test:e2e:run:debug": "cross-env DEBUG=true npm run test:e2e:run", 36 | "test:e2e:debug": "npm run test:e2e:install && npm run test:e2e:run:debug", 37 | "test:e2e:ci": "npm run test:e2e:install && npm run test:e2e:run", 38 | "test:ci": "npm run test && npm run test:mutation && npm run test:e2e:ci", 39 | "test:mutation": "stryker run", 40 | "tsc": "tsc", 41 | "prepare": "is-ci || husky install" 42 | }, 43 | "peerDependencies": { 44 | "cypress": ">=8.0.0" 45 | }, 46 | "dependencies": { 47 | "chalk": "4.1.2" 48 | }, 49 | "devDependencies": { 50 | "@babel/eslint-parser": "7.25.1", 51 | "@stryker-mutator/core": "7.3.0", 52 | "@stryker-mutator/jest-runner": "7.3.0", 53 | "cross-env": "7.0.3", 54 | "cypress": "13.13.2", 55 | "eslint": "8.54.0", 56 | "eslint-config-prettier": "9.1.0", 57 | "eslint-plugin-prettier": "5.2.1", 58 | "eslint-plugin-react": "7.35.0", 59 | "husky": "9.1.4", 60 | "is-ci": "3.0.1", 61 | "jest": "29.7.0", 62 | "lint-staged": "15.2.8", 63 | "prettier": "3.3.3", 64 | "sinon": "18.0.0", 65 | "typescript": "5.5.4" 66 | }, 67 | "lint-staged": { 68 | "*.js": "eslint", 69 | "src/**/*.js": "eslint", 70 | "test/**/*.js": "eslint", 71 | "test-e2e/*.js": "eslint", 72 | "test-e2e/commands/**/*.js": "eslint", 73 | "test-e2e/cypress-src/**/*.js": "eslint", 74 | "test-e2e/test/**/*.js": "eslint" 75 | }, 76 | "engines": { 77 | "node": ">=14.0.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /plugin.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface FailFastParallelCallbacks { 4 | /** 5 | * onCancel callback. 6 | * Callback that will be executed on first test failure producing that cypress-fail-fast starts skipping tests. 7 | */ 8 | onCancel(): void 9 | 10 | /** 11 | * isCancelled callback. 12 | * If this callback returns true, cypress-fail-fast will start skipping tests. 13 | * @returns boolean. true if remaining tests should be skipped 14 | */ 15 | isCancelled(): boolean 16 | } 17 | 18 | interface FailFastPluginConfigOptions { 19 | /** 20 | * Parallel callbacks. 21 | * Callbacks to be executed when strategy is "parallel". 22 | */ 23 | parallelCallbacks?: FailFastParallelCallbacks 24 | } 25 | 26 | /** 27 | * Installs cypress-fail-fast plugin 28 | * @example failFastPlugin(on, config, {}); 29 | * @param on Cypress plugin events 30 | * @param config Cypress plugin config options 31 | * @param failFastConfig cypress-fail-fast plugin config options 32 | * @returns Cypress plugin config options 33 | */ 34 | declare function _exports(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions, failFastConfig?: FailFastPluginConfigOptions): Cypress.PluginConfigOptions 35 | export = _exports; 36 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./src/plugin"); 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>javierbrea/renovate-config" 4 | ], 5 | "packageRules": [ 6 | { 7 | "matchPaths": ["test-e2e/cypress-variants/cypress-9/package.json"], 8 | "matchPackageNames": ["cypress"], 9 | "allowedVersions": "9.x" 10 | }, 11 | { 12 | "matchPaths": ["test-e2e/cypress-variants/cypress-10/package.json"], 13 | "matchPackageNames": ["cypress"], 14 | "allowedVersions": "10.x" 15 | }, 16 | { 17 | "matchPaths": ["test-e2e/cypress-variants/cypress-11/package.json"], 18 | "matchPackageNames": ["cypress"], 19 | "allowedVersions": "11.x" 20 | }, 21 | { 22 | "matchPaths": ["test-e2e/cypress-variants/cypress-12/package.json"], 23 | "matchPackageNames": ["cypress"], 24 | "allowedVersions": "12.x" 25 | }, 26 | { 27 | "matchPaths": ["test-e2e/cypress-variants/cypress-13/package.json"], 28 | "matchPackageNames": ["cypress"], 29 | "allowedVersions": "13.x" 30 | }, 31 | { 32 | "matchPaths": ["package.json"], 33 | "matchPackageNames": ["chalk"], 34 | "allowedVersions": "<5.0.0" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=javierbrea 2 | sonar.projectKey=javierbrea_cypress-fail-fast 3 | sonar.projectVersion=7.1.1 4 | 5 | sonar.javascript.file.suffixes=.js 6 | sonar.sourceEncoding=UTF-8 7 | sonar.exclusions=node_modules/** 8 | sonar.coverage.exclusions=test/**/*,index.js,plugin.js,stryker.conf.js,jest.config.js,test-e2e/**/* 9 | sonar.cpd.exclusions=test/**/*,test-e2e/**/* 10 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 11 | sonar.host.url=https://sonarcloud.io 12 | -------------------------------------------------------------------------------- /src/helpers/config.js: -------------------------------------------------------------------------------- 1 | const { 2 | ENVIRONMENT_DEFAULT_VALUES, 3 | PLUGIN_ENVIRONMENT_VAR, 4 | ENABLED_ENVIRONMENT_VAR, 5 | STRATEGY_ENVIRONMENT_VAR, 6 | BAIL_ENVIRONMENT_VAR, 7 | } = require("./constants"); 8 | 9 | const { getTestConfig } = require("./cypress"); 10 | 11 | const STRATEGIES = ["spec", "parallel"]; 12 | 13 | const TRUTHY_VALUES = [true, "true", 1, "1"]; 14 | const FALSY_VALUES = [false, "false", 0, "0"]; 15 | const UNDEFINED_VALUES = ["", undefined]; 16 | 17 | function valueIsOneOf(value, arrayOfValues) { 18 | return arrayOfValues.includes(value); 19 | } 20 | 21 | function strategyIsParallel(value) { 22 | return value === STRATEGIES[1]; 23 | } 24 | 25 | function strategyIsSpec(value) { 26 | return value === STRATEGIES[0]; 27 | } 28 | 29 | function isTruthy(value) { 30 | return valueIsOneOf(value, TRUTHY_VALUES); 31 | } 32 | 33 | function isFalsy(value) { 34 | return valueIsOneOf(value, FALSY_VALUES); 35 | } 36 | 37 | function isDefined(value) { 38 | return !valueIsOneOf(value, UNDEFINED_VALUES); 39 | } 40 | 41 | function booleanVarValue(value, defaultValue) { 42 | const isTruthyValue = isTruthy(value); 43 | if (!isTruthyValue && !isFalsy(value)) { 44 | return defaultValue; 45 | } 46 | return isTruthyValue; 47 | } 48 | 49 | function numericVarValue(value, defaultValue) { 50 | if (isDefined(value)) { 51 | return Number(value); 52 | } 53 | return defaultValue; 54 | } 55 | 56 | function getFailFastEnvironmentConfig(Cypress) { 57 | return { 58 | plugin: booleanVarValue( 59 | Cypress.env(PLUGIN_ENVIRONMENT_VAR), 60 | ENVIRONMENT_DEFAULT_VALUES[PLUGIN_ENVIRONMENT_VAR], 61 | ), 62 | enabled: booleanVarValue( 63 | Cypress.env(ENABLED_ENVIRONMENT_VAR), 64 | ENVIRONMENT_DEFAULT_VALUES[ENABLED_ENVIRONMENT_VAR], 65 | ), 66 | strategyIsSpec: strategyIsSpec(Cypress.env(STRATEGY_ENVIRONMENT_VAR)), 67 | bail: numericVarValue( 68 | Cypress.env(BAIL_ENVIRONMENT_VAR), 69 | ENVIRONMENT_DEFAULT_VALUES[BAIL_ENVIRONMENT_VAR], 70 | ), 71 | }; 72 | } 73 | 74 | function getTestFailFastConfig(currentTest, Cypress) { 75 | const testConfig = getTestConfig(currentTest); 76 | if (testConfig.failFast) { 77 | return { 78 | ...getFailFastEnvironmentConfig(Cypress), 79 | ...testConfig.failFast, 80 | }; 81 | } 82 | if (currentTest.parent) { 83 | return getTestFailFastConfig(currentTest.parent, Cypress); 84 | } 85 | return getFailFastEnvironmentConfig(Cypress); 86 | } 87 | 88 | function currentStrategyIsSpec(Cypress) { 89 | return getFailFastEnvironmentConfig(Cypress).strategyIsSpec; 90 | } 91 | 92 | function pluginIsEnabled(Cypress) { 93 | return getFailFastEnvironmentConfig(Cypress).plugin; 94 | } 95 | 96 | function bailConfig(Cypress) { 97 | return getFailFastEnvironmentConfig(Cypress).bail; 98 | } 99 | 100 | function failFastIsEnabled(currentTest, Cypress) { 101 | return getTestFailFastConfig(currentTest, Cypress).enabled; 102 | } 103 | 104 | module.exports = { 105 | isTruthy, 106 | isFalsy, 107 | strategyIsParallel, 108 | bailConfig, 109 | pluginIsEnabled, 110 | failFastIsEnabled, 111 | currentStrategyIsSpec, 112 | }; 113 | -------------------------------------------------------------------------------- /src/helpers/constants.js: -------------------------------------------------------------------------------- 1 | const PLUGIN_ENVIRONMENT_VAR = "FAIL_FAST_PLUGIN"; 2 | const ENABLED_ENVIRONMENT_VAR = "FAIL_FAST_ENABLED"; 3 | const STRATEGY_ENVIRONMENT_VAR = "FAIL_FAST_STRATEGY"; 4 | const BAIL_ENVIRONMENT_VAR = "FAIL_FAST_BAIL"; 5 | 6 | const SHOULD_SKIP_TASK = "failFastShouldSkip"; 7 | const RESET_SKIP_TASK = "failFastResetSkip"; 8 | const LOG_TASK = "failFastLog"; 9 | const FAILED_TESTS_TASK = "failFastFailedTests"; 10 | const RESET_FAILED_TESTS_TASK = "failFastResetFailedTests"; 11 | 12 | const STOP_MESSAGE = "Stopping Cypress runner due to a previous failure"; 13 | const SKIP_MESSAGE = "Enabling skip mode"; 14 | const FAILED_TEST_MESSAGE = "Failed tests"; 15 | const LOG_PREFIX = "[fail-fast]"; 16 | 17 | const ENVIRONMENT_DEFAULT_VALUES = { 18 | [PLUGIN_ENVIRONMENT_VAR]: true, 19 | [ENABLED_ENVIRONMENT_VAR]: true, 20 | [BAIL_ENVIRONMENT_VAR]: 1, 21 | }; 22 | 23 | module.exports = { 24 | ENVIRONMENT_DEFAULT_VALUES, 25 | PLUGIN_ENVIRONMENT_VAR, 26 | ENABLED_ENVIRONMENT_VAR, 27 | STRATEGY_ENVIRONMENT_VAR, 28 | BAIL_ENVIRONMENT_VAR, 29 | SHOULD_SKIP_TASK, 30 | RESET_SKIP_TASK, 31 | FAILED_TESTS_TASK, 32 | RESET_FAILED_TESTS_TASK, 33 | LOG_TASK, 34 | STOP_MESSAGE, 35 | SKIP_MESSAGE, 36 | FAILED_TEST_MESSAGE, 37 | LOG_PREFIX, 38 | }; 39 | -------------------------------------------------------------------------------- /src/helpers/cypress.js: -------------------------------------------------------------------------------- 1 | let hookFailedError = null; 2 | let forceErrorOnFailedHook = true; 3 | 4 | function isHeaded(Cypress) { 5 | return Cypress.browser && Cypress.browser.isHeaded; 6 | } 7 | 8 | function testHasFailed(currentTest) { 9 | return currentTest.state === "failed" && currentTest.currentRetry() === currentTest.retries(); 10 | } 11 | 12 | function shouldForceErrorOnFailedHook() { 13 | return forceErrorOnFailedHook; 14 | } 15 | 16 | function setForceErrorOnFailedHook(value) { 17 | forceErrorOnFailedHook = value; 18 | } 19 | 20 | function setHookFailedError(error) { 21 | hookFailedError = error; 22 | } 23 | 24 | function getHookFailedError() { 25 | return hookFailedError; 26 | } 27 | 28 | function wrapCypressRunner(Cypress) { 29 | let hookFailed, hookFailedName, hookError; 30 | const _onRunnableRun = Cypress.runner.onRunnableRun; 31 | Cypress.runner.onRunnableRun = function (runnableRun, runnable, args) { 32 | const isHook = runnable.type === "hook"; 33 | const isBeforeHook = isHook && /before/.test(runnable.hookName); 34 | 35 | const next = args[0]; 36 | 37 | const setFailedFlag = function (error) { 38 | if (error) { 39 | hookFailedName = runnable.hookName; 40 | hookError = error; 41 | hookFailed = true; 42 | } 43 | /* 44 | Do not pass the error, because Cypress stops if there is an error on before hooks, 45 | so this plugin can't set the skip flag 46 | */ 47 | return next.call(this /*, error*/); 48 | }; 49 | 50 | const forceTestToFail = function () { 51 | hookFailed = false; 52 | hookError.message = `"${hookFailedName}" hook failed: ${hookError.message}`; 53 | 54 | // NOTE: In Cypress 13, passing the error does not produce the test to fail, so, we also set a global variable to force the afterEach hook to fail after the test 55 | setHookFailedError(hookError); 56 | return next.call(this, hookError); 57 | }; 58 | 59 | if (isBeforeHook && hookFailed) { 60 | // Skip other before hooks when one failed 61 | return next(); 62 | } else if (!isHook && hookFailed) { 63 | args[0] = forceTestToFail; 64 | } else if (isBeforeHook) { 65 | args[0] = setFailedFlag; 66 | } 67 | 68 | return _onRunnableRun.apply(this, [runnableRun, runnable, args]); 69 | }; 70 | } 71 | 72 | function getTestConfig(test) { 73 | // Cypress <6.7 74 | if (test.cfg) { 75 | return test.cfg; 76 | } 77 | // Cypress >9 78 | if ( 79 | test.ctx && 80 | test.ctx.test && 81 | test.ctx.test._testConfig && 82 | test.ctx.test._testConfig.testConfigList && 83 | test.ctx.test._testConfig.testConfigList[ 84 | test.ctx.test._testConfig.testConfigList.length - 1 85 | ] && 86 | test.ctx.test._testConfig.testConfigList[test.ctx.test._testConfig.testConfigList.length - 1] 87 | .overrides 88 | ) { 89 | return test.ctx.test._testConfig.testConfigList[ 90 | test.ctx.test._testConfig.testConfigList.length - 1 91 | ].overrides; 92 | } 93 | // Cypress >6.7 94 | if (test.ctx && test.ctx.test && test.ctx.test._testConfig) { 95 | return test.ctx.test._testConfig; 96 | } 97 | return {}; 98 | } 99 | 100 | function stopRunner(Cypress) { 101 | Cypress.runner.stop(); 102 | } 103 | 104 | module.exports = { 105 | isHeaded, 106 | testHasFailed, 107 | wrapCypressRunner, 108 | getTestConfig, 109 | stopRunner, 110 | setHookFailedError, 111 | getHookFailedError, 112 | shouldForceErrorOnFailedHook, 113 | setForceErrorOnFailedHook, 114 | }; 115 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const { 3 | SHOULD_SKIP_TASK, 4 | RESET_SKIP_TASK, 5 | FAILED_TESTS_TASK, 6 | RESET_FAILED_TESTS_TASK, 7 | LOG_TASK, 8 | STRATEGY_ENVIRONMENT_VAR, 9 | LOG_PREFIX, 10 | } = require("./helpers/constants"); 11 | 12 | const { strategyIsParallel } = require("./helpers/config"); 13 | 14 | module.exports = (on, config, pluginConfig = {}) => { 15 | // store skip flag 16 | let shouldSkipFlag = false; 17 | let failedTests = 0; 18 | 19 | const parallelCallbacks = 20 | strategyIsParallel(config.env[STRATEGY_ENVIRONMENT_VAR]) && !!pluginConfig.parallelCallbacks 21 | ? pluginConfig.parallelCallbacks 22 | : {}; 23 | const isCancelledCallback = parallelCallbacks.isCancelled; 24 | const onCancelCallback = parallelCallbacks.onCancel; 25 | 26 | const shouldSkip = () => { 27 | if (shouldSkipFlag) { 28 | return shouldSkipFlag; 29 | } 30 | if (isCancelledCallback) { 31 | shouldSkipFlag = isCancelledCallback() || false; 32 | } 33 | return shouldSkipFlag; 34 | }; 35 | 36 | // Expose fail fast tasks 37 | on("task", { 38 | [RESET_SKIP_TASK]: function () { 39 | shouldSkipFlag = false; 40 | return null; 41 | }, 42 | [SHOULD_SKIP_TASK]: function (value) { 43 | if (value === true) { 44 | if (onCancelCallback) { 45 | onCancelCallback(); 46 | } 47 | shouldSkipFlag = value; 48 | } 49 | return shouldSkip(); 50 | }, 51 | [FAILED_TESTS_TASK]: function (value) { 52 | if (value === true) { 53 | failedTests++; 54 | } 55 | return failedTests; 56 | }, 57 | [RESET_FAILED_TESTS_TASK]: function () { 58 | failedTests = 0; 59 | return null; 60 | }, 61 | [LOG_TASK]: function (message) { 62 | console.log(`${chalk.yellow(LOG_PREFIX)} ${message}`); 63 | return null; 64 | }, 65 | }); 66 | 67 | return config; 68 | }; 69 | -------------------------------------------------------------------------------- /src/support.js: -------------------------------------------------------------------------------- 1 | const { 2 | SHOULD_SKIP_TASK, 3 | RESET_SKIP_TASK, 4 | FAILED_TESTS_TASK, 5 | RESET_FAILED_TESTS_TASK, 6 | LOG_TASK, 7 | STOP_MESSAGE, 8 | SKIP_MESSAGE, 9 | FAILED_TEST_MESSAGE, 10 | } = require("./helpers/constants"); 11 | 12 | const { 13 | bailConfig, 14 | pluginIsEnabled, 15 | failFastIsEnabled, 16 | currentStrategyIsSpec, 17 | } = require("./helpers/config"); 18 | 19 | const { 20 | isHeaded, 21 | testHasFailed, 22 | wrapCypressRunner, 23 | stopRunner, 24 | getHookFailedError, 25 | setHookFailedError, 26 | shouldForceErrorOnFailedHook, 27 | } = require("./helpers/cypress"); 28 | 29 | function support(Cypress, cy, beforeEach, afterEach, before) { 30 | function stopCypressRunner() { 31 | cy.task(LOG_TASK, STOP_MESSAGE); 32 | stopRunner(Cypress); 33 | } 34 | 35 | function resetSkipFlag() { 36 | cy.task(RESET_SKIP_TASK, null, { log: false }); 37 | } 38 | 39 | function resetFailedTests() { 40 | cy.task(RESET_FAILED_TESTS_TASK, null, { log: false }); 41 | } 42 | 43 | function enableSkipMode() { 44 | cy.task(LOG_TASK, SKIP_MESSAGE); 45 | return cy.task(SHOULD_SKIP_TASK, true); 46 | } 47 | 48 | function registerFailureAndRunIfBailLimitIsReached(callback) { 49 | cy.task(FAILED_TESTS_TASK, true, { log: false }).then((value) => { 50 | const bail = bailConfig(Cypress); 51 | cy.task(LOG_TASK, `${FAILED_TEST_MESSAGE}: ${value}/${bail}`); 52 | if (value >= bail) { 53 | callback(); 54 | } 55 | }); 56 | } 57 | 58 | function runIfSkipIsEnabled(callback) { 59 | cy.task(SHOULD_SKIP_TASK, null, { log: false }).then((value) => { 60 | if (value === true) { 61 | callback(); 62 | } 63 | }); 64 | } 65 | 66 | beforeEach(function () { 67 | if (pluginIsEnabled(Cypress)) { 68 | runIfSkipIsEnabled(() => { 69 | this.currentTest.pending = true; 70 | stopCypressRunner(); 71 | }); 72 | } 73 | }); 74 | 75 | afterEach(function () { 76 | // Mark skip flag as true if test failed 77 | const currentTest = this.currentTest; 78 | if ( 79 | currentTest && 80 | pluginIsEnabled(Cypress) && 81 | (testHasFailed(currentTest) || getHookFailedError()) && 82 | failFastIsEnabled(currentTest, Cypress) 83 | ) { 84 | registerFailureAndRunIfBailLimitIsReached(() => { 85 | enableSkipMode().then(() => { 86 | if (getHookFailedError()) { 87 | const error = getHookFailedError(); 88 | setHookFailedError(null); 89 | // istanbul ignore next 90 | if (!testHasFailed(currentTest) && shouldForceErrorOnFailedHook()) { 91 | // NOTE: Force the afterEach hook to fail after the test in case the test was not marked as failed. This happens in Cypress 13. 92 | // istanbul ignore next 93 | throw error; 94 | } 95 | } 96 | }); 97 | }); 98 | } 99 | }); 100 | 101 | before(function () { 102 | if (pluginIsEnabled(Cypress)) { 103 | if (isHeaded(Cypress) || currentStrategyIsSpec(Cypress)) { 104 | /* 105 | Reset the shouldSkip flag at the start of a run, so that it doesn't carry over into subsequent runs. Do this only for headed runs because in headless runs, the `before` hook is executed for each spec file. 106 | */ 107 | resetSkipFlag(); 108 | resetFailedTests(); 109 | } else { 110 | runIfSkipIsEnabled(() => { 111 | stopCypressRunner(); 112 | }); 113 | } 114 | } 115 | }); 116 | 117 | if (pluginIsEnabled(Cypress)) { 118 | wrapCypressRunner(Cypress); 119 | } 120 | } 121 | 122 | module.exports = support; 123 | -------------------------------------------------------------------------------- /stryker.conf.js: -------------------------------------------------------------------------------- 1 | const BRANCH_NAME = process.env.TRAVIS_CURRENT_BRANCH || process.env.BRANCH_NAME; 2 | const STRYKER_DASHBOARD_API_KEY = process.env.STRYKER_DASHBOARD_API_KEY; 3 | 4 | const BASE_CONFIG = { 5 | ignorePatterns: ["**", "!*.js", "!src/**/*.js", "!test/**/*.js"], 6 | packageManager: "npm", 7 | thresholds: { 8 | high: 80, 9 | low: 60, 10 | break: 80, 11 | }, 12 | reporters: ["html", "clear-text", "progress", "dashboard"], 13 | testRunner: "jest", 14 | coverageAnalysis: "off", 15 | }; 16 | 17 | const config = { 18 | ...BASE_CONFIG, 19 | dashboard: 20 | BRANCH_NAME && STRYKER_DASHBOARD_API_KEY 21 | ? { 22 | project: "github.com/javierbrea/cypress-fail-fast", 23 | version: BRANCH_NAME, 24 | } 25 | : undefined, 26 | }; 27 | 28 | module.exports = config; 29 | -------------------------------------------------------------------------------- /test-e2e/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | #environment variables 4 | #.env 5 | 6 | # dependencies 7 | /node_modules 8 | 9 | # misc 10 | .DS_Store 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # ides 21 | .idea 22 | .vs 23 | -------------------------------------------------------------------------------- /test-e2e/commands/build.js: -------------------------------------------------------------------------------- 1 | const { copyPluginToCypressSupport, copyCypressSources, copyScripts } = require("./support/copy"); 2 | 3 | const variants = require("./support/variants"); 4 | 5 | variants.forEach((variant) => { 6 | copyScripts(variant.path); 7 | copyCypressSources(variant); 8 | if (variant.typescript) { 9 | copyPluginToCypressSupport(variant.path); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /test-e2e/commands/support/copy.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fsExtra = require("fs-extra"); 3 | 4 | const CYPRESS_PATH = "cypress"; 5 | const CYPRESS_PLUGINS_PATH = "plugins"; 6 | const CYPRESS_CUSTOM_PLUGINS_PATH = "plugins-custom"; 7 | const CYPRESS_SUPPORT_PATH = "support"; 8 | const CYPRESS_INTEGRATION_PATH = "integration"; 9 | const TESTS_PATH = path.resolve(__dirname, "..", ".."); 10 | const ROOT_LIB_PATH = path.resolve(TESTS_PATH, ".."); 11 | const VARIANTS_PATH = path.resolve(TESTS_PATH, "cypress-variants"); 12 | const CYPRESS_SRC_PATH = path.resolve(TESTS_PATH, "cypress-src"); 13 | 14 | const toTypeScriptName = (fileName, hasToBeRenamed) => { 15 | return hasToBeRenamed ? fileName.replace(".js", ".ts") : fileName; 16 | }; 17 | 18 | const variantPaths = (variant) => { 19 | const rootPath = path.resolve(VARIANTS_PATH, variant); 20 | return { 21 | root: rootPath, 22 | cypress: { 23 | integration: path.resolve(rootPath, CYPRESS_PATH, CYPRESS_INTEGRATION_PATH), 24 | support: path.resolve(rootPath, CYPRESS_PATH, CYPRESS_SUPPORT_PATH), 25 | plugins: path.resolve(rootPath, CYPRESS_PATH, CYPRESS_PLUGINS_PATH), 26 | }, 27 | }; 28 | }; 29 | 30 | const copyScripts = (variant) => { 31 | const SCRIPTS_FOLDER = "scripts"; 32 | const destPaths = variantPaths(variant); 33 | const scriptsDestFolder = path.resolve(destPaths.root, SCRIPTS_FOLDER); 34 | const scriptsOriginFolder = path.resolve(CYPRESS_SRC_PATH, SCRIPTS_FOLDER); 35 | 36 | fsExtra.removeSync(scriptsDestFolder); 37 | fsExtra.copySync(scriptsOriginFolder, scriptsDestFolder); 38 | }; 39 | 40 | const copyPluginToCypressSupport = (variant) => { 41 | const PLUGIN_DEST_FOLDER = "cypress-fail-fast"; 42 | const SRC_FOLDER = "src"; 43 | const INDEX_FILE = "index.js"; 44 | const INDEX_TYPE_FILE = "index.d.ts"; 45 | const PLUGIN_FILE = "plugin.js"; 46 | const PLUGIN_TYPE_FILE = "plugin.d.ts"; 47 | const ESLINT_FILE = ".eslintrc.json"; 48 | 49 | const libPath = path.resolve(ROOT_LIB_PATH, SRC_FOLDER); 50 | const indexFile = path.resolve(ROOT_LIB_PATH, INDEX_FILE); 51 | const indexTypeFile = path.resolve(ROOT_LIB_PATH, INDEX_TYPE_FILE); 52 | const pluginFile = path.resolve(ROOT_LIB_PATH, PLUGIN_FILE); 53 | const pluginTypeFile = path.resolve(ROOT_LIB_PATH, PLUGIN_TYPE_FILE); 54 | const eslintFile = path.resolve(ROOT_LIB_PATH, ESLINT_FILE); 55 | 56 | const destPaths = variantPaths(variant); 57 | const pluginDestFolder = path.resolve(destPaths.cypress.support, PLUGIN_DEST_FOLDER); 58 | 59 | fsExtra.removeSync(pluginDestFolder); 60 | fsExtra.ensureDirSync(pluginDestFolder); 61 | fsExtra.copySync(libPath, path.resolve(pluginDestFolder, SRC_FOLDER)); 62 | fsExtra.copySync(indexFile, path.resolve(pluginDestFolder, INDEX_FILE)); 63 | fsExtra.copySync(indexTypeFile, path.resolve(pluginDestFolder, INDEX_TYPE_FILE)); 64 | fsExtra.copySync(pluginFile, path.resolve(pluginDestFolder, PLUGIN_FILE)); 65 | fsExtra.copySync(pluginTypeFile, path.resolve(pluginDestFolder, PLUGIN_TYPE_FILE)); 66 | fsExtra.copySync(eslintFile, path.resolve(pluginDestFolder, ESLINT_FILE)); 67 | }; 68 | 69 | const copyCypressPluginFile = (variant, typescript = false, customPluginFolder = null) => { 70 | const destPaths = variantPaths(variant); 71 | const INDEX_FILE = toTypeScriptName("index.js", typescript); 72 | 73 | const pluginsPath = path.resolve(CYPRESS_SRC_PATH, CYPRESS_PLUGINS_PATH); 74 | const customPluginsPath = path.resolve(CYPRESS_SRC_PATH, CYPRESS_CUSTOM_PLUGINS_PATH); 75 | const pluginFile = customPluginFolder 76 | ? path.resolve(customPluginsPath, customPluginFolder, INDEX_FILE) 77 | : path.resolve(pluginsPath, INDEX_FILE); 78 | 79 | fsExtra.removeSync(destPaths.cypress.plugins); 80 | fsExtra.ensureDirSync(destPaths.cypress.plugins); 81 | fsExtra.copySync(pluginFile, path.resolve(destPaths.cypress.plugins, INDEX_FILE)); 82 | }; 83 | 84 | const copyCypressConfigFile = (variantPath, configFileName, destConfigFileName) => { 85 | const destPaths = variantPaths(variantPath); 86 | const pluginFile = path.resolve(CYPRESS_SRC_PATH, configFileName); 87 | fsExtra.copySync(pluginFile, path.resolve(destPaths.root, destConfigFileName || configFileName)); 88 | }; 89 | 90 | const removeCypressConfigFile = (variantPath, configFileName) => { 91 | const destPaths = variantPaths(variantPath); 92 | fsExtra.removeSync(path.resolve(destPaths.root, configFileName)); 93 | }; 94 | 95 | const copyCypressSources = (variant) => { 96 | const destPaths = variantPaths(variant.path); 97 | const BABEL_CONFIG_FILE = "babel.config.js"; 98 | const CYPRESS_CONFIG_FILE = "cypress.json"; 99 | const INDEX_FILE = toTypeScriptName("index.js", variant.typescript); 100 | 101 | const supportPath = path.resolve(CYPRESS_SRC_PATH, CYPRESS_SUPPORT_PATH); 102 | 103 | const cypressConfigFile = path.resolve( 104 | CYPRESS_SRC_PATH, 105 | variant.configFile || CYPRESS_CONFIG_FILE, 106 | ); 107 | const babelConfigFile = path.resolve(CYPRESS_SRC_PATH, BABEL_CONFIG_FILE); 108 | const supportFile = path.resolve(supportPath, variant.supportFile || INDEX_FILE); 109 | 110 | fsExtra.removeSync(destPaths.cypress.plugins); 111 | if (variant.copyPlugin) { 112 | fsExtra.ensureDirSync(destPaths.cypress.plugins); 113 | } 114 | 115 | fsExtra.removeSync(destPaths.cypress.support); 116 | fsExtra.ensureDirSync(destPaths.cypress.support); 117 | fsExtra.copySync( 118 | supportFile, 119 | path.resolve(destPaths.cypress.support, variant.supportFile || INDEX_FILE), 120 | ); 121 | 122 | fsExtra.copySync( 123 | cypressConfigFile, 124 | path.resolve(destPaths.root, variant.configFile || CYPRESS_CONFIG_FILE), 125 | ); 126 | 127 | if (!variant.typescript) { 128 | fsExtra.copySync(babelConfigFile, path.resolve(destPaths.root, BABEL_CONFIG_FILE)); 129 | } 130 | 131 | if (variant.copyPlugin) { 132 | copyCypressPluginFile(variant.path, variant.typescript); 133 | } 134 | }; 135 | 136 | const copyCypressSpecs = (specsFolder, variant) => { 137 | const destPaths = variantPaths(variant.path); 138 | const INTEGRATION_A_FILE = "a-file.js"; 139 | const INTEGRATION_B_FILE = "b-file.js"; 140 | const INTEGRATION_C_FILE = "c-file.js"; 141 | const INTEGRATION_D_FILE = "d-file.js"; 142 | 143 | const integrationPath = path.resolve(CYPRESS_SRC_PATH, CYPRESS_INTEGRATION_PATH, specsFolder); 144 | 145 | const integrationAFile = path.resolve(integrationPath, INTEGRATION_A_FILE); 146 | const integrationBFile = path.resolve(integrationPath, INTEGRATION_B_FILE); 147 | const integrationCFile = path.resolve(integrationPath, INTEGRATION_C_FILE); 148 | const integrationDFile = path.resolve(integrationPath, INTEGRATION_D_FILE); 149 | 150 | fsExtra.removeSync(destPaths.cypress.integration); 151 | fsExtra.ensureDirSync(destPaths.cypress.integration); 152 | fsExtra.copySync( 153 | integrationAFile, 154 | path.resolve( 155 | destPaths.cypress.integration, 156 | toTypeScriptName(INTEGRATION_A_FILE, variant.typescript), 157 | ), 158 | ); 159 | fsExtra.copySync( 160 | integrationBFile, 161 | path.resolve( 162 | destPaths.cypress.integration, 163 | toTypeScriptName(INTEGRATION_B_FILE, variant.typescript), 164 | ), 165 | ); 166 | fsExtra.copySync( 167 | integrationCFile, 168 | path.resolve( 169 | destPaths.cypress.integration, 170 | toTypeScriptName(INTEGRATION_C_FILE, variant.typescript), 171 | ), 172 | ); 173 | if (fsExtra.existsSync(integrationDFile)) { 174 | fsExtra.copySync( 175 | integrationDFile, 176 | path.resolve( 177 | destPaths.cypress.integration, 178 | toTypeScriptName(INTEGRATION_D_FILE, variant.typescript), 179 | ), 180 | ); 181 | } 182 | }; 183 | 184 | module.exports = { 185 | copyPluginToCypressSupport, 186 | copyCypressSources, 187 | copyCypressSpecs, 188 | copyCypressConfigFile, 189 | copyCypressPluginFile, 190 | copyScripts, 191 | removeCypressConfigFile, 192 | }; 193 | -------------------------------------------------------------------------------- /test-e2e/commands/support/variants.js: -------------------------------------------------------------------------------- 1 | const VARIANTS = [ 2 | { 3 | name: "Cypress 9", 4 | path: "cypress-9", 5 | version: "9", 6 | typescript: false, 7 | skippable: true, 8 | pluginFile: "preprocessor-babel-config", 9 | copyPlugin: true, 10 | }, 11 | { 12 | name: "Cypress 10", 13 | path: "cypress-10", 14 | version: "10", 15 | isLatest: false, 16 | typescript: false, 17 | skippable: true, 18 | configFile: "cypress.config.js", 19 | supportFile: "e2e.js", 20 | copyPlugin: false, 21 | }, 22 | { 23 | name: "Cypress 11", 24 | path: "cypress-11", 25 | version: "11", 26 | isLatest: false, 27 | typescript: false, 28 | skippable: true, 29 | configFile: "cypress.config.js", 30 | supportFile: "e2e.js", 31 | copyPlugin: false, 32 | }, 33 | { 34 | name: "Cypress 12", 35 | path: "cypress-12", 36 | version: "12", 37 | isLatest: false, 38 | typescript: false, 39 | skippable: false, 40 | configFile: "cypress.config.js", 41 | supportFile: "e2e.js", 42 | copyPlugin: false, 43 | }, 44 | { 45 | name: "Cypress 13", 46 | path: "cypress-13", 47 | version: "13", 48 | isLatest: true, 49 | typescript: false, 50 | skippable: false, 51 | configFile: "cypress.config.js", 52 | supportFile: "e2e.js", 53 | copyPlugin: false, 54 | }, 55 | { 56 | name: "TypeScript", 57 | path: "typescript", 58 | version: "ts", 59 | skippable: true, 60 | configFile: "cypress.config.ts", 61 | supportFile: "e2e.ts", 62 | typescript: true, 63 | copyPlugin: false, 64 | }, 65 | ]; 66 | 67 | module.exports = VARIANTS; 68 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "sourceType": "module" 8 | }, 9 | "globals": { 10 | "cy": true, 11 | "afterEach": true, 12 | "after": true, 13 | "before": true, 14 | "beforeEach": true, 15 | "describe": true, 16 | "expect": true, 17 | "it": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | [ 4 | "module-resolver", 5 | { 6 | root: ["."], 7 | alias: { 8 | "cypress-fail-fast": `../../../`, 9 | }, 10 | }, 11 | ], 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/cypress.config.js: -------------------------------------------------------------------------------- 1 | const webpackPreprocessor = require("@cypress/webpack-preprocessor"); 2 | const defaults = webpackPreprocessor.defaultOptions; 3 | 4 | module.exports = { 5 | e2e: { 6 | baseUrl: "http://localhost:3000", 7 | setupNodeEvents(on, config) { 8 | require("../../../plugin")(on, config); 9 | delete defaults.webpackOptions.module.rules[0].use[0].options.presets; 10 | on("file:preprocessor", webpackPreprocessor(defaults)); 11 | // Add log task 12 | on("task", { 13 | log: function (message) { 14 | console.log(message); 15 | return null; 16 | }, 17 | }); 18 | return config; 19 | }, 20 | specPattern: "cypress/integration/**/*.js", 21 | }, 22 | video: false, 23 | reporter: "mochawesome", 24 | reporterOptions: { 25 | reportDir: "cypress/results", 26 | overwrite: false, 27 | html: false, 28 | json: true, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import plugin = require("../../../plugin"); 2 | 3 | export default { 4 | e2e: { 5 | baseUrl: "http://localhost:3000", 6 | setupNodeEvents(on, config) { 7 | plugin(on, config); 8 | // Add log task 9 | on("task", { 10 | log: function (message) { 11 | console.log(message); 12 | return null; 13 | }, 14 | }); 15 | return config; 16 | }, 17 | specPattern: "cypress/integration/**/*.ts", 18 | }, 19 | video: false, 20 | reporter: "mochawesome", 21 | reporterOptions: { 22 | reportDir: "cypress/results", 23 | overwrite: false, 24 | html: false, 25 | json: true, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "video": false, 4 | "reporter": "mochawesome", 5 | "reporterOptions": { 6 | "reportDir": "cypress/results", 7 | "overwrite": false, 8 | "html": false, 9 | "json": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/all-tests-passing/a-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "First item"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | 22 | it("should display third item", () => { 23 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/all-tests-passing/b-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | cy.task("runBIsWaiting"); 5 | cy.task("waitUntilRunAIsCancelled"); 6 | }); 7 | 8 | beforeEach(() => { 9 | cy.visit("/"); 10 | }); 11 | 12 | it("should display title", () => { 13 | cy.get("h1").should("have.text", "Items list"); 14 | }); 15 | 16 | it("should display first item", () => { 17 | cy.get("ul li:eq(0)").should("have.text", "First item"); 18 | }); 19 | 20 | it("should display second item", () => { 21 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 22 | }); 23 | 24 | it("should display third item", () => { 25 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/all-tests-passing/c-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "First item"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | 22 | it("should display third item", () => { 23 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/before-failing/a-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook").then(() => { 4 | expect(true).to.be.false; 5 | }); 6 | }); 7 | 8 | describe("List items tests", () => { 9 | before(() => { 10 | cy.task("log", "Executing second before hook"); 11 | }); 12 | 13 | beforeEach(() => { 14 | cy.task("log", "Executing beforeEach hook"); 15 | }); 16 | 17 | it("should display title", () => { 18 | cy.visit("/"); 19 | cy.get("h1").should("have.text", "Items list"); 20 | }); 21 | 22 | it("should display first item", () => { 23 | cy.visit("/"); 24 | cy.get("ul li:eq(0)").should("have.text", "First item"); 25 | }); 26 | 27 | it("should display second item", () => { 28 | cy.visit("/"); 29 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 30 | }); 31 | 32 | it("should display third item", () => { 33 | cy.visit("/"); 34 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/before-failing/b-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "First item"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | 22 | it("should display third item", () => { 23 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/before-failing/c-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | describe("these tests pass", () => { 11 | it("should display title", () => { 12 | cy.get("h1").should("have.text", "Items list"); 13 | }); 14 | 15 | it("should display second item", () => { 16 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 17 | }); 18 | }); 19 | 20 | describe("before failing", () => { 21 | before(() => { 22 | cy.task("log", "Executing before hook"); 23 | expect(true).to.be.false; 24 | }); 25 | 26 | it("should display first item", () => { 27 | cy.get("ul li:eq(0)").should("have.text", "First item"); 28 | }); 29 | }); 30 | 31 | describe("these tests pass", () => { 32 | it("should display third item", () => { 33 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/before-failing/d-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | describe("these tests pass", () => { 11 | it("should display title", () => { 12 | cy.get("h1").should("have.text", "Items list"); 13 | }); 14 | }); 15 | 16 | describe("beforeEach failing", () => { 17 | beforeEach(() => { 18 | cy.task("log", "Executing before hook"); 19 | expect(true).to.be.false; 20 | }); 21 | 22 | it("should display first item", () => { 23 | cy.get("ul li:eq(0)").should("have.text", "First item"); 24 | }); 25 | 26 | it("should display second item", () => { 27 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 28 | }); 29 | }); 30 | 31 | describe("these tests pass", () => { 32 | it("should display third item", () => { 33 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/describe-disabled-test-enabled/a-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", { failFast: { enabled: false } }, () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it( 15 | "should display first item", 16 | { retries: { runMode: 0, openMode: 3 }, failFast: { enabled: true } }, 17 | () => { 18 | cy.get("ul li:eq(0)").should("have.text", "Wrong text"); 19 | } 20 | ); 21 | 22 | it("should display second item", () => { 23 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 24 | }); 25 | 26 | it("should display third item", () => { 27 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/describe-disabled-test-enabled/b-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", { retries: { runMode: 0, openMode: 0 } }, () => { 15 | cy.get("ul li:eq(0)").should("have.text", "First item"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | 22 | it("should display third item", () => { 23 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/describe-disabled-test-enabled/c-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "Foo text"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/describe-enabled-test-disabled/a-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", { failFast: { enabled: true } }, () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", { retries: 2, failFast: { enabled: false } }, () => { 15 | cy.get("ul li:eq(0)").should("have.text", "Wrong text"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | 22 | it("should display third item", () => { 23 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/describe-enabled-test-disabled/b-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", { retries: { runMode: 0, openMode: 3 } }, () => { 15 | cy.get("ul li:eq(0)").should("have.text", "First item"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | 22 | it("should display third item", () => { 23 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/describe-enabled-test-disabled/c-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "Foo text"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/environment-config-only/a-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "Wrong text"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | 22 | it("should display third item", () => { 23 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/environment-config-only/b-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "First item"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | 22 | it("should display third item", () => { 23 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/environment-config-only/c-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "Foo text"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/grandparent-describe-enabled/a-file.js: -------------------------------------------------------------------------------- 1 | describe("List items disabled", { failFast: { enabled: false } }, () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | describe("Another describe enabled", { failFast: { enabled: true } }, () => { 11 | it("should display title", () => { 12 | cy.get("h1").should("have.text", "Items list"); 13 | }); 14 | 15 | it( 16 | "should display first item with fail-fast disabled", 17 | { failFast: { enabled: false } }, 18 | () => { 19 | cy.get("ul li:eq(0)").should("have.text", "Wrong text"); 20 | } 21 | ); 22 | 23 | it("should display second item", () => { 24 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 25 | }); 26 | 27 | it("should display third item", () => { 28 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/grandparent-describe-enabled/b-file.js: -------------------------------------------------------------------------------- 1 | describe("List items fail-fast enabled", { failFast: { enabled: true } }, () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | describe("Another describe", () => { 11 | describe("Another describe disabled", { failFast: { enabled: false } }, () => { 12 | it("should display title", () => { 13 | cy.get("h1").should("have.text", "Wrong text"); 14 | }); 15 | }); 16 | 17 | describe("Another describe", () => { 18 | it("should display first item", () => { 19 | cy.get("ul li:eq(0)").should("have.text", "First item"); 20 | }); 21 | 22 | it("should display second item", () => { 23 | cy.get("ul li:eq(1)").should("have.text", "Wrong text"); 24 | }); 25 | 26 | it("should display third item", () => { 27 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/grandparent-describe-enabled/c-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "Foo text"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/many-failing-tests/a-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item - failing", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "Wrong text"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | 22 | it("should display third item", () => { 23 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/many-failing-tests/b-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item - failing", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "Wrong text"); 16 | }); 17 | 18 | it("should display second item - failing", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Wrong text"); 20 | }); 21 | 22 | it("should display second item", () => { 23 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 24 | }); 25 | 26 | it("should display third item", () => { 27 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/many-failing-tests/c-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item - failing", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "Wrong Text"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/parallel-failing/a-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | cy.task("waitUntilRunBIsWaiting"); 5 | }); 6 | 7 | beforeEach(() => { 8 | cy.visit("/"); 9 | }); 10 | 11 | it("should display title", () => { 12 | cy.get("h1").should("have.text", "Items list"); 13 | }); 14 | 15 | it("should display first item", () => { 16 | cy.get("ul li:eq(0)").should("have.text", "Wrong text"); 17 | }); 18 | 19 | it("should display second item", () => { 20 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 21 | }); 22 | 23 | it("should display third item", () => { 24 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/parallel-failing/b-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "First item"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | 22 | it("should display third item", () => { 23 | cy.get("ul li:eq(2)").should("have.text", "Third item"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/integration/parallel-failing/c-file.js: -------------------------------------------------------------------------------- 1 | describe("List items", () => { 2 | before(() => { 3 | cy.task("log", "Executing before hook"); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.visit("/"); 8 | }); 9 | 10 | it("should display title", () => { 11 | cy.get("h1").should("have.text", "Items list"); 12 | }); 13 | 14 | it("should display first item", () => { 15 | cy.get("ul li:eq(0)").should("have.text", "Foo text"); 16 | }); 17 | 18 | it("should display second item", () => { 19 | cy.get("ul li:eq(1)").should("have.text", "Second item"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/parallel-preprocessor-babel-config.config.js: -------------------------------------------------------------------------------- 1 | const fsExtra = require("fs-extra"); 2 | const path = require("path"); 3 | const webpackPreprocessor = require("@cypress/webpack-preprocessor"); 4 | const defaults = webpackPreprocessor.defaultOptions; 5 | 6 | const storageFolder = path.resolve(__dirname, "..", "..", "parallel-storage"); 7 | 8 | const cancelledFile = path.resolve(storageFolder, "run-a-is-cancelled.json"); 9 | const waitingFile = path.resolve(storageFolder, "run-b-is-waiting.json"); 10 | 11 | function waitForFile(filePath) { 12 | return new Promise((resolve) => { 13 | console.log(`Waiting until file exists: ${filePath}`); 14 | const checkFileExists = setInterval(() => { 15 | if (fsExtra.pathExistsSync(filePath)) { 16 | clearInterval(checkFileExists); 17 | console.log(`File exists: ${filePath}`); 18 | resolve(true); 19 | } 20 | }, 500); 21 | setTimeout(() => { 22 | console.log(`Timeout. File not exists: ${filePath}`); 23 | clearInterval(checkFileExists); 24 | resolve(false); 25 | }, 20000); 26 | }); 27 | } 28 | 29 | module.exports = { 30 | e2e: { 31 | baseUrl: "http://localhost:3000", 32 | setupNodeEvents(on, config) { 33 | require("../../../plugin")(on, config, { 34 | parallelCallbacks: { 35 | onCancel: () => { 36 | fsExtra.writeJsonSync(cancelledFile, { cancelled: true }); 37 | }, 38 | isCancelled: () => { 39 | if (fsExtra.pathExistsSync(cancelledFile)) { 40 | return fsExtra.readJsonSync(cancelledFile).cancelled; 41 | } 42 | return false; 43 | }, 44 | }, 45 | }); 46 | 47 | // Add log task 48 | on("task", { 49 | log: function (message) { 50 | console.log(message); 51 | return null; 52 | }, 53 | waitUntilRunAIsCancelled: function () { 54 | return waitForFile(cancelledFile); 55 | }, 56 | waitUntilRunBIsWaiting: function () { 57 | return waitForFile(waitingFile); 58 | }, 59 | runBIsWaiting: function () { 60 | fsExtra.writeJsonSync(waitingFile, { waiting: true }); 61 | return true; 62 | }, 63 | }); 64 | 65 | delete defaults.webpackOptions.module.rules[0].use[0].options.presets; 66 | on("file:preprocessor", webpackPreprocessor(defaults)); 67 | 68 | return config; 69 | }, 70 | specPattern: "cypress/integration/**/*.js", 71 | }, 72 | video: false, 73 | reporter: "mochawesome", 74 | reporterOptions: { 75 | reportDir: "cypress/results", 76 | overwrite: false, 77 | html: false, 78 | json: true, 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/parallel-ts.config.js: -------------------------------------------------------------------------------- 1 | const fsExtra = require("fs-extra"); 2 | const path = require("path"); 3 | 4 | const storageFolder = path.resolve(__dirname, "..", "..", "parallel-storage"); 5 | 6 | const cancelledFile = path.resolve(storageFolder, "run-a-is-cancelled.json"); 7 | const waitingFile = path.resolve(storageFolder, "run-b-is-waiting.json"); 8 | 9 | function waitForFile(filePath) { 10 | return new Promise((resolve) => { 11 | console.log(`Waiting until file exists: ${filePath}`); 12 | const checkFileExists = setInterval(() => { 13 | if (fsExtra.pathExistsSync(filePath)) { 14 | clearInterval(checkFileExists); 15 | console.log(`File exists: ${filePath}`); 16 | resolve(true); 17 | } 18 | }, 500); 19 | setTimeout(() => { 20 | console.log(`Timeout. File not exists: ${filePath}`); 21 | clearInterval(checkFileExists); 22 | resolve(false); 23 | }, 20000); 24 | }); 25 | } 26 | 27 | module.exports = { 28 | e2e: { 29 | baseUrl: "http://localhost:3000", 30 | setupNodeEvents(on, config) { 31 | require("../../../plugin")(on, config, { 32 | parallelCallbacks: { 33 | onCancel: () => { 34 | fsExtra.writeJsonSync(cancelledFile, { cancelled: true }); 35 | }, 36 | isCancelled: () => { 37 | if (fsExtra.pathExistsSync(cancelledFile)) { 38 | return fsExtra.readJsonSync(cancelledFile).cancelled; 39 | } 40 | return false; 41 | }, 42 | }, 43 | }); 44 | 45 | // Add log task 46 | on("task", { 47 | log: function (message) { 48 | console.log(message); 49 | return null; 50 | }, 51 | waitUntilRunAIsCancelled: function () { 52 | return waitForFile(cancelledFile); 53 | }, 54 | waitUntilRunBIsWaiting: function () { 55 | return waitForFile(waitingFile); 56 | }, 57 | runBIsWaiting: function () { 58 | fsExtra.writeJsonSync(waitingFile, { waiting: true }); 59 | return true; 60 | }, 61 | }); 62 | 63 | return config; 64 | }, 65 | specPattern: "cypress/integration/**/*.ts", 66 | }, 67 | video: false, 68 | reporter: "mochawesome", 69 | reporterOptions: { 70 | reportDir: "cypress/results", 71 | overwrite: false, 72 | html: false, 73 | json: true, 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/plugins-custom/parallel-preprocessor-babel-config/index.js: -------------------------------------------------------------------------------- 1 | const fsExtra = require("fs-extra"); 2 | const path = require("path"); 3 | const webpackPreprocessor = require("@cypress/webpack-preprocessor"); 4 | const defaults = webpackPreprocessor.defaultOptions; 5 | 6 | const storageFolder = path.resolve(__dirname, "..", "..", "..", "..", "parallel-storage"); 7 | 8 | const cancelledFile = path.resolve(storageFolder, "run-a-is-cancelled.json"); 9 | const waitingFile = path.resolve(storageFolder, "run-b-is-waiting.json"); 10 | 11 | function waitForFile(filePath) { 12 | return new Promise((resolve) => { 13 | console.log(`Waiting until file exists: ${filePath}`); 14 | const checkFileExists = setInterval(() => { 15 | if (fsExtra.pathExistsSync(filePath)) { 16 | clearInterval(checkFileExists); 17 | console.log(`File exists: ${filePath}`); 18 | resolve(true); 19 | } 20 | }, 500); 21 | setTimeout(() => { 22 | console.log(`Timeout. File not exists: ${filePath}`); 23 | clearInterval(checkFileExists); 24 | resolve(false); 25 | }, 20000); 26 | }); 27 | } 28 | 29 | module.exports = (on, config) => { 30 | require("../../../../../plugin")(on, config, { 31 | parallelCallbacks: { 32 | onCancel: () => { 33 | fsExtra.writeJsonSync(cancelledFile, { cancelled: true }); 34 | }, 35 | isCancelled: () => { 36 | if (fsExtra.pathExistsSync(cancelledFile)) { 37 | return fsExtra.readJsonSync(cancelledFile).cancelled; 38 | } 39 | return false; 40 | }, 41 | }, 42 | }); 43 | 44 | // Add log task 45 | on("task", { 46 | log: function (message) { 47 | console.log(message); 48 | return null; 49 | }, 50 | waitUntilRunAIsCancelled: function () { 51 | return waitForFile(cancelledFile); 52 | }, 53 | waitUntilRunBIsWaiting: function () { 54 | return waitForFile(waitingFile); 55 | }, 56 | runBIsWaiting: function () { 57 | fsExtra.writeJsonSync(waitingFile, { waiting: true }); 58 | return true; 59 | }, 60 | }); 61 | 62 | delete defaults.webpackOptions.module.rules[0].use[0].options.presets; 63 | on("file:preprocessor", webpackPreprocessor(defaults)); 64 | 65 | return config; 66 | }; 67 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/plugins-custom/parallel-preprocessor-babel-config/index.ts: -------------------------------------------------------------------------------- 1 | import fsExtra = require("fs-extra"); 2 | import path = require("path"); 3 | 4 | import cypressFailFast = require("../support/cypress-fail-fast/plugin"); 5 | 6 | const storageFolder = path.resolve(__dirname, "..", "..", "..", "..", "parallel-storage"); 7 | 8 | const cancelledFile = path.resolve(storageFolder, "run-a-is-cancelled.json"); 9 | const waitingFile = path.resolve(storageFolder, "run-b-is-waiting.json"); 10 | 11 | function waitForFile(filePath) { 12 | return new Promise((resolve) => { 13 | console.log(`Waiting until file exists: ${filePath}`); 14 | const checkFileExists = setInterval(() => { 15 | if (fsExtra.pathExistsSync(filePath)) { 16 | clearInterval(checkFileExists); 17 | console.log(`File exists: ${filePath}`); 18 | resolve(true); 19 | } 20 | }, 500); 21 | setTimeout(() => { 22 | console.log(`Timeout. File not exists: ${filePath}`); 23 | clearInterval(checkFileExists); 24 | resolve(false); 25 | }, 20000); 26 | }); 27 | } 28 | 29 | export default (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): Cypress.ResolvedConfigOptions => { 30 | cypressFailFast(on, config, { 31 | parallelCallbacks: { 32 | onCancel: () => { 33 | fsExtra.writeJsonSync(cancelledFile, { cancelled: true }); 34 | }, 35 | isCancelled: () => { 36 | if (fsExtra.pathExistsSync(cancelledFile)) { 37 | return fsExtra.readJsonSync(cancelledFile).cancelled; 38 | } 39 | return false; 40 | }, 41 | }, 42 | }); 43 | 44 | on("task", { 45 | log: function (message) { 46 | console.log(message); 47 | return null; 48 | }, 49 | waitUntilRunAIsCancelled: function () { 50 | return waitForFile(cancelledFile); 51 | }, 52 | waitUntilRunBIsWaiting: function () { 53 | return waitForFile(waitingFile); 54 | }, 55 | runBIsWaiting: function () { 56 | fsExtra.writeJsonSync(waitingFile, { waiting: true }); 57 | return true; 58 | }, 59 | }); 60 | 61 | return config; 62 | }; 63 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/plugins-custom/parallel/index.js: -------------------------------------------------------------------------------- 1 | const fsExtra = require("fs-extra"); 2 | const path = require("path"); 3 | 4 | const storageFolder = path.resolve(__dirname, "..", "..", "..", "..", "parallel-storage"); 5 | 6 | const cancelledFile = path.resolve(storageFolder, "run-a-is-cancelled.json"); 7 | const waitingFile = path.resolve(storageFolder, "run-b-is-waiting.json"); 8 | 9 | function waitForFile(filePath) { 10 | return new Promise((resolve) => { 11 | console.log(`Waiting until file exists: ${filePath}`); 12 | const checkFileExists = setInterval(() => { 13 | if (fsExtra.pathExistsSync(filePath)) { 14 | clearInterval(checkFileExists); 15 | console.log(`File exists: ${filePath}`); 16 | resolve(true); 17 | } 18 | }, 500); 19 | setTimeout(() => { 20 | console.log(`Timeout. File not exists: ${filePath}`); 21 | clearInterval(checkFileExists); 22 | resolve(false); 23 | }, 20000); 24 | }); 25 | } 26 | 27 | module.exports = (on, config) => { 28 | require("../../../../../plugin")(on, config, { 29 | parallelCallbacks: { 30 | onCancel: () => { 31 | fsExtra.writeJsonSync(cancelledFile, { cancelled: true }); 32 | }, 33 | isCancelled: () => { 34 | if (fsExtra.pathExistsSync(cancelledFile)) { 35 | return fsExtra.readJsonSync(cancelledFile).cancelled; 36 | } 37 | return false; 38 | }, 39 | }, 40 | }); 41 | 42 | // Add log task 43 | on("task", { 44 | log: function (message) { 45 | console.log(message); 46 | return null; 47 | }, 48 | waitUntilRunAIsCancelled: function () { 49 | return waitForFile(cancelledFile); 50 | }, 51 | waitUntilRunBIsWaiting: function () { 52 | return waitForFile(waitingFile); 53 | }, 54 | runBIsWaiting: function () { 55 | fsExtra.writeJsonSync(waitingFile, { waiting: true }); 56 | return true; 57 | }, 58 | }); 59 | 60 | return config; 61 | }; 62 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/plugins-custom/parallel/index.ts: -------------------------------------------------------------------------------- 1 | import fsExtra = require("fs-extra"); 2 | import path = require("path"); 3 | import cypressFailFast = require("../support/cypress-fail-fast/plugin"); 4 | 5 | const storageFolder = path.resolve(__dirname, "..", "..", "..", "..", "parallel-storage"); 6 | 7 | const cancelledFile = path.resolve(storageFolder, "run-a-is-cancelled.json"); 8 | const waitingFile = path.resolve(storageFolder, "run-b-is-waiting.json"); 9 | 10 | function waitForFile(filePath) { 11 | return new Promise((resolve) => { 12 | console.log(`Waiting until file exists: ${filePath}`); 13 | const checkFileExists = setInterval(() => { 14 | if (fsExtra.pathExistsSync(filePath)) { 15 | clearInterval(checkFileExists); 16 | console.log(`File exists: ${filePath}`); 17 | resolve(true); 18 | } 19 | }, 500); 20 | setTimeout(() => { 21 | console.log(`Timeout. File not exists: ${filePath}`); 22 | clearInterval(checkFileExists); 23 | resolve(false); 24 | }, 20000); 25 | }); 26 | } 27 | 28 | export default (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): Cypress.ResolvedConfigOptions => { 29 | cypressFailFast(on, config, { 30 | parallelCallbacks: { 31 | onCancel: () => { 32 | fsExtra.writeJsonSync(cancelledFile, { cancelled: true }); 33 | }, 34 | isCancelled: () => { 35 | if (fsExtra.pathExistsSync(cancelledFile)) { 36 | return fsExtra.readJsonSync(cancelledFile).cancelled; 37 | } 38 | return false; 39 | }, 40 | }, 41 | }); 42 | 43 | on("task", { 44 | log: function (message) { 45 | console.log(message); 46 | return null; 47 | }, 48 | waitUntilRunAIsCancelled: function () { 49 | return waitForFile(cancelledFile); 50 | }, 51 | waitUntilRunBIsWaiting: function () { 52 | return waitForFile(waitingFile); 53 | }, 54 | runBIsWaiting: function () { 55 | fsExtra.writeJsonSync(waitingFile, { waiting: true }); 56 | return true; 57 | }, 58 | }); 59 | 60 | return config; 61 | }; 62 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/plugins-custom/preprocessor-babel-config/index.js: -------------------------------------------------------------------------------- 1 | const webpackPreprocessor = require("@cypress/webpack-preprocessor"); 2 | const defaults = webpackPreprocessor.defaultOptions; 3 | 4 | module.exports = (on, config) => { 5 | require("../../../../../plugin")(on, config); 6 | 7 | // Add log task 8 | on("task", { 9 | log: function (message) { 10 | console.log(message); 11 | return null; 12 | }, 13 | }); 14 | 15 | delete defaults.webpackOptions.module.rules[0].use[0].options.presets; 16 | on("file:preprocessor", webpackPreprocessor(defaults)); 17 | 18 | return config; 19 | }; 20 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/plugins-custom/preprocessor-babel-config/index.ts: -------------------------------------------------------------------------------- 1 | import cypressFailFast = require("../support/cypress-fail-fast/plugin"); 2 | 3 | export default (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): Cypress.ResolvedConfigOptions => { 4 | cypressFailFast(on, config); 5 | 6 | on("task", { 7 | log: function (message) { 8 | console.log(message); 9 | return null; 10 | }, 11 | }); 12 | 13 | return config; 14 | }; 15 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/plugins/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (on, config) => { 2 | require("../../../../../plugin")(on, config); 3 | 4 | // Add log task 5 | on("task", { 6 | log: function (message) { 7 | console.log(message); 8 | return null; 9 | }, 10 | }); 11 | 12 | return config; 13 | }; 14 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import cypressFailFast = require("../support/cypress-fail-fast/plugin"); 2 | 3 | export default (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): Cypress.ResolvedConfigOptions => { 4 | cypressFailFast(on, config); 5 | 6 | on("task", { 7 | log: function (message) { 8 | console.log(message); 9 | return null; 10 | }, 11 | }); 12 | 13 | return config; 14 | }; 15 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/scripts/reports.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fsExtra = require("fs-extra"); 3 | const { merge } = require("mochawesome-merge"); 4 | 5 | function cypressPathBase() { 6 | return path.resolve(__dirname, "..", "cypress"); 7 | } 8 | 9 | function cypressPath(filePath) { 10 | return path.resolve(cypressPathBase(), filePath); 11 | } 12 | 13 | function deletePreviousReports() { 14 | fsExtra.removeSync(cypressPath("results")); 15 | fsExtra.removeSync(cypressPath("mochawesome.json")); 16 | fsExtra.removeSync(path.resolve(__dirname, "..", "mochawesome-report")); 17 | } 18 | 19 | function mergeReports() { 20 | merge({ 21 | files: ["./cypress/results/*.json"], 22 | }).then((report) => { 23 | fsExtra.writeJsonSync(cypressPath("mochawesome.json"), report); 24 | }); 25 | } 26 | 27 | function run() { 28 | const command = process.argv[2]; 29 | switch (command) { 30 | case "clean": 31 | deletePreviousReports(); 32 | break; 33 | case "merge": 34 | mergeReports(); 35 | break; 36 | default: 37 | console.log("Command not found"); 38 | } 39 | } 40 | 41 | run(); 42 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/support/e2e.js: -------------------------------------------------------------------------------- 1 | /* global Cypress */ 2 | 3 | import addContext from "mochawesome/addContext"; 4 | import "cypress-fail-fast"; 5 | 6 | Cypress.on("test:after:run", (test) => { 7 | if (test.state === "failed") { 8 | addContext({ test }, "Executed test:after:run event in failed test"); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/support/e2e.ts: -------------------------------------------------------------------------------- 1 | import addContext = require("mochawesome/addContext"); 2 | import "./cypress-fail-fast"; 3 | 4 | Cypress.on("test:after:run", (test, runnable) => { 5 | if (test.state === "failed") { 6 | addContext({ test }, "Executed test:after:run event in failed test"); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/support/index.js: -------------------------------------------------------------------------------- 1 | /* global Cypress */ 2 | 3 | import addContext from "mochawesome/addContext"; 4 | import "cypress-fail-fast"; 5 | 6 | Cypress.on("test:after:run", (test) => { 7 | if (test.state === "failed") { 8 | addContext({ test }, "Executed test:after:run event in failed test"); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /test-e2e/cypress-src/support/index.ts: -------------------------------------------------------------------------------- 1 | import addContext = require("mochawesome/addContext"); 2 | import "./cypress-fail-fast"; 3 | 4 | Cypress.on("test:after:run", (test, runnable) => { 5 | if (test.state === "failed") { 6 | addContext({ test }, "Executed test:after:run event in failed test"); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/cypress-10/.gitignore: -------------------------------------------------------------------------------- 1 | # exclude everything 2 | /* 3 | 4 | # exception to the rule 5 | !/.gitignore 6 | !/package.json 7 | !/package-lock.json 8 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/cypress-10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-fail-fast-v10-e2e-tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "report:clean": "cross-env node scripts/reports.js clean", 7 | "report:create": "cross-env node scripts/reports.js merge && marge cypress/mochawesome.json", 8 | "cypress:open": "cypress open", 9 | "cypress:run": "npm run report:clean && cypress run" 10 | }, 11 | "devDependencies": { 12 | "@cypress/webpack-preprocessor": "5.17.1", 13 | "babel-loader": "9.1.3", 14 | "babel-plugin-module-resolver": "5.0.2", 15 | "cross-env": "7.0.3", 16 | "cypress": "10.11.0", 17 | "fs-extra": "11.2.0", 18 | "mochawesome": "7.1.3", 19 | "mochawesome-merge": "4.3.0", 20 | "mochawesome-report-generator": "6.2.0", 21 | "webpack": "5.93.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/cypress-11/.gitignore: -------------------------------------------------------------------------------- 1 | # exclude everything 2 | /* 3 | 4 | # exception to the rule 5 | !/.gitignore 6 | !/package.json 7 | !/package-lock.json 8 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/cypress-11/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-fail-fast-v11-e2e-tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "report:clean": "cross-env node scripts/reports.js clean", 7 | "report:create": "cross-env node scripts/reports.js merge && marge cypress/mochawesome.json", 8 | "cypress:open": "cypress open", 9 | "cypress:run": "npm run report:clean && cypress run" 10 | }, 11 | "devDependencies": { 12 | "@cypress/webpack-preprocessor": "5.17.1", 13 | "babel-loader": "9.1.3", 14 | "babel-plugin-module-resolver": "5.0.2", 15 | "cross-env": "7.0.3", 16 | "cypress": "11.2.0", 17 | "fs-extra": "11.2.0", 18 | "mochawesome": "7.1.3", 19 | "mochawesome-merge": "4.3.0", 20 | "mochawesome-report-generator": "6.2.0", 21 | "webpack": "5.93.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/cypress-12/.gitignore: -------------------------------------------------------------------------------- 1 | # exclude everything 2 | /* 3 | 4 | # exception to the rule 5 | !/.gitignore 6 | !/package.json 7 | !/package-lock.json 8 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/cypress-12/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-fail-fast-v12-e2e-tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "report:clean": "cross-env node scripts/reports.js clean", 7 | "report:create": "cross-env node scripts/reports.js merge && marge cypress/mochawesome.json", 8 | "cypress:open": "cypress open", 9 | "cypress:run": "npm run report:clean && cypress run" 10 | }, 11 | "devDependencies": { 12 | "@cypress/webpack-preprocessor": "5.17.1", 13 | "babel-loader": "9.1.3", 14 | "babel-plugin-module-resolver": "5.0.2", 15 | "cross-env": "7.0.3", 16 | "cypress": "12.17.3", 17 | "fs-extra": "11.2.0", 18 | "mochawesome": "7.1.3", 19 | "mochawesome-merge": "4.3.0", 20 | "mochawesome-report-generator": "6.2.0", 21 | "webpack": "5.93.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/cypress-13/.gitignore: -------------------------------------------------------------------------------- 1 | # exclude everything 2 | /* 3 | 4 | # exception to the rule 5 | !/.gitignore 6 | !/package.json 7 | !/package-lock.json 8 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/cypress-13/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-fail-fast-v13-e2e-tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "report:clean": "cross-env node scripts/reports.js clean", 7 | "report:create": "cross-env node scripts/reports.js merge && marge cypress/mochawesome.json", 8 | "cypress:open": "cypress open", 9 | "cypress:run": "npm run report:clean && cypress run" 10 | }, 11 | "devDependencies": { 12 | "@cypress/webpack-preprocessor": "6.0.2", 13 | "babel-loader": "9.1.3", 14 | "babel-plugin-module-resolver": "5.0.2", 15 | "cross-env": "7.0.3", 16 | "cypress": "13.13.2", 17 | "fs-extra": "11.2.0", 18 | "mochawesome": "7.1.3", 19 | "mochawesome-merge": "4.3.0", 20 | "mochawesome-report-generator": "6.2.0", 21 | "webpack": "5.93.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/cypress-9/.gitignore: -------------------------------------------------------------------------------- 1 | # exclude everything 2 | /* 3 | 4 | # exception to the rule 5 | !/.gitignore 6 | !/package.json 7 | !/package-lock.json 8 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/cypress-9/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-fail-fast-v9-e2e-tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "report:clean": "cross-env node scripts/reports.js clean", 7 | "report:create": "cross-env node scripts/reports.js merge && marge cypress/mochawesome.json", 8 | "cypress:open": "cypress open", 9 | "cypress:run": "npm run report:clean && cypress run" 10 | }, 11 | "devDependencies": { 12 | "@cypress/webpack-preprocessor": "5.17.1", 13 | "babel-loader": "9.1.3", 14 | "babel-plugin-module-resolver": "5.0.2", 15 | "cross-env": "7.0.3", 16 | "cypress": "9.7.0", 17 | "fs-extra": "11.2.0", 18 | "mochawesome": "7.1.3", 19 | "mochawesome-merge": "4.3.0", 20 | "mochawesome-report-generator": "6.2.0", 21 | "webpack": "5.93.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # exclude everything 2 | /* 3 | 4 | # exception to the rule 5 | !/.gitignore 6 | !/package.json 7 | !/package-lock.json 8 | !/tsconfig.json 9 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-fail-fast-typescript-e2e-tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "report:clean": "cross-env node scripts/reports.js clean", 7 | "report:create": "cross-env node scripts/reports.js merge && marge cypress/mochawesome.json", 8 | "cypress:open": "cypress open", 9 | "tsc": "tsc", 10 | "cypress:run": "npm run report:clean && cypress run" 11 | }, 12 | "devDependencies": { 13 | "cross-env": "7.0.3", 14 | "cypress": "13.13.2", 15 | "fs-extra": "11.2.0", 16 | "mochawesome": "7.1.3", 17 | "mochawesome-merge": "4.3.0", 18 | "mochawesome-report-generator": "6.2.0", 19 | "typescript": "5.5.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test-e2e/cypress-variants/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "types": ["cypress"], 8 | "skipLibCheck": true, 9 | }, 10 | "include": [ 11 | "**/*.ts", 12 | ], 13 | "exclude": [ 14 | "dist", 15 | "node_modules", 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /test-e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // Stop on first fail 6 | bail: true, 7 | 8 | // Automatically clear mock calls and instances between every test 9 | clearMocks: true, 10 | 11 | // Indicates whether the coverage information should be collected while executing the test 12 | collectCoverage: false, 13 | 14 | // The test environment that will be used for testing 15 | testEnvironment: "node", 16 | 17 | // The glob patterns Jest uses to detect test files 18 | testMatch: ["**/test/**/*.spec.js"], 19 | // testMatch: ["**/test/**/before.spec.js"], 20 | }; 21 | -------------------------------------------------------------------------------- /test-e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-fail-fast-e2e-tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "serve -l 3000", 7 | "test": "jest --runInBand --verbose", 8 | "test:build": "cross-env node commands/build.js", 9 | "test:install": "cd cypress-variants/typescript && npm i && cd ../cypress-9 && npm i && cd ../cypress-10 && npm i && cd ../cypress-11 && npm i && cd ../cypress-12 && npm i && cd ../cypress-13 && npm i", 10 | "serve-and-test": "start-server-and-test serve http-get://localhost:3000 test", 11 | "test:ci": "npm run test:build && npm run test:install && npm run serve-and-test" 12 | }, 13 | "devDependencies": { 14 | "cross-env": "7.0.3", 15 | "fs-extra": "11.2.0", 16 | "jest": "29.7.0", 17 | "serve": "14.2.3", 18 | "start-server-and-test": "2.0.5", 19 | "strip-ansi": "6.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test-e2e/parallel-storage/.gitignore: -------------------------------------------------------------------------------- 1 | # exclude everything 2 | /* 3 | 4 | # exception to the rule 5 | !/.gitignore 6 | -------------------------------------------------------------------------------- /test-e2e/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testing app 6 | 7 | 8 |

Items list

9 |
    10 |
  • First item
  • 11 |
  • Second item
  • 12 |
  • Third item
  • 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /test-e2e/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": "public", 3 | "trailingSlash": false, 4 | "directoryListing": false, 5 | "cleanUrls": true, 6 | "rewrites": [ 7 | { 8 | "source": "/", 9 | "destination": "/index.html" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test-e2e/test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "jest": true, 4 | "afterEach": true, 5 | "afterAll": true, 6 | "beforeAll": true, 7 | "beforeEach": true, 8 | "describe": true, 9 | "expect": true, 10 | "it": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-e2e/test/before.spec.js: -------------------------------------------------------------------------------- 1 | const { runSpecsTests } = require("./support/testsRunner"); 2 | 3 | runSpecsTests("When before hook fails", { 4 | skipVariants: false, 5 | specs: "before-failing", 6 | specsResults: [ 7 | { 8 | logBefore: true, 9 | logSecondBefore: false, 10 | logBeforeEach: false, 11 | executed: 4, 12 | passed: 0, 13 | failed: 1, 14 | skipped: 3, 15 | }, 16 | { 17 | logBefore: false, 18 | executed: 4, 19 | passed: 0, 20 | failed: 0, 21 | skipped: 4, 22 | }, 23 | { 24 | logBefore: false, 25 | executed: 4, 26 | passed: 0, 27 | failed: 0, 28 | skipped: 4, 29 | }, 30 | { 31 | logBefore: false, 32 | executed: 4, 33 | passed: 0, 34 | failed: 0, 35 | skipped: 4, 36 | }, 37 | ], 38 | env: { 39 | CYPRESS_FAIL_FAST_STRATEGY: "run", 40 | }, 41 | }); 42 | 43 | runSpecsTests("When before hook fails in spec mode", { 44 | skipVariants: false, 45 | specs: "before-failing", 46 | specsResults: [ 47 | { 48 | logBefore: true, 49 | logSecondBefore: false, 50 | logBeforeEach: false, 51 | executed: 4, 52 | passed: 0, 53 | failed: 1, 54 | skipped: 3, 55 | }, 56 | { 57 | logBefore: true, 58 | executed: 4, 59 | passed: 4, 60 | failed: 0, 61 | skipped: 0, 62 | }, 63 | { 64 | logBefore: true, 65 | executed: 4, 66 | passed: 2, 67 | failed: 1, 68 | skipped: 1, 69 | }, 70 | { 71 | logBefore: true, 72 | executed: 4, 73 | passed: 1, 74 | failed: 1, 75 | skipped: 2, 76 | }, 77 | ], 78 | env: { 79 | CYPRESS_FAIL_FAST_STRATEGY: "spec", 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /test-e2e/test/environment-config.spec.js: -------------------------------------------------------------------------------- 1 | const { runSpecsTests } = require("./support/testsRunner"); 2 | 3 | runSpecsTests("When it has default configuration", { 4 | specs: "environment-config-only", 5 | skipVariants: false, 6 | specsResults: [ 7 | { 8 | logBefore: true, 9 | executed: 4, 10 | passed: 1, 11 | failed: 1, 12 | skipped: 2, 13 | }, 14 | { 15 | logBefore: false, 16 | executed: 4, 17 | passed: 0, 18 | failed: 0, 19 | skipped: 4, 20 | }, 21 | { 22 | logBefore: false, 23 | executed: 3, 24 | passed: 0, 25 | failed: 0, 26 | skipped: 3, 27 | }, 28 | ], 29 | }); 30 | 31 | runSpecsTests("When it has BAIL configured and strategy is run", { 32 | specs: "many-failing-tests", 33 | skipVariants: true, 34 | specsResults: [ 35 | { 36 | logBefore: true, 37 | executed: 4, 38 | passed: 3, 39 | failed: 1, 40 | skipped: 0, 41 | }, 42 | { 43 | logBefore: true, 44 | executed: 5, 45 | passed: 1, 46 | failed: 2, 47 | skipped: 2, 48 | }, 49 | { 50 | logBefore: false, 51 | executed: 3, 52 | passed: 0, 53 | failed: 0, 54 | skipped: 3, 55 | }, 56 | ], 57 | env: { 58 | CYPRESS_FAIL_FAST_BAIL: 3, 59 | }, 60 | }); 61 | 62 | runSpecsTests("When it has BAIL configured and strategy is spec", { 63 | specs: "many-failing-tests", 64 | skipVariants: true, 65 | specsResults: [ 66 | { 67 | logBefore: true, 68 | executed: 4, 69 | passed: 3, 70 | failed: 1, 71 | skipped: 0, 72 | }, 73 | { 74 | logBefore: true, 75 | executed: 5, 76 | passed: 1, 77 | failed: 2, 78 | skipped: 2, 79 | }, 80 | { 81 | logBefore: true, 82 | executed: 3, 83 | passed: 2, 84 | failed: 1, 85 | skipped: 0, 86 | }, 87 | ], 88 | env: { 89 | CYPRESS_FAIL_FAST_BAIL: 2, 90 | CYPRESS_FAIL_FAST_STRATEGY: "spec", 91 | }, 92 | }); 93 | 94 | runSpecsTests("When it is disabled using plugin environment variable", { 95 | specs: "environment-config-only", 96 | skipVariants: false, 97 | specsResults: [ 98 | { 99 | logBefore: true, 100 | executed: 4, 101 | passed: 3, 102 | failed: 1, 103 | skipped: 0, 104 | }, 105 | { 106 | logBefore: true, 107 | executed: 4, 108 | passed: 4, 109 | failed: 0, 110 | skipped: 0, 111 | }, 112 | { 113 | logBefore: true, 114 | executed: 3, 115 | passed: 2, 116 | failed: 1, 117 | skipped: 0, 118 | }, 119 | ], 120 | env: { 121 | CYPRESS_FAIL_FAST_PLUGIN: false, 122 | CYPRESS_FAIL_FAST_ENABLED: true, 123 | }, 124 | }); 125 | 126 | runSpecsTests("When it is disabled using enabled environment variable", { 127 | skipVariants: true, 128 | specs: "environment-config-only", 129 | specsResults: [ 130 | { 131 | logBefore: true, 132 | executed: 4, 133 | passed: 3, 134 | failed: 1, 135 | skipped: 0, 136 | }, 137 | { 138 | logBefore: true, 139 | executed: 4, 140 | passed: 4, 141 | failed: 0, 142 | skipped: 0, 143 | }, 144 | { 145 | logBefore: true, 146 | executed: 3, 147 | passed: 2, 148 | failed: 1, 149 | skipped: 0, 150 | }, 151 | ], 152 | env: { 153 | CYPRESS_FAIL_FAST_ENABLED: false, 154 | }, 155 | }); 156 | -------------------------------------------------------------------------------- /test-e2e/test/parallel.spec.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fsExtra = require("fs-extra"); 3 | 4 | const { runParallelSpecsTests } = require("./support/testsRunner"); 5 | 6 | const removeParallelStorage = () => { 7 | const parallelStorageFolder = path.resolve(__dirname, "..", "parallel-storage"); 8 | fsExtra.removeSync(path.resolve(parallelStorageFolder, "run-a-is-cancelled.json")); 9 | fsExtra.removeSync(path.resolve(parallelStorageFolder, "run-b-is-waiting.json")); 10 | }; 11 | 12 | runParallelSpecsTests( 13 | "When parallel strategy is enabled and first tests run fails", 14 | [ 15 | { 16 | cypressVersion: "latest", 17 | configFile: "parallel-preprocessor-babel-config.config.js", 18 | specs: "parallel-failing", 19 | delay: 5000, 20 | specsResults: [ 21 | { 22 | logBefore: true, 23 | executed: 4, 24 | passed: 1, 25 | failed: 1, 26 | skipped: 2, 27 | }, 28 | { 29 | logBefore: false, 30 | executed: 4, 31 | passed: 0, 32 | failed: 0, 33 | skipped: 4, 34 | }, 35 | { 36 | logBefore: false, 37 | executed: 3, 38 | passed: 0, 39 | failed: 0, 40 | skipped: 3, 41 | }, 42 | ], 43 | env: { 44 | CYPRESS_FAIL_FAST_STRATEGY: "parallel", 45 | }, 46 | }, 47 | { 48 | cypressVersion: "ts", 49 | configFile: "parallel-ts.config.js", 50 | configFileDest: "cypress.config.js", 51 | specs: "all-tests-passing", 52 | specsResults: [ 53 | { 54 | logBefore: true, 55 | executed: 4, 56 | passed: 4, 57 | failed: 0, 58 | skipped: 0, 59 | }, 60 | { 61 | logBefore: true, 62 | executed: 4, 63 | passed: 0, 64 | failed: 0, 65 | skipped: 4, 66 | }, 67 | { 68 | logBefore: false, 69 | executed: 4, 70 | passed: 0, 71 | failed: 0, 72 | skipped: 4, 73 | }, 74 | ], 75 | env: { 76 | CYPRESS_FAIL_FAST_STRATEGY: "parallel", 77 | }, 78 | }, 79 | ], 80 | { 81 | afterAll: removeParallelStorage, 82 | }, 83 | ); 84 | 85 | runParallelSpecsTests( 86 | "When parallel strategy is disabled and first tests run fails", 87 | [ 88 | { 89 | cypressVersion: "latest", 90 | configFile: "parallel-preprocessor-babel-config.config.js", 91 | specs: "parallel-failing", 92 | delay: 5000, 93 | specsResults: [ 94 | { 95 | logBefore: true, 96 | executed: 4, 97 | passed: 1, 98 | failed: 1, 99 | skipped: 2, 100 | }, 101 | { 102 | logBefore: false, 103 | executed: 4, 104 | passed: 0, 105 | failed: 0, 106 | skipped: 4, 107 | }, 108 | { 109 | logBefore: false, 110 | executed: 3, 111 | passed: 0, 112 | failed: 0, 113 | skipped: 3, 114 | }, 115 | ], 116 | env: { 117 | CYPRESS_FAIL_FAST_STRATEGY: "run", 118 | }, 119 | }, 120 | { 121 | cypressVersion: "9", 122 | pluginFile: "parallel-preprocessor-babel-config", 123 | specs: "all-tests-passing", 124 | specsResults: [ 125 | { 126 | logBefore: true, 127 | executed: 4, 128 | passed: 4, 129 | failed: 0, 130 | skipped: 0, 131 | }, 132 | { 133 | logBefore: true, 134 | executed: 4, 135 | passed: 4, 136 | failed: 0, 137 | skipped: 0, 138 | }, 139 | { 140 | logBefore: true, 141 | executed: 4, 142 | passed: 4, 143 | failed: 0, 144 | skipped: 0, 145 | }, 146 | ], 147 | env: { 148 | CYPRESS_FAIL_FAST_STRATEGY: "run", 149 | }, 150 | }, 151 | ], 152 | { 153 | afterAll: removeParallelStorage, 154 | }, 155 | ); 156 | -------------------------------------------------------------------------------- /test-e2e/test/strategy.spec.js: -------------------------------------------------------------------------------- 1 | const { runSpecsTests } = require("./support/testsRunner"); 2 | 3 | runSpecsTests("When strategy is spec", { 4 | specs: "describe-disabled-test-enabled", 5 | skipVariants: false, 6 | specsResults: [ 7 | { 8 | logBefore: true, 9 | executed: 4, 10 | passed: 1, 11 | failed: 1, 12 | skipped: 2, 13 | }, 14 | { 15 | logBefore: true, 16 | executed: 4, 17 | passed: 4, 18 | failed: 0, 19 | skipped: 0, 20 | }, 21 | { 22 | logBefore: true, 23 | executed: 3, 24 | passed: 1, 25 | failed: 1, 26 | skipped: 1, 27 | }, 28 | ], 29 | env: { 30 | CYPRESS_FAIL_FAST_STRATEGY: "spec", 31 | }, 32 | }); 33 | 34 | runSpecsTests("When strategy is run", { 35 | specs: "describe-disabled-test-enabled", 36 | skipVariants: true, 37 | specsResults: [ 38 | { 39 | logBefore: true, 40 | executed: 4, 41 | passed: 1, 42 | failed: 1, 43 | skipped: 2, 44 | }, 45 | { 46 | logBefore: false, 47 | executed: 4, 48 | passed: 0, 49 | failed: 0, 50 | skipped: 4, 51 | }, 52 | { 53 | logBefore: false, 54 | executed: 3, 55 | passed: 0, 56 | failed: 0, 57 | skipped: 3, 58 | }, 59 | ], 60 | env: { 61 | CYPRESS_FAIL_FAST_STRATEGY: "run", 62 | }, 63 | }); 64 | 65 | runSpecsTests("When strategy is parallel", { 66 | specs: "describe-disabled-test-enabled", 67 | skipVariants: true, 68 | specsResults: [ 69 | { 70 | logBefore: true, 71 | executed: 4, 72 | passed: 1, 73 | failed: 1, 74 | skipped: 2, 75 | }, 76 | { 77 | logBefore: false, 78 | executed: 4, 79 | passed: 0, 80 | failed: 0, 81 | skipped: 4, 82 | }, 83 | { 84 | logBefore: false, 85 | executed: 3, 86 | passed: 0, 87 | failed: 0, 88 | skipped: 3, 89 | }, 90 | ], 91 | env: { 92 | CYPRESS_FAIL_FAST_STRATEGY: "parallel", 93 | }, 94 | }); 95 | -------------------------------------------------------------------------------- /test-e2e/test/support/logs.js: -------------------------------------------------------------------------------- 1 | const splitLogsBySpec = (logs) => { 2 | return logs.split("Running:"); 3 | }; 4 | 5 | module.exports = { 6 | splitLogsBySpec, 7 | }; 8 | -------------------------------------------------------------------------------- /test-e2e/test/support/npmCommandRunner.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const childProcess = require("child_process"); 3 | const stripAnsi = require("strip-ansi"); 4 | 5 | const ENCODING_TYPE = "utf8"; 6 | const VARIANTS_FOLDER = path.resolve(__dirname, "..", "..", "cypress-variants"); 7 | 8 | const npmRun = (commands, variant, env, { getCode = false } = {}) => { 9 | let npmProcess; 10 | const logs = []; 11 | const logData = (log) => { 12 | const cleanLog = stripAnsi(log.trim()); 13 | if (cleanLog.length) { 14 | if (process.env.DEBUG === "true") { 15 | console.log(cleanLog); 16 | } 17 | logs.push(cleanLog); 18 | } 19 | }; 20 | 21 | return new Promise((resolve) => { 22 | const commandsArray = Array.isArray(commands) ? commands : [commands]; 23 | npmProcess = childProcess.spawn("npm", ["run"].concat(commandsArray), { 24 | cwd: path.resolve(VARIANTS_FOLDER, variant), 25 | env: { 26 | ...process.env, 27 | ...env, 28 | }, 29 | }); 30 | 31 | npmProcess.stdin.setEncoding(ENCODING_TYPE); 32 | npmProcess.stdout.setEncoding(ENCODING_TYPE); 33 | npmProcess.stderr.setEncoding(ENCODING_TYPE); 34 | npmProcess.stdout.on("data", logData); 35 | npmProcess.stderr.on("data", logData); 36 | 37 | npmProcess.on("close", (code) => { 38 | if (getCode) { 39 | resolve(code); 40 | } else { 41 | resolve(logs.join("\n")); 42 | } 43 | }); 44 | }); 45 | }; 46 | 47 | module.exports = { 48 | npmRun, 49 | VARIANTS_FOLDER, 50 | }; 51 | -------------------------------------------------------------------------------- /test-e2e/test/support/testsRunner.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const cypressVariants = require("../../commands/support/variants"); 4 | const { 5 | copyCypressSpecs, 6 | copyCypressPluginFile, 7 | copyCypressConfigFile, 8 | removeCypressConfigFile, 9 | } = require("../../commands/support/copy"); 10 | const { npmRun, VARIANTS_FOLDER } = require("./npmCommandRunner"); 11 | const { splitLogsBySpec } = require("./logs"); 12 | 13 | const AFTER_EVENT_LOG = "Executed test:after:run event in failed test"; 14 | const BEFORE_HOOK_LOG = "Executing before hook"; 15 | const SECOND_BEFORE_HOOK_LOG = "Executing second before hook"; 16 | const BEFORE_EACH_HOOK_LOG = "Executing beforeEach hook"; 17 | 18 | const runOnlyLatest = process.env.TEST_ONLY_LATEST; 19 | 20 | function findCypressVariant(variantVersion) { 21 | if (variantVersion === "latest") { 22 | return cypressVariants.find((variant) => !!variant.isLatest); 23 | } 24 | return cypressVariants.find((variant) => variant.version === variantVersion); 25 | } 26 | 27 | const wait = (time = 1000) => { 28 | return new Promise((resolve) => { 29 | setTimeout(() => { 30 | resolve(); 31 | }, time); 32 | }); 33 | }; 34 | 35 | const readReport = (variantPath) => { 36 | return new Promise((resolve, reject) => { 37 | fs.readFile( 38 | path.resolve(VARIANTS_FOLDER, variantPath, "mochawesome-report", "mochawesome.html"), 39 | "utf8", 40 | (err, data) => { 41 | if (err) { 42 | reject(err); 43 | } else { 44 | resolve(data); 45 | } 46 | }, 47 | ); 48 | }); 49 | }; 50 | 51 | const pluralize = (text, amount) => { 52 | return amount < 2 ? text : `${text}s`; 53 | }; 54 | 55 | const expectTestsAmount = (status, statusKey, amount, getSpecLogs) => { 56 | if (amount !== null) { 57 | it(`should have ${status} ${amount} ${pluralize("test", amount)}`, () => { 58 | expect(getSpecLogs()).toEqual( 59 | expect.stringMatching(new RegExp(`\\s*│\\s*${statusKey}:\\s*${amount}`)), 60 | ); 61 | }); 62 | } 63 | }; 64 | 65 | const expectLogReported = (report, getReport) => { 66 | it(`should have reported "${report}"`, () => { 67 | expect(getReport()).toEqual(expect.stringContaining(report)); 68 | }); 69 | }; 70 | 71 | const expectLogPrinted = (log, getSpecLogs) => { 72 | it(`should have logged "${log}"`, () => { 73 | expect(getSpecLogs()).toEqual(expect.stringContaining(log)); 74 | }); 75 | }; 76 | 77 | const expectLogNotPrinted = (log, getSpecLogs) => { 78 | it(`should have not logged "${log}"`, () => { 79 | expect(getSpecLogs()).toEqual(expect.not.stringContaining(log)); 80 | }); 81 | }; 82 | 83 | const getSpecTests = ( 84 | { 85 | spec = 1, 86 | logBefore = true, 87 | logSecondBefore, 88 | logBeforeEach, 89 | executed = null, 90 | passed = null, 91 | failed = null, 92 | skipped = null, 93 | }, 94 | getLogs, 95 | getReport, 96 | ) => { 97 | const getSpecLogs = () => getLogs(spec); 98 | describe(`Spec ${spec}`, () => { 99 | if (logBefore) { 100 | expectLogPrinted(BEFORE_HOOK_LOG, getSpecLogs); 101 | } else { 102 | expectLogNotPrinted(BEFORE_HOOK_LOG, getSpecLogs); 103 | } 104 | if (logSecondBefore === false) { 105 | expectLogNotPrinted(SECOND_BEFORE_HOOK_LOG, getSpecLogs); 106 | } 107 | if (logBeforeEach === false) { 108 | expectLogNotPrinted(BEFORE_EACH_HOOK_LOG, getSpecLogs); 109 | } 110 | expectTestsAmount("executed", "Tests", executed, getSpecLogs); 111 | expectTestsAmount("passed", "Passing", passed, getSpecLogs); 112 | expectTestsAmount("failed", "Failing", failed, getSpecLogs); 113 | if (failed > 0) { 114 | expectLogReported(AFTER_EVENT_LOG, getReport); 115 | } 116 | expectTestsAmount("skipped", "Skipped", skipped, getSpecLogs); 117 | }); 118 | }; 119 | 120 | const getSpecsStatusesTests = (specsExpectedStatuses) => { 121 | return (getLogs, getReport) => { 122 | specsExpectedStatuses.forEach((specExpectedStatuses, index) => { 123 | getSpecTests({ ...specExpectedStatuses, spec: index + 1 }, getLogs, getReport); 124 | }); 125 | }; 126 | }; 127 | 128 | const getParallelSpecsStatusesTests = (runIndex, specsExpectedStatuses) => { 129 | return (getLogs, getReport) => { 130 | describe(`Run ${runIndex}`, () => { 131 | specsExpectedStatuses.forEach((specExpectedStatuses, index) => { 132 | getSpecTests({ ...specExpectedStatuses, spec: index + 1 }, getLogs, getReport); 133 | }); 134 | }); 135 | }; 136 | }; 137 | 138 | const runVariantTests = (cypressVariant, tests, options = {}) => { 139 | describe(`Executed in ${cypressVariant.name}`, () => { 140 | let logs; 141 | let report; 142 | let tscExitCode; 143 | const getLogs = (specIndex) => logs[specIndex]; 144 | const getReport = () => report; 145 | 146 | beforeAll(async () => { 147 | copyCypressSpecs(options.specs, cypressVariant); 148 | if (cypressVariant.typescript) { 149 | tscExitCode = await npmRun(["tsc"], cypressVariant.path, options.env, { getCode: true }); 150 | } 151 | if (cypressVariant.pluginFile) { 152 | copyCypressPluginFile( 153 | cypressVariant.path, 154 | cypressVariant.typescript, 155 | cypressVariant.pluginFile, 156 | ); 157 | } 158 | logs = splitLogsBySpec(await npmRun(["cypress:run"], cypressVariant.path, options.env)); 159 | await npmRun(["report:create"], cypressVariant.path, options.env); 160 | report = await readReport(cypressVariant.path).catch(() => { 161 | console.warn("Mochawesome report not found"); 162 | return null; 163 | }); 164 | }, 120000); 165 | 166 | if (cypressVariant.typescript) { 167 | describe("TypeScript", () => { 168 | it("should run compiler without errors", () => { 169 | expect(tscExitCode).toEqual(0); 170 | }); 171 | }); 172 | } 173 | 174 | tests(getLogs, getReport); 175 | }); 176 | }; 177 | 178 | const runSpecsTests = (description, options = {}) => { 179 | describe(description, () => { 180 | cypressVariants.forEach((cypressVariant) => { 181 | if ( 182 | (options.skipVariants && cypressVariant.skippable) || 183 | (runOnlyLatest && !cypressVariant.isLatest) || 184 | (!!options.cypressVersion && options.cypressVersion !== cypressVariant.version) 185 | ) { 186 | return; 187 | } 188 | runVariantTests(cypressVariant, getSpecsStatusesTests(options.specsResults), options); 189 | }); 190 | }); 191 | }; 192 | 193 | const waitAndRun = (time, run) => { 194 | if (!time) { 195 | return run(); 196 | } 197 | return wait(time).then(run); 198 | }; 199 | 200 | const runParallelTests = ( 201 | cypressVariant1, 202 | cypressVariant2, 203 | tests1, 204 | tests2, 205 | options1 = {}, 206 | options2 = {}, 207 | commonOptions = {}, 208 | ) => { 209 | describe(`Running in parallel ${cypressVariant1.name}:${options1.specs} and ${cypressVariant2.name}:${options2.specs}`, () => { 210 | let logs1; 211 | let logs2; 212 | let report1; 213 | let report2; 214 | const getLogs1 = (specIndex) => logs1[specIndex]; 215 | const getReport1 = () => report1; 216 | const getLogs2 = (specIndex) => logs2[specIndex]; 217 | const getReport2 = () => report2; 218 | 219 | beforeAll(async () => { 220 | copyCypressSpecs(options1.specs, cypressVariant1); 221 | copyCypressSpecs(options2.specs, cypressVariant2); 222 | if (options1.pluginFile) { 223 | copyCypressPluginFile( 224 | cypressVariant1.path, 225 | cypressVariant1.typescript, 226 | options1.pluginFile, 227 | ); 228 | } else if (cypressVariant1.pluginFile) { 229 | copyCypressPluginFile( 230 | cypressVariant1.path, 231 | cypressVariant1.typescript, 232 | cypressVariant1.pluginFile, 233 | ); 234 | } 235 | if (options2.pluginFile) { 236 | copyCypressPluginFile( 237 | cypressVariant2.path, 238 | cypressVariant2.typescript, 239 | options2.pluginFile, 240 | ); 241 | } else if (cypressVariant2.pluginFile) { 242 | copyCypressPluginFile( 243 | cypressVariant2.path, 244 | cypressVariant2.typescript, 245 | cypressVariant2.pluginFile, 246 | ); 247 | } 248 | if (options1.configFile) { 249 | if (options1.configFileDest) { 250 | removeCypressConfigFile(cypressVariant1.path, cypressVariant1.configFile); 251 | } 252 | copyCypressConfigFile( 253 | cypressVariant1.path, 254 | options1.configFile, 255 | options1.configFileDest || cypressVariant1.configFile, 256 | ); 257 | } 258 | if (options2.configFile) { 259 | if (options2.configFileDest) { 260 | removeCypressConfigFile(cypressVariant2.path, cypressVariant2.configFile); 261 | } 262 | copyCypressConfigFile( 263 | cypressVariant2.path, 264 | options2.configFile, 265 | options2.configFileDest || cypressVariant2.configFile, 266 | ); 267 | } 268 | const logs = await Promise.all([ 269 | waitAndRun(options1.delay, () => 270 | npmRun(["cypress:run"], cypressVariant1.path, options1.env), 271 | ), 272 | waitAndRun(options2.delay, () => 273 | npmRun(["cypress:run"], cypressVariant2.path, options2.env), 274 | ), 275 | ]); 276 | 277 | logs1 = splitLogsBySpec(logs[0]); 278 | logs2 = splitLogsBySpec(logs[1]); 279 | await npmRun(["report:create"], cypressVariant1.path, options1.env); 280 | await npmRun(["report:create"], cypressVariant2.path, options2.env); 281 | report1 = await readReport(cypressVariant1.path); 282 | report2 = await readReport(cypressVariant2.path); 283 | }, 120000); 284 | 285 | afterAll(() => { 286 | if (options1.pluginFile) { 287 | copyCypressPluginFile( 288 | cypressVariant1.path, 289 | cypressVariant1.typescript, 290 | cypressVariant1.pluginFile, 291 | ); 292 | } 293 | if (options2.pluginFile) { 294 | copyCypressPluginFile( 295 | cypressVariant2.path, 296 | cypressVariant2.typescript, 297 | cypressVariant2.pluginFile, 298 | ); 299 | } 300 | if (options1.configFileDest) { 301 | removeCypressConfigFile(cypressVariant1.path, options1.configFileDest); 302 | } 303 | if (options1.configFile) { 304 | copyCypressConfigFile(cypressVariant1.path, cypressVariant1.configFile); 305 | } 306 | 307 | if (options2.configFileDest) { 308 | removeCypressConfigFile(cypressVariant2.path, options2.configFileDest); 309 | } 310 | if (options2.configFile) { 311 | copyCypressConfigFile(cypressVariant2.path, cypressVariant2.configFile); 312 | } 313 | 314 | if (commonOptions.afterAll) { 315 | commonOptions.afterAll(); 316 | } 317 | }); 318 | 319 | tests1(getLogs1, getReport1); 320 | tests2(getLogs2, getReport2); 321 | }); 322 | }; 323 | 324 | const runParallelSpecsTests = (description, runsOptions, options) => { 325 | if (!runOnlyLatest) { 326 | describe(description, () => { 327 | runParallelTests( 328 | findCypressVariant(runsOptions[0].cypressVersion), 329 | findCypressVariant(runsOptions[1].cypressVersion), 330 | getParallelSpecsStatusesTests(1, runsOptions[0].specsResults), 331 | getParallelSpecsStatusesTests(2, runsOptions[1].specsResults), 332 | runsOptions[0], 333 | runsOptions[1], 334 | options, 335 | ); 336 | }); 337 | } 338 | }; 339 | 340 | module.exports = { 341 | runSpecsTests, 342 | runParallelSpecsTests, 343 | }; 344 | -------------------------------------------------------------------------------- /test-e2e/test/test-config.spec.js: -------------------------------------------------------------------------------- 1 | const { runSpecsTests } = require("./support/testsRunner"); 2 | 3 | runSpecsTests("When it is enabled in describe but disabled in test", { 4 | specs: "describe-enabled-test-disabled", 5 | skipVariants: false, 6 | specsResults: [ 7 | { 8 | logBefore: true, 9 | executed: 4, 10 | passed: 3, 11 | failed: 1, 12 | skipped: 0, 13 | }, 14 | { 15 | logBefore: true, 16 | executed: 4, 17 | passed: 4, 18 | failed: 0, 19 | skipped: 0, 20 | }, 21 | { 22 | logBefore: true, 23 | executed: 3, 24 | passed: 1, 25 | failed: 1, 26 | skipped: 1, 27 | }, 28 | ], 29 | }); 30 | 31 | runSpecsTests("When it is disabled in describe but enabled in test", { 32 | specs: "describe-disabled-test-enabled", 33 | skipVariants: false, 34 | specsResults: [ 35 | { 36 | logBefore: true, 37 | executed: 4, 38 | passed: 1, 39 | failed: 1, 40 | skipped: 2, 41 | }, 42 | { 43 | logBefore: false, 44 | executed: 4, 45 | passed: 0, 46 | failed: 0, 47 | skipped: 4, 48 | }, 49 | { 50 | logBefore: false, 51 | executed: 3, 52 | passed: 0, 53 | failed: 0, 54 | skipped: 3, 55 | }, 56 | ], 57 | }); 58 | 59 | runSpecsTests("When it is disabled in environment, disabled in describe and enabled in test", { 60 | specs: "describe-disabled-test-enabled", 61 | skipVariants: true, 62 | specsResults: [ 63 | { 64 | logBefore: true, 65 | executed: 4, 66 | passed: 1, 67 | failed: 1, 68 | skipped: 2, 69 | }, 70 | { 71 | logBefore: false, 72 | executed: 4, 73 | passed: 0, 74 | failed: 0, 75 | skipped: 4, 76 | }, 77 | { 78 | logBefore: false, 79 | executed: 3, 80 | passed: 0, 81 | failed: 0, 82 | skipped: 3, 83 | }, 84 | ], 85 | env: { 86 | CYPRESS_FAIL_FAST_ENABLED: "false", 87 | }, 88 | }); 89 | 90 | runSpecsTests( 91 | "When it is disabled in describe, enabled in test but plugin is disabled in environment", 92 | { 93 | specs: "describe-disabled-test-enabled", 94 | skipVariants: true, 95 | specsResults: [ 96 | { 97 | logBefore: true, 98 | executed: 4, 99 | passed: 3, 100 | failed: 1, 101 | skipped: 0, 102 | }, 103 | { 104 | logBefore: true, 105 | executed: 4, 106 | passed: 4, 107 | failed: 0, 108 | skipped: 0, 109 | }, 110 | { 111 | logBefore: true, 112 | executed: 3, 113 | passed: 2, 114 | failed: 1, 115 | skipped: 0, 116 | }, 117 | ], 118 | env: { 119 | CYPRESS_FAIL_FAST_PLUGIN: "false", 120 | }, 121 | }, 122 | ); 123 | 124 | runSpecsTests( 125 | "When it is disabled in environment but enabled in configuration in grandparent suites", 126 | { 127 | specs: "grandparent-describe-enabled", 128 | skipVariants: true, 129 | specsResults: [ 130 | { 131 | logBefore: true, 132 | executed: 4, 133 | passed: 3, 134 | failed: 1, 135 | skipped: 0, 136 | }, 137 | { 138 | logBefore: true, 139 | executed: 4, 140 | passed: 1, 141 | failed: 2, 142 | skipped: 1, 143 | }, 144 | { 145 | logBefore: false, 146 | executed: 3, 147 | passed: 0, 148 | failed: 0, 149 | skipped: 3, 150 | }, 151 | ], 152 | env: { 153 | CYPRESS_FAIL_FAST_ENABLED: "false", 154 | }, 155 | }, 156 | ); 157 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "jest": true, 4 | "afterEach": true, 5 | "afterAll": true, 6 | "beforeAll": true, 7 | "beforeEach": true, 8 | "describe": true, 9 | "expect": true, 10 | "it": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/config.spec.js: -------------------------------------------------------------------------------- 1 | const { isTruthy, isFalsy } = require("../src/helpers/config"); 2 | 3 | describe("config", () => { 4 | describe("isTruthy", () => { 5 | it("should return true when value is true as boolean", () => { 6 | expect(isTruthy(true)).toEqual(true); 7 | }); 8 | 9 | it("should return true when value is true as string", () => { 10 | expect(isTruthy("true")).toEqual(true); 11 | }); 12 | 13 | it("should return true when value is 1", () => { 14 | expect(isTruthy(1)).toEqual(true); 15 | }); 16 | 17 | it("should return true when value is 1 as string", () => { 18 | expect(isTruthy("1")).toEqual(true); 19 | }); 20 | 21 | it("should return false when value is false as boolean", () => { 22 | expect(isTruthy(false)).toEqual(false); 23 | }); 24 | 25 | it("should return false when value is false as string", () => { 26 | expect(isTruthy("false")).toEqual(false); 27 | }); 28 | 29 | it("should return false when value is 0", () => { 30 | expect(isTruthy(0)).toEqual(false); 31 | }); 32 | 33 | it("should return false when value is 0 as string", () => { 34 | expect(isTruthy("0")).toEqual(false); 35 | }); 36 | 37 | it("should return false when value is undefined", () => { 38 | expect(isTruthy()).toEqual(false); 39 | }); 40 | 41 | it("should return false when value is any other value", () => { 42 | expect(isTruthy("foo")).toEqual(false); 43 | }); 44 | }); 45 | 46 | describe("isFalsy", () => { 47 | it("should return true when value is false as boolean", () => { 48 | expect(isFalsy(false)).toEqual(true); 49 | }); 50 | 51 | it("should return true when value is false as string", () => { 52 | expect(isFalsy("false")).toEqual(true); 53 | }); 54 | 55 | it("should return true when value is 0", () => { 56 | expect(isFalsy(0)).toEqual(true); 57 | }); 58 | 59 | it("should return true when value is 0 as string", () => { 60 | expect(isFalsy("0")).toEqual(true); 61 | }); 62 | 63 | it("should return false when value is true as boolean", () => { 64 | expect(isFalsy(true)).toEqual(false); 65 | }); 66 | 67 | it("should return false when value is true as string", () => { 68 | expect(isFalsy("true")).toEqual(false); 69 | }); 70 | 71 | it("should return false when value is 1", () => { 72 | expect(isFalsy(1)).toEqual(false); 73 | }); 74 | 75 | it("should return false when value is 1 as string", () => { 76 | expect(isFalsy("1")).toEqual(false); 77 | }); 78 | 79 | it("should return false when value is undefined", () => { 80 | expect(isFalsy()).toEqual(false); 81 | }); 82 | 83 | it("should return false when value is any other value", () => { 84 | expect(isFalsy("foo")).toEqual(false); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/plugin.spec.js: -------------------------------------------------------------------------------- 1 | const sinon = require("sinon"); 2 | const chalk = require("chalk"); 3 | const plugin = require("../src/plugin"); 4 | 5 | describe("plugin", () => { 6 | let onEventSpy; 7 | let sandbox; 8 | let eventName; 9 | let failFastShouldSkip; 10 | let failFastResetSkip; 11 | let failFastLog; 12 | let failFastFailedTests; 13 | let failFastResetFailedTests; 14 | 15 | const getPluginMethods = (config, env) => { 16 | plugin(onEventSpy, { env: env || {} }, config); 17 | eventName = onEventSpy.getCall(0).args[0]; 18 | failFastShouldSkip = onEventSpy.getCall(0).args[1].failFastShouldSkip; 19 | failFastResetSkip = onEventSpy.getCall(0).args[1].failFastResetSkip; 20 | failFastFailedTests = onEventSpy.getCall(0).args[1].failFastFailedTests; 21 | failFastResetFailedTests = onEventSpy.getCall(0).args[1].failFastResetFailedTests; 22 | failFastLog = onEventSpy.getCall(0).args[1].failFastLog; 23 | }; 24 | 25 | beforeEach(() => { 26 | sandbox = sinon.createSandbox(); 27 | onEventSpy = sandbox.spy(); 28 | }); 29 | 30 | afterEach(() => { 31 | sandbox.restore(); 32 | }); 33 | 34 | describe("when initialized", () => { 35 | it("should register plugin methods for the task event", () => { 36 | getPluginMethods(); 37 | expect(eventName).toEqual("task"); 38 | }); 39 | }); 40 | 41 | describe("shouldSkip task", () => { 42 | describe("when plugin is disabled", () => { 43 | it("should return shouldSkip flag as false by default", () => { 44 | getPluginMethods(); 45 | expect(failFastShouldSkip()).toEqual(false); 46 | }); 47 | 48 | it("should return shouldSkip flag as true after setting it as true", () => { 49 | getPluginMethods(); 50 | failFastShouldSkip(true); 51 | expect(failFastShouldSkip()).toEqual(true); 52 | }); 53 | }); 54 | 55 | describe("when plugin is enabled using boolean", () => { 56 | it("should return shouldSkip flag as false by default", () => { 57 | getPluginMethods(); 58 | expect(failFastShouldSkip()).toEqual(false); 59 | }); 60 | 61 | it("should return shouldSkip flag as true after setting it as true", () => { 62 | getPluginMethods(); 63 | failFastShouldSkip(true); 64 | expect(failFastShouldSkip()).toEqual(true); 65 | }); 66 | 67 | it("should reset shouldSkip flag after calling to reset method", () => { 68 | getPluginMethods(); 69 | failFastShouldSkip(true); 70 | expect(failFastShouldSkip()).toEqual(true); 71 | failFastResetSkip(); 72 | expect(failFastShouldSkip()).toEqual(false); 73 | }); 74 | }); 75 | 76 | describe("when plugin is enabled using string", () => { 77 | it("should return shouldSkip flag as false by default", () => { 78 | getPluginMethods(); 79 | expect(failFastShouldSkip()).toEqual(false); 80 | }); 81 | 82 | it("should return shouldSkip flag as true after setting it as true", () => { 83 | getPluginMethods(); 84 | failFastShouldSkip(true); 85 | expect(failFastShouldSkip()).toEqual(true); 86 | }); 87 | 88 | it("should reset shouldSkip flag after calling to reset method", () => { 89 | getPluginMethods(); 90 | failFastShouldSkip(true); 91 | expect(failFastShouldSkip()).toEqual(true); 92 | failFastResetSkip(); 93 | expect(failFastShouldSkip()).toEqual(false); 94 | }); 95 | }); 96 | 97 | describe("when parallel callbacks are provided", () => { 98 | describe("when strategy is parallel", () => { 99 | const env = { 100 | FAIL_FAST_STRATEGY: "parallel", 101 | }; 102 | 103 | it("should return shouldSkip flag as true if flag is false but isCancelled callback returns true", () => { 104 | getPluginMethods( 105 | { 106 | parallelCallbacks: { 107 | isCancelled: () => true, 108 | }, 109 | }, 110 | env, 111 | ); 112 | expect(failFastShouldSkip()).toEqual(true); 113 | }); 114 | 115 | it("should return shouldSkip flag as false if flag is false and isCancelled callback does not return value", () => { 116 | getPluginMethods( 117 | { 118 | parallelCallbacks: { 119 | isCancelled: () => null, 120 | }, 121 | }, 122 | env, 123 | ); 124 | expect(failFastShouldSkip()).toEqual(false); 125 | }); 126 | 127 | it("should return shouldSkip flag as true if flag is true even when isCancelled callback returns false", () => { 128 | getPluginMethods( 129 | { 130 | parallelCallbacks: { 131 | isCancelled: () => false, 132 | }, 133 | }, 134 | env, 135 | ); 136 | failFastShouldSkip(true); 137 | expect(failFastShouldSkip()).toEqual(true); 138 | }); 139 | 140 | it("should call to onCancel callback when shouldSkip flag is set to true", () => { 141 | const spy = sandbox.spy(); 142 | getPluginMethods( 143 | { 144 | parallelCallbacks: { 145 | onCancel: spy, 146 | }, 147 | }, 148 | env, 149 | ); 150 | failFastShouldSkip(true); 151 | expect(spy.callCount).toEqual(1); 152 | }); 153 | 154 | it("should not call to onCancel callback when shouldSkip flag is set with a value different to true", () => { 155 | const spy = sandbox.spy(); 156 | getPluginMethods( 157 | { 158 | parallelCallbacks: { 159 | onCancel: spy, 160 | }, 161 | }, 162 | env, 163 | ); 164 | failFastShouldSkip(); 165 | failFastShouldSkip(false); 166 | expect(spy.callCount).toEqual(0); 167 | }); 168 | }); 169 | 170 | describe("when strategy is not parallel", () => { 171 | const env = { 172 | FAIL_FAST_STRATEGY: "run", 173 | }; 174 | 175 | it("should return shouldSkip flag as false if flag is false but isCancelled callback returns true", () => { 176 | getPluginMethods( 177 | { 178 | parallelCallbacks: { 179 | isCancelled: () => true, 180 | }, 181 | }, 182 | env, 183 | ); 184 | expect(failFastShouldSkip()).toEqual(false); 185 | }); 186 | 187 | it("should not call to isCancelled callback", () => { 188 | const spy = sandbox.spy(); 189 | getPluginMethods( 190 | { 191 | parallelCallbacks: { 192 | isCancelled: spy, 193 | }, 194 | }, 195 | env, 196 | ); 197 | failFastShouldSkip(); 198 | expect(spy.callCount).toEqual(0); 199 | }); 200 | }); 201 | }); 202 | }); 203 | 204 | describe("failedTests task", () => { 205 | it("should return 0 failed tests when initialized", () => { 206 | getPluginMethods(); 207 | expect(failFastFailedTests()).toEqual(0); 208 | }); 209 | 210 | it("should increase amount of failed tests when called with true", () => { 211 | getPluginMethods(); 212 | failFastFailedTests(true); 213 | failFastFailedTests(true); 214 | expect(failFastFailedTests()).toEqual(2); 215 | }); 216 | 217 | it("should return 0 failed tests after calling to resetFailedTests task", () => { 218 | getPluginMethods(); 219 | failFastFailedTests(true); 220 | failFastFailedTests(true); 221 | failFastResetFailedTests(); 222 | expect(failFastFailedTests()).toEqual(0); 223 | }); 224 | }); 225 | 226 | describe("log task", () => { 227 | it("should call to console.log adding the plugin name", () => { 228 | const MESSAGE = "Foo message"; 229 | getPluginMethods(); 230 | sandbox.spy(console, "log"); 231 | failFastLog(MESSAGE); 232 | expect(console.log.getCall(0).args[0]).toEqual(`${chalk.yellow("[fail-fast]")} ${MESSAGE}`); 233 | }); 234 | 235 | it("should return null", () => { 236 | getPluginMethods(); 237 | expect(failFastLog("foo")).toEqual(null); 238 | }); 239 | }); 240 | }); 241 | -------------------------------------------------------------------------------- /test/support.spec.js: -------------------------------------------------------------------------------- 1 | const sinon = require("sinon"); 2 | const support = require("../src/support"); 3 | const { setHookFailedError, setForceErrorOnFailedHook } = require("../src/helpers/cypress"); 4 | 5 | const wait = (time) => { 6 | return new Promise((resolve) => { 7 | setTimeout(() => { 8 | resolve(); 9 | }, time); 10 | }); 11 | }; 12 | 13 | describe("support", () => { 14 | let sandbox; 15 | let beforeEachCallback; 16 | let afterEachCallback; 17 | let beforeCallback; 18 | let getCurrentTest; 19 | let Cypress; 20 | let CypressOnRunnableRun; 21 | let cy; 22 | 23 | const getSupportCallbacks = (options = {}) => { 24 | sandbox = sinon.createSandbox(); 25 | CypressOnRunnableRun = sandbox.spy(); 26 | Cypress = { 27 | browser: { 28 | isHeaded: options.browserIsHeaded, 29 | }, 30 | env: (envKey) => { 31 | if (envKey === "FAIL_FAST_PLUGIN") { 32 | return options.pluginEnabled; 33 | } 34 | if (envKey === "FAIL_FAST_ENABLED") { 35 | return options.enabled; 36 | } 37 | if (envKey === "FAIL_FAST_STRATEGY") { 38 | return options.strategy; 39 | } 40 | if (envKey === "FAIL_FAST_BAIL") { 41 | return options.bail; 42 | } 43 | }, 44 | runner: { 45 | stop: sandbox.spy(), 46 | onRunnableRun: CypressOnRunnableRun, 47 | }, 48 | }; 49 | if (options.removeCypressBrowser) { 50 | delete Cypress.browser; 51 | } 52 | cy = { 53 | task: sandbox.spy((taskName) => { 54 | if (taskName === "failFastShouldSkip") { 55 | return Promise.resolve(options.shouldSkip); 56 | } else if (taskName === "failFastFailedTests") { 57 | return Promise.resolve(options.failedTests || 1); 58 | } 59 | return Promise.resolve(null); 60 | }), 61 | }; 62 | const currentTest = options.disableCurrentTest 63 | ? {} 64 | : { 65 | currentTest: { 66 | state: options.testState, 67 | currentRetry: () => options.testCurrentRetry, 68 | retries: () => options.testRetries, 69 | // Private property storing test config in Cypress <6.6 70 | cfg: options.customConfig, 71 | parent: options.testParent, 72 | }, 73 | }; 74 | 75 | // Private property storing test config in Cypress >6.6 76 | if (currentTest.currentTest && options.customConfigCypress7) { 77 | currentTest.currentTest.ctx = { 78 | test: { 79 | _testConfig: options.customConfigCypress7, 80 | }, 81 | }; 82 | } 83 | 84 | // Private property storing test config in Cypress >9.0 85 | if (currentTest.currentTest && options.customConfigCypress9) { 86 | currentTest.currentTest.ctx = { 87 | test: { 88 | _testConfig: { 89 | testConfigList: [{}, { overrides: options.customConfigCypress9 }], 90 | }, 91 | }, 92 | }; 93 | } 94 | 95 | getCurrentTest = () => currentTest; 96 | 97 | const beforeEachMethod = (callback) => { 98 | beforeEachCallback = callback.bind(currentTest); 99 | }; 100 | const afterEachMethod = (callback) => { 101 | afterEachCallback = callback.bind(currentTest); 102 | }; 103 | const beforeMethod = (callback) => { 104 | beforeCallback = callback.bind(currentTest); 105 | }; 106 | support(Cypress, cy, beforeEachMethod, afterEachMethod, beforeMethod); 107 | }; 108 | 109 | beforeEach(() => { 110 | sandbox = sinon.createSandbox(); 111 | }); 112 | 113 | afterEach(() => { 114 | sandbox.restore(); 115 | }); 116 | 117 | const testPluginDisabled = (extraDescription, config) => { 118 | describe(extraDescription, () => { 119 | describe("beforeEach callback", () => { 120 | it("should not call to any plugin task", () => { 121 | getSupportCallbacks(config); 122 | beforeEachCallback(); 123 | expect(cy.task.callCount).toEqual(0); 124 | }); 125 | }); 126 | 127 | describe("afterEach callback", () => { 128 | it("should not call to any plugin task when test fails", () => { 129 | getSupportCallbacks({ 130 | ...config, 131 | testState: "failed", 132 | testCurrentRetry: 3, 133 | testRetries: 3, 134 | }); 135 | afterEachCallback(); 136 | expect(cy.task.callCount).toEqual(0); 137 | }); 138 | 139 | it("should not call to stop runner nor set plugin flag if current test config is enabled", () => { 140 | getSupportCallbacks({ 141 | ...config, 142 | testState: "failed", 143 | testCurrentRetry: 3, 144 | testRetries: 3, 145 | customConfig: { 146 | failFast: { 147 | enabled: true, 148 | }, 149 | }, 150 | }); 151 | afterEachCallback(); 152 | expect(Cypress.runner.stop.callCount).toEqual(0); 153 | expect(cy.task.callCount).toEqual(0); 154 | }); 155 | }); 156 | 157 | describe("before callback", () => { 158 | it("should not call to any plugin task", () => { 159 | getSupportCallbacks(config); 160 | beforeCallback(); 161 | expect(cy.task.callCount).toEqual(0); 162 | }); 163 | }); 164 | 165 | describe("Cypress onRunnableRun method", () => { 166 | it("should not be wrapped", () => { 167 | getSupportCallbacks(config); 168 | expect(Cypress.runner.onRunnableRun).toBe(CypressOnRunnableRun); 169 | }); 170 | }); 171 | }); 172 | }; 173 | 174 | const testPluginAndFailFastEnabled = (extraDescription, config) => { 175 | describe(extraDescription, () => { 176 | describe("beforeEach callback", () => { 177 | it("should call stop runner if failFastShouldSkip returns true", async () => { 178 | getSupportCallbacks({ 179 | ...config, 180 | shouldSkip: true, 181 | }); 182 | beforeEachCallback(); 183 | await wait(200); 184 | expect(Cypress.runner.stop.callCount).toEqual(1); 185 | }); 186 | 187 | it("should set currentTest as pending if failFastShouldSkip returns true", async () => { 188 | getSupportCallbacks({ 189 | ...config, 190 | shouldSkip: true, 191 | }); 192 | beforeEachCallback(); 193 | await wait(200); 194 | expect(getCurrentTest().currentTest.pending).toEqual(true); 195 | }); 196 | 197 | it("should not log the task when setting flag to true", async () => { 198 | getSupportCallbacks({ 199 | ...config, 200 | shouldSkip: true, 201 | }); 202 | beforeEachCallback(); 203 | await wait(200); 204 | expect(cy.task.calledWith("failFastShouldSkip", null, { log: false })).toEqual(true); 205 | }); 206 | 207 | it("should not call stop runner if failFastShouldSkip returns false", async () => { 208 | getSupportCallbacks({ 209 | ...config, 210 | shouldSkip: false, 211 | }); 212 | beforeEachCallback(); 213 | await wait(200); 214 | expect(Cypress.runner.stop.callCount).toEqual(0); 215 | }); 216 | }); 217 | 218 | describe("afterEach callback", () => { 219 | it("should set plugin flag and do not stop runner if current test state is failed and it is last retry", async () => { 220 | getSupportCallbacks({ 221 | ...config, 222 | shouldSkip: true, 223 | testState: "failed", 224 | testCurrentRetry: 3, 225 | testRetries: 3, 226 | }); 227 | afterEachCallback(); 228 | await wait(200); 229 | expect(Cypress.runner.stop.callCount).toEqual(0); 230 | expect(cy.task.calledWith("failFastShouldSkip", true)).toEqual(true); 231 | }); 232 | 233 | it("should not set plugin flag if current test state is not failed", async () => { 234 | getSupportCallbacks({ 235 | ...config, 236 | shouldSkip: true, 237 | testState: "passed", 238 | testCurrentRetry: 3, 239 | testRetries: 3, 240 | }); 241 | afterEachCallback(); 242 | await wait(200); 243 | expect(cy.task.callCount).toEqual(0); 244 | }); 245 | 246 | it("should set plugin flag if current test state is not failed but the hookFailed flag is set", async () => { 247 | const hookError = new Error("foo error message"); 248 | setForceErrorOnFailedHook(false); 249 | setHookFailedError(hookError); 250 | 251 | getSupportCallbacks({ 252 | ...config, 253 | shouldSkip: true, 254 | testState: "passed", 255 | testCurrentRetry: 3, 256 | testRetries: 3, 257 | }); 258 | afterEachCallback(); 259 | await wait(500); 260 | expect(Cypress.runner.stop.callCount).toEqual(0); 261 | expect(cy.task.calledWith("failFastShouldSkip", true)).toEqual(true); 262 | }); 263 | 264 | it("should not set plugin flag if current test retry is not the last one", async () => { 265 | getSupportCallbacks({ 266 | ...config, 267 | shouldSkip: true, 268 | testState: "failed", 269 | testCurrentRetry: 3, 270 | testRetries: 4, 271 | }); 272 | afterEachCallback(); 273 | await wait(200); 274 | expect(cy.task.callCount).toEqual(0); 275 | }); 276 | 277 | it("should not set plugin flag if test.currentTest is not found", async () => { 278 | getSupportCallbacks({ 279 | ...config, 280 | shouldSkip: true, 281 | disableCurrentTest: true, 282 | }); 283 | afterEachCallback(); 284 | await wait(200); 285 | expect(cy.task.callCount).toEqual(0); 286 | }); 287 | }); 288 | 289 | describe("before callback", () => { 290 | it("should not call to reset plugin if browser is not headed", async () => { 291 | getSupportCallbacks({ 292 | ...config, 293 | browserIsHeaded: false, 294 | }); 295 | beforeCallback(); 296 | await wait(200); 297 | expect(cy.task.calledWith("failFastResetSkip")).toEqual(false); 298 | }); 299 | 300 | it("should not call to reset plugin if Cypress browser does not exists", async () => { 301 | getSupportCallbacks({ 302 | ...config, 303 | removeCypressBrowser: true, 304 | }); 305 | beforeCallback(); 306 | await wait(200); 307 | expect(cy.task.calledWith("failFastResetSkip")).toEqual(false); 308 | }); 309 | 310 | it("should call to reset plugin if browser is headed", async () => { 311 | getSupportCallbacks({ 312 | ...config, 313 | browserIsHeaded: true, 314 | }); 315 | beforeCallback(); 316 | await wait(200); 317 | expect(cy.task.calledWith("failFastResetSkip")).toEqual(true); 318 | }); 319 | 320 | it("should call to reset plugin if strategy is spec", async () => { 321 | getSupportCallbacks({ 322 | ...config, 323 | strategy: "spec", 324 | }); 325 | beforeCallback(); 326 | await wait(200); 327 | expect(cy.task.calledWith("failFastResetSkip")).toEqual(true); 328 | }); 329 | 330 | it("should not log the task when resetting the plugin flag", async () => { 331 | getSupportCallbacks({ 332 | ...config, 333 | browserIsHeaded: true, 334 | }); 335 | beforeCallback(); 336 | await wait(200); 337 | expect(cy.task.getCall(0).args[1]).toEqual(null); 338 | expect(cy.task.getCall(0).args[2]).toEqual({ log: false }); 339 | }); 340 | 341 | it("should call to stop runner if browser is not headed and should skip", async () => { 342 | getSupportCallbacks({ 343 | ...config, 344 | browserIsHeaded: false, 345 | shouldSkip: true, 346 | }); 347 | beforeCallback(); 348 | await wait(200); 349 | expect(Cypress.runner.stop.callCount).toEqual(1); 350 | }); 351 | 352 | it("should not log the task when checking if has to skip", async () => { 353 | getSupportCallbacks({ 354 | ...config, 355 | browserIsHeaded: false, 356 | shouldSkip: true, 357 | }); 358 | beforeCallback(); 359 | await wait(200); 360 | expect(cy.task.getCall(0).args[1]).toEqual(null); 361 | expect(cy.task.getCall(0).args[2]).toEqual({ log: false }); 362 | }); 363 | 364 | it("should not call to stop runner if browser is not headed and should not skip", async () => { 365 | getSupportCallbacks({ 366 | ...config, 367 | browserIsHeaded: false, 368 | shouldSkip: false, 369 | }); 370 | beforeCallback(); 371 | await wait(200); 372 | expect(Cypress.runner.stop.callCount).toEqual(0); 373 | }); 374 | }); 375 | 376 | describe("Cypress onRunnableRun method", () => { 377 | let runnableRun, runnable, args, firstArg; 378 | beforeEach(() => { 379 | firstArg = sandbox.spy(); 380 | runnableRun = sandbox.spy(); 381 | runnable = {}; 382 | args = [firstArg]; 383 | }); 384 | 385 | it("should be wrapped", () => { 386 | getSupportCallbacks(config); 387 | expect(Cypress.runner.onRunnableRun).not.toBe(CypressOnRunnableRun); 388 | }); 389 | 390 | it("should be called with original arguments if runnable is not a hook", () => { 391 | getSupportCallbacks(config); 392 | Cypress.runner.onRunnableRun(runnableRun, runnable, args); 393 | expect(CypressOnRunnableRun.getCall(0).args).toEqual([runnableRun, runnable, args]); 394 | expect(CypressOnRunnableRun.getCall(0).args[2][0]).toBe(firstArg); 395 | }); 396 | 397 | it("should be called with a wrapped first arg if runnable is a before hook", () => { 398 | getSupportCallbacks(config); 399 | runnable.type = "hook"; 400 | runnable.hookName = "before"; 401 | Cypress.runner.onRunnableRun(runnableRun, runnable, args); 402 | expect(CypressOnRunnableRun.getCall(0).args[2][0]).not.toBe(firstArg); 403 | }); 404 | 405 | it("should be called with a wrapped first arg if runnable is a beforeEach hook", () => { 406 | getSupportCallbacks(config); 407 | runnable.type = "hook"; 408 | runnable.hookName = "beforeEach"; 409 | Cypress.runner.onRunnableRun(runnableRun, runnable, args); 410 | expect(CypressOnRunnableRun.getCall(0).args[2][0]).not.toBe(firstArg); 411 | }); 412 | 413 | it("should call to original first arg", () => { 414 | getSupportCallbacks(config); 415 | runnable.type = "hook"; 416 | runnable.hookName = "beforeEach"; 417 | Cypress.runner.onRunnableRun(runnableRun, runnable, args); 418 | CypressOnRunnableRun.getCall(0).args[2][0](); 419 | expect(firstArg.callCount).toEqual(1); 420 | }); 421 | 422 | it("should call to original first arg without error even when it is received", () => { 423 | getSupportCallbacks(config); 424 | runnable.type = "hook"; 425 | runnable.hookName = "beforeEach"; 426 | Cypress.runner.onRunnableRun(runnableRun, runnable, args); 427 | CypressOnRunnableRun.getCall(0).args[2][0](new Error()); 428 | expect(firstArg.getCall(0).args[1]).toBe(undefined); 429 | }); 430 | 431 | it("next before hook after the hook should be skipped", async () => { 432 | getSupportCallbacks(config); 433 | const hookError = new Error("foo error message"); 434 | runnable.type = "hook"; 435 | runnable.hookName = "beforeEach"; 436 | Cypress.runner.onRunnableRun(runnableRun, runnable, args); 437 | CypressOnRunnableRun.getCall(0).args[2][0](hookError); 438 | runnable = { 439 | type: "hook", 440 | hookName: "beforeEach", 441 | }; 442 | 443 | const testCallbackSpy = () => "foo"; 444 | expect(Cypress.runner.onRunnableRun(runnableRun, runnable, [testCallbackSpy])).toEqual( 445 | "foo", 446 | ); 447 | }); 448 | 449 | it("next test after the hook should be forced to fail with the hook error", async () => { 450 | getSupportCallbacks(config); 451 | const hookError = new Error("foo error message"); 452 | runnable.type = "hook"; 453 | runnable.hookName = "beforeEach"; 454 | Cypress.runner.onRunnableRun(runnableRun, runnable, args); 455 | CypressOnRunnableRun.getCall(0).args[2][0](hookError); 456 | runnable = { 457 | type: "it", 458 | }; 459 | 460 | const testCallbackSpy = sandbox.spy(); 461 | Cypress.runner.onRunnableRun(runnableRun, runnable, [testCallbackSpy]); 462 | CypressOnRunnableRun.getCall(1).args[2][0](); 463 | const testCallBackArgument = testCallbackSpy.getCall(0).args[0]; 464 | expect(testCallBackArgument).toBe(hookError); 465 | expect(testCallBackArgument.message).toEqual( 466 | '"beforeEach" hook failed: foo error message', 467 | ); 468 | }); 469 | }); 470 | }); 471 | }; 472 | 473 | const afterEachShouldSetPluginFlag = (extraDescription, config) => { 474 | describe(extraDescription, () => { 475 | it("should set plugin flag", async () => { 476 | getSupportCallbacks({ 477 | ...config, 478 | testState: "failed", 479 | testCurrentRetry: 3, 480 | testRetries: 3, 481 | }); 482 | afterEachCallback(); 483 | await wait(200); 484 | expect(cy.task.calledWith("failFastShouldSkip", true)).toEqual(true); 485 | }); 486 | }); 487 | }; 488 | 489 | const afterEachShouldNotSetPluginFlag = (extraDescription, config) => { 490 | describe(extraDescription, () => { 491 | it("should not set plugin flag", async () => { 492 | getSupportCallbacks({ 493 | ...config, 494 | testState: "failed", 495 | testCurrentRetry: 3, 496 | testRetries: 3, 497 | }); 498 | afterEachCallback(); 499 | await wait(200); 500 | expect(cy.task.calledWith("failFastShouldSkip", true)).toEqual(false); 501 | }); 502 | }); 503 | }; 504 | 505 | describe("when plugin is disabled", () => { 506 | testPluginDisabled("with false as string", { 507 | enabled: true, 508 | pluginEnabled: "false", 509 | }); 510 | 511 | testPluginDisabled("with false as boolean", { 512 | enabled: true, 513 | pluginEnabled: false, 514 | }); 515 | 516 | testPluginDisabled("with 0", { 517 | enabled: true, 518 | pluginEnabled: 0, 519 | }); 520 | 521 | testPluginDisabled("with 0 string", { 522 | enabled: true, 523 | pluginEnabled: "0", 524 | }); 525 | }); 526 | 527 | describe("when plugin and failFast are enabled", () => { 528 | testPluginAndFailFastEnabled("by default", {}); 529 | 530 | testPluginAndFailFastEnabled("using environment vars", { 531 | enabled: true, 532 | pluginEnabled: true, 533 | }); 534 | 535 | testPluginAndFailFastEnabled("using environment vars as strings", { 536 | enabled: "true", 537 | pluginEnabled: "true", 538 | }); 539 | 540 | testPluginAndFailFastEnabled("using environment vars as numbers", { 541 | enabled: 1, 542 | pluginEnabled: 1, 543 | }); 544 | }); 545 | 546 | describe("afterEach callback", () => { 547 | describe("when plugin is enabled and config is enabled", () => { 548 | afterEachShouldSetPluginFlag("by default", {}); 549 | 550 | afterEachShouldSetPluginFlag("in environment variable as boolean", { 551 | enabled: true, 552 | }); 553 | 554 | afterEachShouldSetPluginFlag("in environment variable as string", { 555 | enabled: "true", 556 | }); 557 | 558 | afterEachShouldSetPluginFlag("in environment variable as number", { 559 | enabled: 1, 560 | }); 561 | 562 | afterEachShouldSetPluginFlag("in environment variable as number string", { 563 | enabled: "1", 564 | }); 565 | 566 | afterEachShouldSetPluginFlag("in test but disabled in grandparent", { 567 | customConfig: { 568 | failFast: { 569 | enabled: true, 570 | }, 571 | }, 572 | testParent: { 573 | cfg: { 574 | failFast: { 575 | enabled: false, 576 | }, 577 | }, 578 | }, 579 | }); 580 | 581 | afterEachShouldSetPluginFlag("in test but disabled in environment", { 582 | enabled: false, 583 | customConfig: { 584 | failFast: { 585 | enabled: true, 586 | }, 587 | }, 588 | }); 589 | }); 590 | 591 | describe("when plugin is enabled and config is disabled", () => { 592 | afterEachShouldNotSetPluginFlag("in environment variable", { 593 | enabled: false, 594 | }); 595 | 596 | afterEachShouldNotSetPluginFlag("in environment variable as string", { 597 | enabled: "false", 598 | }); 599 | 600 | afterEachShouldNotSetPluginFlag("in environment variable as number", { 601 | enabled: 0, 602 | }); 603 | 604 | afterEachShouldNotSetPluginFlag("in environment variable as number string", { 605 | enabled: "0", 606 | }); 607 | 608 | afterEachShouldNotSetPluginFlag("in current test", { 609 | customConfig: { 610 | failFast: { 611 | enabled: false, 612 | }, 613 | }, 614 | }); 615 | 616 | afterEachShouldNotSetPluginFlag("in current test in Cypress >6.6", { 617 | customConfigCypress7: { 618 | failFast: { 619 | enabled: false, 620 | }, 621 | }, 622 | }); 623 | 624 | afterEachShouldNotSetPluginFlag("in current test in Cypress >9.0", { 625 | customConfigCypress9: { 626 | failFast: { 627 | enabled: false, 628 | }, 629 | }, 630 | }); 631 | 632 | afterEachShouldNotSetPluginFlag("in current test but enabled in environment", { 633 | enabled: true, 634 | customConfig: { 635 | failFast: { 636 | enabled: false, 637 | }, 638 | }, 639 | }); 640 | 641 | afterEachShouldNotSetPluginFlag("in parent test", { 642 | testParent: { 643 | cfg: { 644 | failFast: { 645 | enabled: false, 646 | }, 647 | }, 648 | }, 649 | }); 650 | 651 | afterEachShouldNotSetPluginFlag("in parent test but enabled in environment", { 652 | enabled: true, 653 | testParent: { 654 | cfg: { 655 | failFast: { 656 | enabled: false, 657 | }, 658 | }, 659 | }, 660 | }); 661 | 662 | afterEachShouldNotSetPluginFlag("in grandparent test", { 663 | testParent: { 664 | parent: { 665 | parent: { 666 | cfg: { 667 | failFast: { 668 | enabled: false, 669 | }, 670 | }, 671 | }, 672 | }, 673 | }, 674 | }); 675 | 676 | afterEachShouldNotSetPluginFlag("in grandparent test but enabled in environment", { 677 | enabled: true, 678 | testParent: { 679 | parent: { 680 | parent: { 681 | cfg: { 682 | failFast: { 683 | enabled: false, 684 | }, 685 | }, 686 | }, 687 | }, 688 | }, 689 | }); 690 | }); 691 | 692 | describe("when bail config is set", () => { 693 | afterEachShouldSetPluginFlag("and failed tests are equal to bail", { 694 | failedTests: 2, 695 | bail: 2, 696 | }); 697 | 698 | afterEachShouldSetPluginFlag("and failed tests are greater than bail", { 699 | failedTests: 3, 700 | bail: 2, 701 | }); 702 | 703 | afterEachShouldNotSetPluginFlag("and failed tests are lower than bail", { 704 | failedTests: 2, 705 | bail: 3, 706 | }); 707 | 708 | describe("failed tests log", () => { 709 | it("should log 1/1 when first test fails and bail is 1", async () => { 710 | getSupportCallbacks({ 711 | failedTests: 1, 712 | bail: 1, 713 | testState: "failed", 714 | testCurrentRetry: 3, 715 | testRetries: 3, 716 | }); 717 | afterEachCallback(); 718 | await wait(200); 719 | expect(cy.task.calledWith("failFastLog", "Failed tests: 1/1")).toBe(true); 720 | }); 721 | 722 | it("should log 1/2 when first test fails and bail is 2", async () => { 723 | getSupportCallbacks({ 724 | failedTests: 1, 725 | bail: 2, 726 | testState: "failed", 727 | testCurrentRetry: 3, 728 | testRetries: 3, 729 | }); 730 | afterEachCallback(); 731 | await wait(200); 732 | expect(cy.task.calledWith("failFastLog", "Failed tests: 1/2")).toBe(true); 733 | }); 734 | 735 | it("should log 3/4 when third test fails and bail is 4", async () => { 736 | getSupportCallbacks({ 737 | failedTests: 3, 738 | bail: 4, 739 | testState: "failed", 740 | testCurrentRetry: 3, 741 | testRetries: 3, 742 | }); 743 | afterEachCallback(); 744 | await wait(200); 745 | expect(cy.task.calledWith("failFastLog", "Failed tests: 3/4")).toBe(true); 746 | }); 747 | }); 748 | }); 749 | }); 750 | }); 751 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "moduleResolution": "node", 7 | "types": ["cypress"] 8 | }, 9 | "include": [ 10 | "*.ts", 11 | ], 12 | "exclude": [ 13 | "node_modules" 14 | ], 15 | } 16 | --------------------------------------------------------------------------------