├── .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] [](https://dashboard.stryker-mutator.io/reports/github.com/javierbrea/cypress-fail-fast/main)
2 |
3 | [](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 | 
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 |
--------------------------------------------------------------------------------