├── .github └── workflows │ ├── badges.yml │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── README.md ├── cypress-v9 ├── cypress.json ├── cypress │ └── integration │ │ └── spec.cy.js ├── package-lock.json └── package.json ├── cypress.config.js ├── cypress ├── checkbox.html ├── close-dialog.html ├── colors.html ├── e2e │ ├── alias.cy.js │ ├── and-or.cy.js │ ├── callback.cy.js │ ├── checked.cy.js │ ├── click-enabled.cy.js │ ├── close-dialog.cy.js │ ├── colors.cy.js │ ├── contains.cy.js │ ├── else.cy.js │ ├── exists.cy.js │ ├── finally.cy.js │ ├── find.cy.js │ ├── has-attribute.cy.js │ ├── has-class.cy.js │ ├── if-should.cy.js │ ├── input-value.cy.js │ ├── issue-59.cy.js │ ├── more-than-n.cy.js │ ├── not.cy.js │ ├── null.cy.js │ ├── raise-error.cy.js │ ├── spec.cy.js │ ├── task.cy.js │ ├── terms-and-conditions.cy.js │ ├── traversal.cy.js │ ├── url.cy.js │ └── wrap.cy.js ├── enabled-button.html ├── funky-input.html ├── index.html ├── list.html └── terms.html ├── img ├── debug.png ├── dialog-closed.png └── dialog-open.gif ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── index-v11.js ├── index.d.ts └── index.js └── tsconfig.json /.github/workflows/badges.yml: -------------------------------------------------------------------------------- 1 | name: badges 2 | on: 3 | schedule: 4 | # update badges every night 5 | # because we have a few badges that are linked 6 | # to the external repositories 7 | - cron: '0 3 * * *' 8 | 9 | jobs: 10 | badges: 11 | name: Badges 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - name: Checkout 🛎 15 | uses: actions/checkout@v4 16 | 17 | - name: Update version badges 🏷 18 | run: npm run badges 19 | 20 | - name: Commit any changed files 💾 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: Updated badges 24 | branch: main 25 | file_pattern: README.md 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-20.04 6 | steps: 7 | - name: Checkout 🛎 8 | uses: actions/checkout@v4 9 | 10 | - name: Run Cypress tests 🧪 11 | # https://github.com/cypress-io/github-action 12 | uses: cypress-io/github-action@v6 13 | with: 14 | # check if the types agree 15 | build: npm run types 16 | 17 | # https://github.com/actions/upload-artifact 18 | - uses: actions/upload-artifact@v4 19 | name: Store any error screenshots 🖼 20 | if: failure() 21 | with: 22 | name: cypress-screenshots 23 | path: cypress/screenshots 24 | 25 | - uses: actions/upload-artifact@v4 26 | name: Store any videos 🖼 27 | if: failure() 28 | with: 29 | name: cypress-videos 30 | path: cypress/videos 31 | 32 | # there was a breaking change under the hood in Cypress v11.1.0 33 | # so make sure this plugin still works for older versions 34 | test-cypress-v11-0: 35 | runs-on: ubuntu-20.04 36 | steps: 37 | - name: Checkout 🛎 38 | uses: actions/checkout@v4 39 | 40 | - name: Run Cypress tests 🧪 41 | # https://github.com/cypress-io/github-action 42 | uses: cypress-io/github-action@v6 43 | with: 44 | build: npm install -D cypress@11.0.1 45 | 46 | test-cypress-v9: 47 | runs-on: ubuntu-20.04 48 | steps: 49 | - name: Checkout 🛎 50 | uses: actions/checkout@v4 51 | 52 | - name: Install top dependencies 📦 53 | # https://github.com/cypress-io/github-action 54 | uses: cypress-io/github-action@v6 55 | with: 56 | runTests: false 57 | 58 | - name: Run Cypress v9 tests 🧪 59 | uses: cypress-io/github-action@v6 60 | with: 61 | working-directory: cypress-v9 62 | 63 | # https://github.com/actions/upload-artifact 64 | - uses: actions/upload-artifact@v4 65 | name: Store any v9 screenshots 🖼 66 | if: failure() 67 | with: 68 | name: cypress-screenshots-v9 69 | path: cypress-v9/cypress/screenshots 70 | 71 | release: 72 | needs: [test, test-cypress-v9, test-cypress-v11-0] 73 | runs-on: ubuntu-20.04 74 | if: github.ref == 'refs/heads/main' 75 | steps: 76 | - name: Checkout 🛎 77 | uses: actions/checkout@v4 78 | 79 | - name: Install only the semantic release 📦 80 | run: npm install semantic-release 81 | 82 | - name: Semantic Release 🚀 83 | uses: cycjimmy/semantic-release-action@v4 84 | with: 85 | branch: main 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cypress-if ![cypress version](https://img.shields.io/badge/cypress-14.3.0-brightgreen) [![ci](https://github.com/bahmutov/cypress-if/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bahmutov/cypress-if/actions/workflows/ci.yml) 2 | 3 | > Easy conditional if-else logic for your Cypress tests 4 | 5 | Tested with `cy.get`, `cy.contains`, `cy.find`, `.then`, `.within` commands in Cypress v9 and v10+. 6 | 7 | - 📝 [Conditional Commands For Cypress](https://glebbahmutov.com/blog/cypress-if/) 8 | - 📝 [Cypress Flakiness Examples](https://glebbahmutov.com/blog/flakiness-example/) 9 | - 📝 [Click Button If Enabled](https://glebbahmutov.com/blog/click-button-if-enabled/) 10 | - 📺 [Introduction To Using cypress-if Plugin to Write Conditional Cypress Commands](https://youtu.be/TVwU0OvrVUA) 11 | - 📺 [Confirm Cypress Command Execution Order Using Sinon.js Spies](https://youtu.be/RTzJu44yAc8) 12 | - 📺 [cypress-if Plugin Supports Cypress v12+: Close The Popup Dialog If It Is Visible](https://youtu.be/PLP5Bq7KHTk) 13 | - 📺 [Click Button If Enabled Using cypress-if And cypress-await Plugins](https://youtu.be/H04FSnH-6U0) 14 | - 🎓 Covered in my [Cypress Plugins course](https://cypress.tips/courses/cypress-plugins) 15 | - [Lesson d1: Write conditional commands using cypress-if](https://cypress.tips/courses/cypress-plugins/lessons/d1) 16 | - [Lesson d2: Conditionally clear the items in a TodoMVC application](https://cypress.tips/courses/cypress-plugins/lessons/d2) 17 | - [Lesson d3: Ensure the settings dialog is open](https://cypress.tips/courses/cypress-plugins/lessons/d3) 18 | - [Lesson d4: How to avoid conditional test logic](https://cypress.tips/courses/cypress-plugins/lessons/d4) 19 | - [Lesson d5: Create a new user if the test cannot log in](https://cypress.tips/courses/cypress-plugins/lessons/d5) 20 | - [Lesson n5: Pagination using cypress-if](https://cypress.tips/courses/cypress-plugins/lessons/n5) 21 | - [Lesson d6: Expand a section based on its attribute value](https://cypress.tips/courses/cypress-plugins/lessons/n6) 22 | - 🎓 Used in my course [Visual Testing With Cypress](https://cypress.tips/courses/visual-testing) 23 | - [Bonus 04: Hide an element if it is visible](https://cypress.tips/courses/visual-testing/lessons/bonus04) 24 | 25 | ## ⚠️ Warning 26 | 27 | In general, Cypress team considers [conditional testing an anti-pattern](https://on.cypress.io/conditional-testing). Thus `cypress-if` should be used only if the test really cannot deterministically execute its steps. You can also read my [conditional testing](https://glebbahmutov.com/cypress-examples/recipes/conditional-testing.html) examples. 28 | 29 | ## No xpath support 30 | 31 | This plugin works by overriding `cy.get`, `cy.find`, and some other Cypress commands. It does NOT override the [cy.xpath](https://www.npmjs.com/package/@cypress/xpath) commands that comes from another plugin. I personally suggest never using `xpath` selectors (and I wrote `cy.xpath`), the jQuery selectors included with Cypress are much more powerful and less prone to breaking. Learn them using [cypress-examples](https://glebbahmutov.com/cypress-examples). 32 | 33 | ## Install 34 | 35 | Add this package as a dev dependency 36 | 37 | ``` 38 | $ npm i -D cypress-if 39 | # or using Yarn 40 | $ yarn add -D cypress-if 41 | ``` 42 | 43 | Include this package in your spec or support file 44 | 45 | ```js 46 | import 'cypress-if' 47 | ``` 48 | 49 | ### Types 50 | 51 | Types for the `.if()` and `.else()` commands are described in the include typescript file [src/index.d.ts](./src/index.d.ts) file. If you need intellisense, include the type for this package in your `tscofig.json` 52 | 53 | ```jsonc 54 | "compilerOptions": { 55 | "types": [ 56 | "cypress", 57 | "cypress-if" // add this line 58 | ] 59 | } 60 | ``` 61 | 62 | For JavaScript projects that cannot use `tsconfig.json` or `jscofig.json`, the special comment might do the trick: 63 | 64 | ```js 65 | // your spec file "cypress/e2e/spec.cy.js" add this comment 66 | /// 67 | ``` 68 | 69 | If it does not work, and TS still complains about unknown command `.if`, then do the following trick and move on: 70 | 71 | ```js 72 | cy.get(...) 73 | // @ts-ignore 74 | .if() 75 | ``` 76 | 77 | ## Use 78 | 79 | Let's say, there is a dialog that might sometimes be visible when you visit the page. You can close it by finding it using the [cy.get](https://on.cypress.io/get) command follows by the `.if()` command. If the dialog really exists, then all commands chained after `.if()` run. If the dialog is not found, then the rest of the chain is skipped. 80 | 81 | ```js 82 | cy.get('dialog#survey').if().contains('button', 'Close').click() 83 | ``` 84 | 85 | ![Dialog was open](./img/dialog-open.gif) 86 | 87 | ## Assertions 88 | 89 | By default, the `.if()` command just checks the existence of the element returned by the `cy.get` command. You might use instead a different assertion, like close a dialog if it is visible: 90 | 91 | ```js 92 | cy.get('dialog#survey').if('visible').contains('button', 'Close').click() 93 | ``` 94 | 95 | If the dialog was invisible, the visibility assertion fails, and the rest of the commands was skipped 96 | 97 | ![Dialog was closed](./img/dialog-closed.png) 98 | 99 | You can use assertions with arguments 100 | 101 | ```js 102 | cy.wrap(42).if('equal', 42)... 103 | ``` 104 | 105 | You can use assertions with `not` 106 | 107 | ```js 108 | cy.get('#enrolled').if('not.checked').check() 109 | ``` 110 | 111 | ### Callback function 112 | 113 | You can check the value yourself by writing a callback function, similar to the [should(callback)](http://on.cypress.io/should#Function) and its [many examples](https://glebbahmutov.com/cypress-examples/commands/assertions.html). You can use predicate and Chai assertions, but you **cannot use any Cypress commands inside the callback**, since it only synchronously checks the given value. 114 | 115 | ```js 116 | // predicate function returning a boolean 117 | const isEven = (n) => n % 2 === 0 118 | cy.wrap(42).if(isEven).log('even').else().log('odd') 119 | // a function using Chai assertions 120 | const is42 = (n) => expect(n).to.equal(42) 121 | cy.wrap(42).if(is42).log('42!').else().log('some other number') 122 | ``` 123 | 124 | For more examples, see the [cypress/e2e/callback.cy.js](./cypress/e2e/callback.cy.js) spec 125 | 126 | ### Combining assertions 127 | 128 | If you want to right complex assertions that combine other checks using AND, OR connectors, please use a callback function. 129 | 130 | ```js 131 | // AND predicate using && 132 | cy.wrap(42).if((n) => n > 20 && n < 50) 133 | // AND connector using Chai "and" connector 134 | cy.wrap(42).if((n) => expect(n).to.be.greaterThan(20).and.to.be.lessThan(50)) 135 | // OR predicate using || 136 | cy.wrap(42).if((n) => n > 20 || n < 10) 137 | ``` 138 | 139 | Unfortunately, there is no Chai OR connector. 140 | 141 | For more examples, see the [cypress/e2e/and-or.cy.js](./cypress/e2e/and-or.cy.js) spec file 142 | 143 | ## else command 144 | 145 | You can chain `.else()` command that is only executed if the `.if()` is skipped. 146 | 147 | ```js 148 | cy.contains('Accept cookies') 149 | .if('visible') 150 | .click() 151 | .else() 152 | .log('no cookie banner') 153 | ``` 154 | 155 | The subject from the `.if()` command will be passed to the `.else()` chain, this allows you to work with the original element: 156 | 157 | ```js 158 | cy.get('#enrolled') 159 | .if('checked') 160 | .log('**already enrolled**') 161 | // the checkbox should be passed into .else() 162 | .else() 163 | .check() 164 | ``` 165 | 166 | You can print a message if the `ELSE` branch is taken 167 | 168 | ```js 169 | cy.get('...').if('...').else().log('a message') 170 | // same as 171 | cy.get('...').if('...').else('a message') 172 | ``` 173 | 174 | ## Multiple commands 175 | 176 | Sometimes it makes sense to place the "if" or "else" commands into `.then()` block 177 | 178 | ```js 179 | cy.get('#survey') 180 | .if('visible') 181 | .then(() => { 182 | cy.log('closing the survey') 183 | cy.contains('button', 'Close').click() 184 | }) 185 | .else() 186 | .then(() => { 187 | cy.log('Already closed') 188 | }) 189 | ``` 190 | 191 | ## Within 192 | 193 | You can attach `.within()` command to the `.if()` 194 | 195 | ```js 196 | cy.get('#survey') 197 | .if('visible') 198 | .within(() => { 199 | // fill the survey 200 | // click the submit button 201 | }) 202 | ``` 203 | 204 | ## finally 205 | 206 | You might want to finish if/else command chains and continue afterwards. This is the purpose for the `.finally()` child command: 207 | 208 | ```js 209 | cy.get('#agreed') 210 | .if('not.checked') 211 | .check() 212 | .else() 213 | .log('already checked') 214 | .finally() 215 | .should('be.checked') 216 | ``` 217 | 218 | `.finally` comes in useful when you are chaining something and don't want the "if/else" to "leak" to the next series of commands. From [#59](https://github.com/bahmutov/cypress-if/issues/59) comes the [issue-59.cy.js](./github/../cypress/e2e/issue-59.cy.js) 219 | 220 | ```js 221 | function bar() { 222 | return ( 223 | cy 224 | .wrap('testing') 225 | .if() 226 | .then(() => cy.wrap('got it')) 227 | .else() 228 | .then(() => cy.wrap('else do')) 229 | // to correctly STOP the chaining if/else 230 | // from putting anything chained of bar() 231 | // need to add .finally() command 232 | .finally() 233 | ) 234 | } 235 | bar().then((it) => { 236 | cy.log(`result: ${it}`) 237 | }) 238 | // logs: 239 | // "testing" 240 | // "got it" 241 | // result: got it" 242 | ``` 243 | 244 | ## cy.task 245 | 246 | You can perform commands if the `cy.task` failed 247 | 248 | ```js 249 | cy.task('throws').if('failed') 250 | // handle the failure 251 | ``` 252 | 253 | ## Aliases 254 | 255 | You can have conditional commands depending on an alias that might exist. 256 | 257 | ```js 258 | cy.get('@maybe') 259 | .if() 260 | // commands to execute if the alias "maybe" exists 261 | .else() 262 | // commands to execute if the alias "maybe" does not exist 263 | .finally() 264 | // commands to execute after 265 | .log(...) 266 | ``` 267 | 268 | See spec [alias.cy.js](./cypress/e2e/alias.cy.js) 269 | 270 | ## Null values 271 | 272 | Typically `null` values are treated same as `undefined` and follow the "else" path. You can specifically check for `null` and `not.null` using these assertions: 273 | 274 | ```js 275 | cy.wrap(null).if('null') // takes IF path 276 | cy.wrap(null).if('not.null') // takes ELSE path 277 | cy.wrap(42).if('not.null') // takes IF path 278 | ``` 279 | 280 | See spec [null.cy.js](./cypress/e2e/null.cy.js) 281 | 282 | ## Multiple values 283 | 284 | Some assertions need two values, for example: 285 | 286 | ```js 287 | // only checks the presence of the "data-x" HTML attribute 288 | .if('have.attr', 'data-x') 289 | // checks if the "data-x" attribute present AND has value "123" 290 | .if('have.attr', 'data-x', '123') 291 | ``` 292 | 293 | ## raise 294 | 295 | This plugin includes a utility custom command `cy.raise` that lets you conveniently throw an error. 296 | 297 | ```js 298 | cy.get('li').if('not.have.length', 3).raise('Wrong number of todos') 299 | ``` 300 | 301 | **Tip:** the above syntax works, but you better pass an Error instance rather than a string to get the exact stack trace location 302 | 303 | ```js 304 | cy.get('li').if('not.have.length', 3).raise(new Error('Wrong number of todos')) 305 | ``` 306 | 307 | ## More examples 308 | 309 | Check out the spec files in [cypress/e2e](./cypress/e2e/) folder. If you still have a question, [open a GitHub issue](https://github.com/bahmutov/cypress-if/issues). 310 | 311 | ## Debugging 312 | 313 | This module uses [debug](https://github.com/debug-js/debug#readme) module to output verbose browser console messages when needed. To turn the logging on, open the browser's DevTools console and set the local storage entry: 314 | 315 | ```js 316 | localStorage.debug = 'cypress-if' 317 | ``` 318 | 319 | If you re-run the tests, you should see the messages appear in the console 320 | 321 | ![Debug messages in the console](./img/debug.png) 322 | 323 | ## See also 324 | 325 | - [cypress-wait-if-happens](https://github.com/bahmutov/cypress-wait-if-happens) 326 | - [cypress-ngx-ui-testing](https://github.com/swimlane/ngx-ui/tree/master/projects/swimlane/ngx-ui-testing) 327 | 328 | ## Small print 329 | 330 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2022 331 | 332 | - [@bahmutov](https://twitter.com/bahmutov) 333 | - [glebbahmutov.com](https://glebbahmutov.com) 334 | - [blog](https://glebbahmutov.com/blog) 335 | - [videos](https://www.youtube.com/glebbahmutov) 336 | - [presentations](https://slides.com/bahmutov) 337 | - [cypress.tips](https://cypress.tips) 338 | - [Cypress Tips & Tricks Newsletter](https://cypresstips.substack.com/) 339 | - [my Cypress courses](https://cypress.tips/courses) 340 | 341 | License: MIT - do anything with the code, but don't blame me if it does not work. 342 | 343 | Support: if you find any problems with this module, email / tweet / 344 | [open issue](https://github.com/bahmutov/cypress-if/issues) on Github 345 | 346 | ## MIT License 347 | 348 | Copyright (c) 2022 Gleb Bahmutov <gleb.bahmutov@gmail.com> 349 | 350 | Permission is hereby granted, free of charge, to any person 351 | obtaining a copy of this software and associated documentation 352 | files (the "Software"), to deal in the Software without 353 | restriction, including without limitation the rights to use, 354 | copy, modify, merge, publish, distribute, sublicense, and/or sell 355 | copies of the Software, and to permit persons to whom the 356 | Software is furnished to do so, subject to the following 357 | conditions: 358 | 359 | The above copyright notice and this permission notice shall be 360 | included in all copies or substantial portions of the Software. 361 | 362 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 363 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 364 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 365 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 366 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 367 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 368 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 369 | OTHER DEALINGS IN THE SOFTWARE. 370 | -------------------------------------------------------------------------------- /cypress-v9/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": false, 3 | "supportFile": false, 4 | "fixturesFolder": false 5 | } 6 | -------------------------------------------------------------------------------- /cypress-v9/cypress/integration/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | import '../../../src' 3 | 4 | it('executes the IF branch', () => { 5 | cy.wrap(1) 6 | .if('equal', 1) 7 | .then(cy.spy().as('if')) 8 | .else() 9 | .then(cy.spy().as('else')) 10 | .finally() 11 | .should('equal', 1) 12 | cy.get('@if').should('have.been.calledOnce') 13 | cy.get('@else').should('not.be.called') 14 | }) 15 | 16 | describe('wrapped value', () => { 17 | it('performs an action if the wrapped value is equal to 42', () => { 18 | cy.wrap(42).if('equal', 42).then(cy.spy().as('action')).then(cy.log) 19 | cy.get('@action').should('have.been.calledOnce') 20 | }) 21 | 22 | it('does nothing if it is not 42', () => { 23 | cy.wrap(1).if('equal', 42).then(cy.spy().as('action')).then(cy.log) 24 | cy.get('@action').should('not.have.been.called') 25 | }) 26 | 27 | context('.else', () => { 28 | it('passes the subject to the else branch', () => { 29 | cy.wrap(1).if('equal', 42).log('if branch').else().should('equal', 1) 30 | }) 31 | 32 | it('passes the subject if().else()', () => { 33 | cy.wrap(1).if('equal', 42).else().should('equal', 1) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /cypress-v9/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-v9", 3 | "version": "1.0.0", 4 | "description": "Testing on Cypress v9", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "cypress run" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "cypress": "9.7.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | experimentalRunAllSpecs: true, 6 | fixturesFolder: false, 7 | supportFile: false, 8 | viewportWidth: 200, 9 | viewportHeight: 200, 10 | defaultCommandTimeout: 1000, 11 | video: true, 12 | setupNodeEvents(on, config) { 13 | // implement node event listeners here 14 | on('task', { 15 | get42() { 16 | console.log('returning 42') 17 | return 42 18 | }, 19 | 20 | throws() { 21 | console.log('throwing an error from cy.task') 22 | throw new Error('Nope') 23 | }, 24 | }) 25 | }, 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /cypress/checkbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | Enrolled already 7 |
8 | 9 |
10 | 11 | Agree 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /cypress/close-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 |

My page

15 |

16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 17 | tempor incididunt ut labore et dolore magna aliqua. Hac habitasse platea 18 | dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Scelerisque 19 | fermentum dui faucibus in ornare. Suspendisse potenti nullam ac tortor 20 | vitae purus. Porta lorem mollis aliquam ut. Nunc sed augue lacus viverra 21 | vitae congue eu consequat. Consequat id porta nibh venenatis cras sed. 22 | Gravida quis blandit turpis cursus in hac habitasse. Felis eget nunc 23 | lobortis mattis. Ac odio tempor orci dapibus. Est pellentesque elit 24 | ullamcorper dignissim cras. Mollis nunc sed id semper risus in hendrerit. 25 | Vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Et netus et 26 | malesuada fames ac turpis egestas. 27 |

28 | 29 |

Take a survey!

30 |
31 |
32 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /cypress/colors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /cypress/e2e/alias.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('aliases', () => { 7 | it('has an existing alias', () => { 8 | cy.wrap(42).as('answer') 9 | cy.get('@answer') 10 | .if('exist') 11 | .log('alias exists') 12 | .then(cy.spy().as('if')) 13 | .else() 14 | .then(cy.spy().as('else')) 15 | cy.get('@if').should('have.been.called') 16 | cy.get('@else').should('not.have.been.called') 17 | }) 18 | 19 | it('has an existing alias no arguments', () => { 20 | cy.wrap(42).as('answer') 21 | cy.get('@answer') 22 | .if() 23 | .log('alias exists') 24 | .then(cy.spy().as('if')) 25 | .else() 26 | .then(cy.spy().as('else')) 27 | cy.get('@if').should('have.been.called') 28 | cy.get('@else').should('not.have.been.called') 29 | }) 30 | 31 | it('has no alias', () => { 32 | // notice there is no alias with the name "answer" 33 | cy.get('@answer') 34 | .if() 35 | .log('alias exists') 36 | .then(cy.spy().as('if')) 37 | .else() 38 | .then(cy.spy().as('else')) 39 | cy.get('@else').should('have.been.called') 40 | cy.get('@if').should('not.have.been.called') 41 | }) 42 | 43 | it('alias has null value is treated as non-existent', () => { 44 | cy.wrap(null).as('nullAlias') 45 | cy.get('@nullAlias') 46 | .if() 47 | .log('alias exists') 48 | .then(cy.spy().as('if')) 49 | .else() 50 | .then(cy.spy().as('else')) 51 | cy.get('@else').should('have.been.called') 52 | cy.get('@if').should('not.have.been.called') 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /cypress/e2e/and-or.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('AND assertions', () => { 7 | context('uses && inside the predicate callback', () => { 8 | it('T && T', () => { 9 | cy.wrap(42) 10 | .if((n) => n > 20 && n < 50) 11 | .then(cy.spy().as('if')) 12 | .else() 13 | .then(cy.spy().as('else')) 14 | cy.get('@if').should('have.been.called') 15 | cy.get('@else').should('not.have.been.called') 16 | }) 17 | 18 | it('T && F', () => { 19 | cy.wrap(42) 20 | .if((n) => n > 20 && n < 40) 21 | .then(cy.spy().as('if')) 22 | .else() 23 | .then(cy.spy().as('else')) 24 | cy.get('@else').should('have.been.called') 25 | cy.get('@if').should('not.have.been.called') 26 | }) 27 | }) 28 | 29 | context('uses Chai assertions', () => { 30 | it('ok and ok', () => { 31 | cy.wrap(42) 32 | .if((n) => expect(n).to.be.greaterThan(20).and.to.be.lessThan(50)) 33 | .then(cy.spy().as('if')) 34 | .else() 35 | .then(cy.spy().as('else')) 36 | cy.get('@if').should('have.been.called') 37 | cy.get('@else').should('not.have.been.called') 38 | }) 39 | 40 | it('ok and not ok', () => { 41 | cy.wrap(42) 42 | .if((n) => expect(n).to.be.greaterThan(20).and.to.be.lessThan(40)) 43 | .then(cy.spy().as('if')) 44 | .else() 45 | .then(cy.spy().as('else')) 46 | cy.get('@else').should('have.been.called') 47 | cy.get('@if').should('not.have.been.called') 48 | }) 49 | }) 50 | }) 51 | 52 | describe('OR assertions', () => { 53 | context('uses || inside the predicate callback', () => { 54 | it('T || F', () => { 55 | cy.wrap(42) 56 | .if((n) => n > 20 || n < 10) 57 | .then(cy.spy().as('if')) 58 | .else() 59 | .then(cy.spy().as('else')) 60 | cy.get('@if').should('have.been.called') 61 | cy.get('@else').should('not.have.been.called') 62 | }) 63 | 64 | it('F || T', () => { 65 | cy.wrap(42) 66 | .if((n) => n > 200 || n < 50) 67 | .then(cy.spy().as('if')) 68 | .else() 69 | .then(cy.spy().as('else')) 70 | cy.get('@if').should('have.been.called') 71 | cy.get('@else').should('not.have.been.called') 72 | }) 73 | 74 | it('F || F', () => { 75 | cy.wrap(42) 76 | .if((n) => n > 200 || n < -40) 77 | .then(cy.spy().as('if')) 78 | .else() 79 | .then(cy.spy().as('else')) 80 | cy.get('@else').should('have.been.called') 81 | cy.get('@if').should('not.have.been.called') 82 | }) 83 | }) 84 | 85 | // note that we cannot easily do OR using Chai assertions 86 | }) 87 | -------------------------------------------------------------------------------- /cypress/e2e/callback.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import '../../src' 4 | 5 | describe('assertion callback function', () => { 6 | context('predicate', () => { 7 | const isEven = (n) => n % 2 === 0 8 | 9 | it('if branch', () => { 10 | cy.wrap(42) 11 | .if(isEven) 12 | .log('even') 13 | .then(cy.spy().as('if')) 14 | .else() 15 | .log('odd') 16 | .then(cy.spy().as('else')) 17 | cy.get('@if').should('have.been.called') 18 | cy.get('@else').should('not.be.called') 19 | }) 20 | 21 | it('else branch', () => { 22 | cy.wrap(1) 23 | .if(isEven) 24 | .log('even') 25 | .then(cy.spy().as('if')) 26 | .else() 27 | .log('odd') 28 | .then(cy.spy().as('else')) 29 | cy.get('@else').should('have.been.called') 30 | cy.get('@if').should('not.be.called') 31 | }) 32 | }) 33 | 34 | context('Chai assertion', () => { 35 | const is42 = (n) => expect(n).to.equal(42) 36 | 37 | it('if branch', () => { 38 | cy.wrap(42) 39 | .if(is42) 40 | .log('even') 41 | .then(cy.spy().as('if')) 42 | .else() 43 | .log('odd') 44 | .then(cy.spy().as('else')) 45 | cy.get('@if').should('have.been.called') 46 | cy.get('@else').should('not.be.called') 47 | }) 48 | 49 | it('else branch', () => { 50 | cy.wrap(1) 51 | .if(is42) 52 | .log('even') 53 | .then(cy.spy().as('if')) 54 | .else() 55 | .log('odd') 56 | .then(cy.spy().as('else')) 57 | cy.get('@else').should('have.been.called') 58 | cy.get('@if').should('not.be.called') 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /cypress/e2e/checked.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import '../../src' 4 | 5 | describe('checkbox', () => { 6 | it('checks the box when it is not checked already', () => { 7 | cy.visit('cypress/checkbox.html') 8 | cy.get('#enrolled').if('not.checked').check() 9 | }) 10 | 11 | it('does nothing if the box is already checked', () => { 12 | cy.visit('cypress/checkbox.html') 13 | cy.get('#agreed').if('not.checked').check() 14 | }) 15 | 16 | context('with else() branches', () => { 17 | it('logs a message when nothing to check', () => { 18 | cy.visit('cypress/checkbox.html') 19 | cy.get('#agreed') 20 | .if('not.checked') 21 | .check() 22 | .else() 23 | .log('**already agreed**') 24 | }) 25 | 26 | it('handles if().else() short chain', () => { 27 | cy.visit('cypress/checkbox.html') 28 | cy.get('#enrolled').if('checked').else().check() 29 | cy.get('#enrolled').should('be.checked') 30 | }) 31 | 32 | it('checks the button if not checked', () => { 33 | cy.visit('cypress/checkbox.html') 34 | cy.get('#enrolled') 35 | .if('checked') 36 | // .log('**already enrolled**') 37 | .else() 38 | .check() 39 | cy.get('#enrolled').should('be.checked') 40 | }) 41 | 42 | it('passes the subject to the else() branch', () => { 43 | cy.visit('cypress/checkbox.html') 44 | cy.get('#enrolled') 45 | .if('checked') 46 | .log('**already enrolled**') 47 | // the checkbox should be passed into .else() 48 | .else() 49 | .check() 50 | cy.get('#enrolled').should('be.checked') 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /cypress/e2e/click-enabled.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | // https://github.com/bahmutov/cypress-if 5 | import '../../src' 6 | 7 | it('clicks the button if enabled', () => { 8 | cy.visit('cypress/enabled-button.html') 9 | cy.contains('button', 'Click Me') 10 | // tests using Chai assertion "be.enabled" 11 | .if('enabled') 12 | .click() 13 | .should('have.text', 'Clicked') 14 | .else('Button is disabled') 15 | }) 16 | 17 | it('clicks the button if enabled, checks using jQuery is :enabled', () => { 18 | cy.visit('cypress/enabled-button.html') 19 | cy.contains('button', 'Click Me') 20 | .invoke('is', ':enabled') 21 | .if('equals', true) 22 | // grab the button again using "cy.document + cy.contains" 23 | // combination to avoid using the Boolean subject 24 | // from the previous command 25 | .document() 26 | .contains('button', 'Click Me') 27 | .click() 28 | .should('have.text', 'Clicked') 29 | .else('Button is disabled') 30 | }) 31 | -------------------------------------------------------------------------------- /cypress/e2e/close-dialog.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | function visit(showDialog = true) { 7 | cy.visit('cypress/close-dialog.html', { 8 | onBeforeLoad(win) { 9 | // trick the app to open / hide the dialog 10 | cy.stub(win.Math, 'random').returns(showDialog ? 0 : 1) 11 | }, 12 | }) 13 | const submitForm = cy.stub().as('submitForm') 14 | if (showDialog) { 15 | cy.get('dialog#survey').invoke('on', 'submit', submitForm) 16 | } 17 | } 18 | 19 | it( 20 | 'closes the survey dialog', 21 | { viewportWidth: 500, viewportHeight: 500 }, 22 | () => { 23 | visit(true) 24 | cy.get('dialog#survey') 25 | .if('visible') 26 | .wait(1000) 27 | .contains('button', 'Close') 28 | .click() 29 | // if there is a dialog on top, 30 | // then the main text is not visible 31 | cy.get('#main').should('be.visible') 32 | // in this test the dialog should have been submitted 33 | cy.get('@submitForm').should('have.been.calledOnce') 34 | }, 35 | ) 36 | 37 | it( 38 | 'skips the commands since the dialog is closed', 39 | { viewportWidth: 500, viewportHeight: 500 }, 40 | () => { 41 | visit(false) 42 | cy.get('dialog#survey') 43 | .if('visible') 44 | .wait(1000) 45 | .contains('button', 'Close') 46 | .click() 47 | // if there is a dialog on top, 48 | // then the main text is not visible 49 | cy.get('#main').should('be.visible') 50 | // in this test the dialog was never submitted 51 | cy.get('@submitForm').should('not.have.been.called') 52 | }, 53 | ) 54 | 55 | it( 56 | 'controls the cy.get timeout', 57 | { viewportWidth: 500, viewportHeight: 500 }, 58 | () => { 59 | visit(false) 60 | cy.get('does-not-exist', { timeout: 0 }) 61 | .if() 62 | .log('found it') 63 | .else() 64 | .log('does not exist') 65 | }, 66 | ) 67 | 68 | describe('cy.then support', () => { 69 | it( 70 | 'executes the .then callback if the dialog is visible', 71 | { viewportWidth: 500, viewportHeight: 500 }, 72 | () => { 73 | visit(true) 74 | cy.get('dialog#survey') 75 | .if('visible') 76 | .then(() => { 77 | cy.log('**closing the dialog**') 78 | cy.contains('dialog#survey button', 'Close').wait(1000).click() 79 | cy.get('dialog').should('not.be.visible') 80 | }) 81 | 82 | // if there is a dialog on top, 83 | // then the main text is not visible 84 | cy.get('#main').should('be.visible') 85 | // in this test the dialog should have been submitted 86 | cy.get('@submitForm').should('have.been.calledOnce') 87 | }, 88 | ) 89 | 90 | it( 91 | 'skips the .then callback if the dialog is hidden', 92 | { viewportWidth: 500, viewportHeight: 500 }, 93 | () => { 94 | visit(false) 95 | cy.get('dialog#survey') 96 | .if('visible') 97 | .then(() => { 98 | cy.log('**closing the dialog**') 99 | cy.contains('dialog#survey button', 'Close').wait(1000).click() 100 | cy.get('dialog').should('not.be.visible') 101 | }) 102 | // if there is a dialog on top, 103 | // then the main text is not visible 104 | cy.get('#main').should('be.visible') 105 | // in this test the dialog was never submitted 106 | cy.get('@submitForm').should('not.have.been.called') 107 | }, 108 | ) 109 | }) 110 | 111 | describe('cy.contains support', () => { 112 | it( 113 | 'clicks the close survey button', 114 | { viewportWidth: 500, viewportHeight: 500 }, 115 | () => { 116 | visit(true) 117 | cy.contains('dialog#survey button', 'Close') 118 | .if('visible') 119 | .wait(1000) 120 | .click() 121 | // if there is a dialog on top, 122 | // then the main text is not visible 123 | cy.get('#main').should('be.visible') 124 | // in this test the dialog should have been submitted 125 | cy.get('@submitForm').should('have.been.calledOnce') 126 | }, 127 | ) 128 | 129 | it( 130 | 'skips click when the button is hidden', 131 | { viewportWidth: 500, viewportHeight: 500 }, 132 | () => { 133 | visit(false) 134 | cy.contains('dialog#survey button', 'Close') 135 | .if('visible') 136 | .wait(1000) 137 | .click() 138 | // if there is a dialog on top, 139 | // then the main text is not visible 140 | cy.get('#main').should('be.visible') 141 | // in this test the dialog was never submitted 142 | cy.get('@submitForm').should('not.have.been.called') 143 | }, 144 | ) 145 | }) 146 | 147 | describe('cy.find support', () => { 148 | it( 149 | 'finds the close button and closes the dialog', 150 | { viewportWidth: 500, viewportHeight: 500 }, 151 | () => { 152 | visit(true) 153 | // make sure the page has finished loading 154 | cy.get('#main') 155 | cy.get('body').find('#close').if('visible').wait(1000).click() 156 | // if there is a dialog on top, 157 | // then the main text is not visible 158 | cy.get('#main').should('be.visible') 159 | // in this test the dialog should have been submitted 160 | cy.get('@submitForm').should('have.been.calledOnce') 161 | }, 162 | ) 163 | 164 | it( 165 | 'skips click when it cannot find the button', 166 | { viewportWidth: 500, viewportHeight: 500 }, 167 | () => { 168 | visit(false) 169 | // make sure the page has finished loading 170 | cy.get('#main') 171 | // then check if the close button is present 172 | cy.get('body').find('#close').if('visible').wait(1000).click() 173 | // if there is a dialog on top, 174 | // then the main text is not visible 175 | cy.get('#main').should('be.visible') 176 | // in this test the dialog was never submitted 177 | cy.get('@submitForm').should('not.have.been.called') 178 | }, 179 | ) 180 | }) 181 | -------------------------------------------------------------------------------- /cypress/e2e/colors.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | it('combines all colors on the page', () => { 7 | cy.visit('cypress/colors.html') 8 | for (let k = 1; k < 10; k += 1) { 9 | cy.get('#colors #color' + k, { timeout: 100 }) 10 | .if('exist') 11 | .invoke('text') 12 | .invoke('trim') 13 | .as('color' + k) 14 | } 15 | cy.then(function () { 16 | const colorNames = [ 17 | this.color1, 18 | this.color2, 19 | this.color3, 20 | this.color4, 21 | this.color5, 22 | this.color6, 23 | this.color7, 24 | this.color8, 25 | this.color9, 26 | ] 27 | .filter(Boolean) 28 | .join(', ') 29 | expect(colorNames, 'color names').to.equal('red, green') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /cypress/e2e/contains.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('cy.contains support', () => { 7 | beforeEach(() => { 8 | cy.visit('cypress/index.html') 9 | }) 10 | 11 | it('clicks on the existing button', () => { 12 | cy.get('#load').invoke('on', 'click', cy.spy().as('clicked')) 13 | cy.log('**button exists**') 14 | cy.contains('button', 'Load').if().click() 15 | cy.get('@clicked').should('have.been.calledOnce') 16 | }) 17 | 18 | it('works with the text only', () => { 19 | cy.get('#load').invoke('on', 'click', cy.spy().as('clicked')) 20 | cy.contains('Load').if().click() 21 | cy.get('@clicked').should('have.been.calledOnce') 22 | }) 23 | 24 | it('passes the attached assertions', () => { 25 | cy.log('**attached assertions are passing**') 26 | cy.contains('button', 'Load').if().should('be.visible') 27 | }) 28 | 29 | it('clicks on the button by text if exists', () => { 30 | cy.log('**button does not exist**') 31 | cy.contains('button', 'does-not-exist').if().click() 32 | cy.log('**attached assertions are skipped**') 33 | cy.contains('button', 'does-not-exist').if().should('not.exist') 34 | }) 35 | 36 | it('passes the timeout', () => { 37 | cy.contains('button', 'does-not-exist', { timeout: 500 }).if().click() 38 | }) 39 | 40 | it('does not click invisible button', () => { 41 | cy.log('**button exist but is hidden**') 42 | cy.contains('button#hidden', 'Cannot see me').if('visible').click() 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /cypress/e2e/else.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('else branch', () => { 7 | it('takes the if branch', () => { 8 | cy.wrap(42).if('equal', 42).log('if branch').else().log('else branch') 9 | cy.log('**built-in log**') 10 | cy.wrap(42).if('equal', 42).log('if branch').else('else branch') 11 | }) 12 | 13 | it('takes the else branch', () => { 14 | cy.wrap(42).if('equal', 1).log('if branch').else().log('else branch') 15 | cy.log('**built-in log**') 16 | cy.wrap(42).if('equal', 1).log('if branch').else('else branch') 17 | cy.log('**prints numbers**') 18 | cy.wrap(42).if('equal', 1).log('if branch').else(42) 19 | }) 20 | 21 | it('logs the else message', () => { 22 | cy.spy(cy, 'log').as('log') 23 | cy.wrap(true).if('false').else('else branch') 24 | cy.get('@log').should('have.been.calledWith', 'else branch') 25 | }) 26 | 27 | it('logs the default else message', () => { 28 | cy.spy(cy, 'log').as('log') 29 | cy.wrap(true).if('false').else() 30 | cy.get('@log').should('not.have.been.called') 31 | }) 32 | 33 | it('can have multiple if-else', () => { 34 | cy.wrap(1) 35 | .if('equal', 2) 36 | .log('is 2') 37 | .else() 38 | .if('equal', 3) 39 | .log('is 3') 40 | .if('equal', 1) 41 | .log('is 1') 42 | }) 43 | 44 | it('attaches should', () => { 45 | cy.wrap(1).if('equal', 1).should('equal', 1).else().should('equal', 2) 46 | }) 47 | 48 | context('with checks', () => { 49 | it('calls actions in the if branch', () => { 50 | cy.wrap(42) 51 | .if('equal', 42) 52 | .then(cy.spy().as('if')) 53 | .else() 54 | .then(cy.spy().as('else')) 55 | cy.get('@if').should('have.been.calledOnce') 56 | cy.get('@else').should('not.be.called') 57 | }) 58 | 59 | it('skips the entire ELSE chain', () => { 60 | cy.visit('cypress/index.html') 61 | cy.get('#load') 62 | .if() 63 | .log('found it') 64 | .get('#load') 65 | .click() 66 | .else() 67 | .log('ughh, why execute the else branch') 68 | .then(() => { 69 | throw new Error('no!!!') 70 | }) 71 | }) 72 | 73 | it('skips the entire ELSE chain even if it has parent commands', () => { 74 | cy.visit('cypress/index.html') 75 | cy.get('#load') 76 | .if() 77 | .log('found it') 78 | .get('#load') 79 | .click() 80 | .else() 81 | .get('#load') 82 | .log('ughh, why execute the else branch after a parent command') 83 | .then(() => { 84 | throw new Error('no!!!') 85 | }) 86 | }) 87 | 88 | it('calls actions in the else branch', () => { 89 | cy.wrap(42) 90 | .if('equal', 1) 91 | .then(cy.spy().as('if')) 92 | .else() 93 | .then(cy.spy().as('else')) 94 | cy.get('@else').should('have.been.calledOnce') 95 | cy.get('@if').should('not.be.called') 96 | }) 97 | 98 | it('attaches the .then block correctly', () => { 99 | cy.wrap(42) 100 | .if('equal', 1) 101 | .then(cy.spy().as('if')) 102 | .else() 103 | .then(() => { 104 | cy.spy().as('else1')() 105 | cy.spy().as('else2')() 106 | }) 107 | cy.get('@else1').should('have.been.calledOnce') 108 | cy.get('@else2').should('have.been.calledOnce') 109 | cy.get('@if').should('not.be.called') 110 | }) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /cypress/e2e/exists.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | beforeEach(() => { 7 | cy.visit('cypress/index.html') 8 | }) 9 | 10 | it('checks an element that exists', () => { 11 | cy.get('#fruits') 12 | .if('exist') 13 | .then(cy.spy().as('if')) 14 | .else() 15 | .then(cy.spy().as('else')) 16 | cy.get('@if').should('have.been.calledOnce') 17 | cy.get('@else').should('not.have.been.called') 18 | }) 19 | 20 | it('checks an element that does not exists', () => { 21 | cy.get('#not-found') 22 | .if('exist') 23 | .then(cy.spy().as('if')) 24 | .else() 25 | .then(cy.spy().as('else')) 26 | cy.get('@else').should('have.been.calledOnce') 27 | cy.get('@if').should('not.have.been.called') 28 | }) 29 | 30 | // https://github.com/bahmutov/cypress-if/issues/45 31 | it('checks an element that does not exists using not.exist', () => { 32 | cy.get('#not-found').should('not.exist') 33 | cy.get('#not-found') 34 | .if('not.exist') 35 | .then(cy.spy().as('if')) 36 | .else() 37 | .then(cy.spy().as('else')) 38 | cy.get('@if').should('have.been.calledOnce') 39 | cy.get('@else').should('not.have.been.called') 40 | }) 41 | 42 | it('clicks on the element with force: true', () => { 43 | cy.window() 44 | .its('console') 45 | .then((console) => { 46 | cy.spy(console, 'log').withArgs('clicked').as('logClicked') 47 | }) 48 | cy.get('#load').if('exist').click({ force: true }) 49 | cy.get('@logClicked').should('have.been.calledOnce') 50 | }) 51 | 52 | it('uses exists as an alias to exist', () => { 53 | cy.get('#fruits') 54 | .if('exists') 55 | .then(cy.spy().as('if')) 56 | .else() 57 | .then(cy.spy().as('else')) 58 | cy.get('@if').should('have.been.calledOnce') 59 | cy.get('@else').should('not.have.been.called') 60 | }) 61 | 62 | it('supports cy.not', () => { 63 | cy.log('**IF path**') 64 | cy.get('#fruits li') 65 | .not(':odd') 66 | .if('exists') 67 | .log('found even') 68 | .else() 69 | .raise('it is odd') 70 | 71 | cy.log('**ELSE path**') 72 | cy.get('#fruits li') 73 | .not('li') 74 | .if('exists') 75 | .raise('cy.not is not supported') 76 | .else() 77 | .log('cy.not is supported') 78 | }) 79 | -------------------------------------------------------------------------------- /cypress/e2e/finally.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('finally', () => { 7 | it('executes after the IF path', () => { 8 | cy.visit('cypress/checkbox.html') 9 | cy.get('#enrolled').if('not.checked').check().finally().should('be.checked') 10 | }) 11 | 12 | it('executes after the ELSE path', () => { 13 | cy.visit('cypress/checkbox.html') 14 | cy.get('#agreed') 15 | .if('not.checked') 16 | .check() 17 | .else() 18 | .log('already checked') 19 | .finally() 20 | .should('be.checked') 21 | }) 22 | 23 | it('executes the IF branch', () => { 24 | cy.wrap(1) 25 | .if('equal', 1) 26 | .then(cy.spy().as('if')) 27 | .else() 28 | .then(cy.spy().as('else')) 29 | .finally() 30 | .then((/** @type number */ subject) => { 31 | expect(subject, 'subject').to.equal(1) 32 | }) 33 | .should('equal', 1) 34 | cy.get('@if').should('have.been.calledOnce') 35 | cy.get('@else').should('not.be.called') 36 | }) 37 | 38 | it('executes the ELSE branch', () => { 39 | cy.wrap(1) 40 | .if('equal', 42) 41 | .then(cy.spy().as('if')) 42 | .else() 43 | .then(cy.spy().as('else')) 44 | .finally() 45 | .should('equal', 1) 46 | cy.get('@else').should('have.been.calledOnce') 47 | cy.get('@if').should('not.be.called') 48 | }) 49 | 50 | it('executes the FINALLY command', () => { 51 | cy.wrap(1) 52 | .if('equal', 42) 53 | .then(cy.spy().as('if')) 54 | .else() 55 | .then(cy.spy().as('else')) 56 | .finally() 57 | .then(cy.spy().as('finally')) 58 | cy.get('@else').should('have.been.calledOnce') 59 | cy.get('@if').should('not.be.called') 60 | cy.get('@finally').should('have.been.calledOnce') 61 | }) 62 | 63 | it('yields the IF subject without ELSE branch', () => { 64 | cy.wrap(1) 65 | .if('equal', 1) 66 | .then((n) => { 67 | expect(n, 'if n').to.equal(1) 68 | console.log('if path, n = %d', n) 69 | return 101 70 | }) 71 | .finally() 72 | .should('equal', 101) 73 | }) 74 | 75 | it('yields the IF subject', () => { 76 | cy.wrap(1) 77 | .if('equal', 1) 78 | .then((n) => { 79 | expect(n, 'if n').to.equal(1) 80 | console.log('if path, n = %d', n) 81 | return 101 82 | }) 83 | .else() 84 | .then(() => -1) 85 | .finally() 86 | .should('equal', 101) 87 | }) 88 | 89 | it('yields the ELSE subject', () => { 90 | cy.wrap(1) 91 | .if('equal', 42) 92 | .then((n) => { 93 | expect(n, 'if n').to.equal(1) 94 | console.log('if path, n = %d', n) 95 | return 101 96 | }) 97 | .else() 98 | .then(() => -1) 99 | .finally() 100 | .should('equal', -1) 101 | }) 102 | 103 | it('calls else command before finally', () => { 104 | cy.wrap(1) 105 | // .if() comes from the cypress-if plugin 106 | // https://github.com/bahmutov/cypress-if 107 | .if('equals', 2) 108 | .log('if branch') 109 | .then(cy.spy().as('if')) 110 | .else() 111 | .log('else branch') 112 | .then(cy.spy().as('else')) 113 | .finally() 114 | .log('finally') 115 | .then(cy.spy().as('finally')) 116 | cy.get('@else').should('have.been.calledOnce') 117 | cy.get('@finally').should('have.been.calledOnce') 118 | cy.get('@if').should('not.be.called') 119 | cy.log('**else was called before finally**') 120 | cy.get('@finally').then((fin) => { 121 | cy.get('@else').should('have.been.calledBefore', fin) 122 | }) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /cypress/e2e/find.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('cy.find', () => { 7 | it('takes the if branch', () => { 8 | cy.visit('cypress/checkbox.html') 9 | cy.get('#app1') 10 | .find('#enrolled') 11 | .if('exist') 12 | .then(cy.spy().as('if')) 13 | .log('checkbox found') 14 | .else() 15 | .log('checkbox not found') 16 | .then(cy.spy().as('else')) 17 | cy.get('@if').should('have.been.called') 18 | cy.get('@else').should('not.have.been.called') 19 | }) 20 | 21 | it('takes the else branch', () => { 22 | cy.visit('cypress/checkbox.html') 23 | // the checkbox should not be checked 24 | cy.get('#enrolled').should('not.be.checked') 25 | 26 | cy.get('#app1') 27 | .find('#enrolled') 28 | .if('checked') 29 | .then(cy.spy().as('if')) 30 | .log('checkbox found') 31 | .else() 32 | .log('checkbox not found') 33 | .then(cy.spy().as('else')) 34 | cy.get('@else').should('have.been.called') 35 | cy.get('@if').should('not.have.been.called') 36 | }) 37 | 38 | // https://github.com/bahmutov/cypress-if/issues/32 39 | it('checks if the element exists (it does not)', () => { 40 | cy.visit('cypress/checkbox.html') 41 | cy.get('#app1') 42 | .find('#doest-not-exist') 43 | .if('exist') 44 | .then(cy.spy().as('if')) 45 | .log('checkbox found') 46 | .else() 47 | .log('checkbox not found') 48 | .then(cy.spy().as('else')) 49 | cy.get('@else').should('have.been.called') 50 | cy.get('@if').should('not.have.been.called') 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /cypress/e2e/has-attribute.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('has attribute assertion', () => { 7 | beforeEach(() => { 8 | cy.visit('cypress/terms.html') 9 | }) 10 | 11 | it('has attribute present', () => { 12 | cy.get('#submit') 13 | .if('have.attr', 'id') 14 | .log('button has an id') 15 | .else() 16 | .raise(new Error('button should have an id')) 17 | }) 18 | 19 | it( 20 | 'has attribute present after delay', 21 | { defaultCommandTimeout: 2000 }, 22 | () => { 23 | cy.get('#submit').should('have.attr', 'data-x') 24 | cy.get('#submit') 25 | .if('have.attr', 'data-x') 26 | .invoke('attr', 'data-x') 27 | .should('equal', '123') 28 | .else() 29 | .raise(new Error('data-x not found')) 30 | }, 31 | ) 32 | 33 | it( 34 | 'has attribute with matching value present after delay', 35 | { defaultCommandTimeout: 2000 }, 36 | () => { 37 | cy.get('#submit').should('have.attr', 'data-x') 38 | cy.get('#submit') 39 | .if('have.attr', 'data-x', '123') 40 | .log('data-X found') 41 | .else() 42 | .raise(new Error('data-x not found')) 43 | }, 44 | ) 45 | 46 | it( 47 | 'has attribute with a different value', 48 | { defaultCommandTimeout: 2000 }, 49 | () => { 50 | cy.get('#submit').should('have.attr', 'data-x') 51 | cy.get('#submit') 52 | // the attribute is present, but has a different value 53 | .if('have.attr', 'data-x', '99') 54 | .raise(new Error('data-x has wrong value')) 55 | .else('data-x value is correct') 56 | }, 57 | ) 58 | }) 59 | -------------------------------------------------------------------------------- /cypress/e2e/has-class.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('has class assertion', () => { 7 | beforeEach(() => { 8 | cy.visit('cypress/index.html') 9 | }) 10 | 11 | it('has active class', () => { 12 | cy.get('#fruits').invoke('addClass', 'active') 13 | // check if the fruits element has class "active" 14 | cy.get('#fruits') 15 | .if('have.class', 'active') 16 | .log('has class active') 17 | .then(cy.spy().as('if')) 18 | .else() 19 | .then(cy.spy().as('else')) 20 | cy.get('@if').should('have.been.called') 21 | cy.get('@else').should('not.have.been.called') 22 | }) 23 | 24 | it('has no active class', () => { 25 | cy.get('#fruits') 26 | .if('have.class', 'active') 27 | .log('has class active') 28 | .then(cy.spy().as('if')) 29 | .else() 30 | .then(cy.spy().as('else')) 31 | cy.get('@else').should('have.been.called') 32 | cy.get('@if').should('not.have.been.called') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /cypress/e2e/if-should.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | it('checks that there are 3 items if they exist', () => { 7 | cy.visit('cypress/index.html') 8 | cy.get('#fruits li').if('exist').should('have.length', 3) 9 | }) 10 | 11 | it('skips assertion if there are no items', () => { 12 | cy.visit('cypress/index.html') 13 | cy.get('#does-not-exist li', { timeout: 1000 }) 14 | .if('exist') 15 | .should('have.length', 3) 16 | cy.log('all good') 17 | }) 18 | -------------------------------------------------------------------------------- /cypress/e2e/input-value.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('Input element', () => { 7 | it('types again if the value was corrupted', () => { 8 | cy.visit('cypress/funky-input.html') 9 | cy.get('#name') 10 | .type('Cypress', { delay: 20 }) 11 | .if('not.have.value', 'Cypress') 12 | .clear() 13 | .type('Cypress') 14 | .else() 15 | .log('Input has expected value') 16 | .finally() 17 | .should('have.value', 'Cypress') 18 | }) 19 | 20 | it('have.value positive', () => { 21 | cy.wrap(Cypress.$('')) 22 | .if('have.value', 'foo') 23 | .then(cy.spy().as('if')) 24 | .else() 25 | .then(cy.spy().as('else')) 26 | cy.get('@if').should('be.calledOnce') 27 | cy.get('@else').should('not.be.called') 28 | }) 29 | 30 | it('have.value negative', () => { 31 | cy.wrap(Cypress.$('')) 32 | .if('have.value', 'bar') 33 | .then(cy.spy().as('if')) 34 | .else() 35 | .then(cy.spy().as('else')) 36 | cy.get('@else').should('be.calledOnce') 37 | cy.get('@if').should('not.be.called') 38 | }) 39 | 40 | it('have.value positive', () => { 41 | cy.wrap(Cypress.$('')) 42 | .if('have.value', 'foo') 43 | .then(cy.spy().as('if')) 44 | .else() 45 | .then(cy.spy().as('else')) 46 | cy.get('@if').should('be.calledOnce') 47 | cy.get('@else').should('not.be.called') 48 | }) 49 | 50 | context('not.have.value', () => { 51 | it('positive', () => { 52 | cy.wrap(Cypress.$('')) 53 | .if('not.have.value', 'foo') 54 | .then(cy.spy().as('if')) 55 | .else() 56 | .then(cy.spy().as('else')) 57 | cy.get('@else').should('be.calledOnce') 58 | cy.get('@if').should('not.be.called') 59 | }) 60 | 61 | it('negative', () => { 62 | cy.wrap(Cypress.$('')) 63 | .if('not.have.value', 'bar') 64 | .then(cy.spy().as('if')) 65 | .else() 66 | .then(cy.spy().as('else')) 67 | cy.get('@if').should('be.calledOnce') 68 | cy.get('@else').should('not.be.called') 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /cypress/e2e/issue-59.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | // https://github.com/bahmutov/cypress-if/issues/59 7 | 8 | function bar() { 9 | return ( 10 | cy 11 | .wrap('testing') 12 | .if() 13 | .then(() => cy.wrap('got it')) 14 | .else() 15 | .then(() => cy.wrap('else do')) 16 | // to correctly STOP the chaining if/else 17 | // from putting anything chained of bar() 18 | // need to add .finally() command 19 | .finally() 20 | ) 21 | } 22 | 23 | it('stops the chaining', () => { 24 | bar().then((it) => { 25 | cy.log(`result: ${it}`) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /cypress/e2e/more-than-n.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | it('clicks on the button that appears if there are more than 6 items', () => { 7 | cy.visit('cypress/list.html') 8 | // in 50% of the tests the page will show > 5 items 9 | // and will show the button "Load more" 10 | cy.get('#fruits li') 11 | .if('have.length.above', 5) 12 | .root() 13 | .contains('button', 'Load more') 14 | .click() 15 | .else() 16 | .log('Few items, no button') 17 | }) 18 | -------------------------------------------------------------------------------- /cypress/e2e/not.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import '../../src' 4 | 5 | describe('Not', () => { 6 | it('not.equal', () => { 7 | cy.wrap(42).if('not.equal', 42).raise(new Error('should not be here')) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/e2e/null.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | // https://github.com/bahmutov/cypress-if/issues/38 7 | describe('null value', () => { 8 | it('handles null assertion', () => { 9 | cy.wrap(null) 10 | .if('null') 11 | .log('null value') 12 | .then(cy.spy().as('if')) 13 | .else() 14 | .then(cy.spy().as('else')) 15 | cy.get('@if').should('have.been.called') 16 | cy.get('@else').should('not.have.been.called') 17 | }) 18 | 19 | it('handles not.null assertion', () => { 20 | cy.wrap(null) 21 | .if('not.null') 22 | .log('null value') 23 | .then(cy.spy().as('if')) 24 | .else() 25 | .then(cy.spy().as('else')) 26 | cy.get('@else').should('have.been.called') 27 | cy.get('@if').should('not.have.been.called') 28 | }) 29 | 30 | it('handles 42 with not.null assertion', () => { 31 | cy.wrap(42) 32 | .if('not.null') 33 | .log('null value') 34 | .then(cy.spy().as('if')) 35 | .else() 36 | .then(cy.spy().as('else')) 37 | cy.get('@if').should('have.been.called') 38 | cy.get('@else').should('not.have.been.called') 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /cypress/e2e/raise-error.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | it('raises an error if wrong number of elements', () => { 7 | // prevent ".raise" from failing the test 8 | Cypress.Commands.overwrite('raise', cy.stub().as('raise')) 9 | 10 | cy.visit('cypress/index.html') 11 | // we have 3 items 12 | cy.get('#fruits li').should('have.length', 3) 13 | // force an error 14 | cy.get('#fruits li') 15 | .if('have.length', 1) 16 | .then(cy.spy().as('if')) 17 | .log('right number of elements') 18 | .else() 19 | .log('too many elements') 20 | .then(cy.spy().as('else')) 21 | .raise('Too many elements') 22 | cy.get('@else').should('have.been.calledOnce') 23 | cy.get('@if').should('not.have.been.called') 24 | }) 25 | 26 | it('raises an error if not the right number of elements', () => { 27 | // prevent ".raise" from failing the test 28 | Cypress.Commands.overwrite('raise', cy.stub().as('raise')) 29 | 30 | cy.visit('cypress/index.html') 31 | // we have 3 items 32 | cy.get('#fruits li').should('have.length', 3) 33 | // force an error 34 | cy.get('#fruits li') 35 | .if('not.have.length', 1) 36 | .log('wrong number of items') 37 | .then(cy.spy().as('else')) 38 | .raise('Too many elements') 39 | cy.get('@else').should('have.been.calledOnce') 40 | }) 41 | 42 | it('raises an error instance', () => { 43 | // prevent ".raise" from failing the test 44 | Cypress.Commands.overwrite('raise', cy.stub().as('raise')) 45 | 46 | cy.visit('cypress/index.html') 47 | // we have 3 items 48 | cy.get('#fruits li').should('have.length', 3) 49 | // force an error 50 | cy.get('#fruits li') 51 | .if('not.have.length', 1) 52 | .log('wrong number of items') 53 | .then(cy.spy().as('else')) 54 | // when using an Error instance (and not a string) 55 | // the error stack will point at this spec location 56 | .raise(new Error('Too many elements')) 57 | cy.get('@else').should('have.been.calledOnce') 58 | }) 59 | 60 | it.skip('raises an error if element does not exist', () => { 61 | // prevent ".raise" from failing the test 62 | Cypress.Commands.overwrite('raise', cy.stub().as('raise')) 63 | 64 | cy.visit('cypress/index.html') 65 | // force an error 66 | cy.get('#does-not-exist') 67 | .if('not.exist') 68 | .log('no such element') 69 | .then(cy.spy().as('else')) 70 | .raise('Cannot find it') 71 | cy.get('@else').should('have.been.calledOnce') 72 | }) 73 | -------------------------------------------------------------------------------- /cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | it('finds the li elements', () => { 7 | cy.visit('cypress/index.html') 8 | // if the list exists, it should have three items 9 | cy.get('#fruits li').if().should('have.length', 3) 10 | // if the list exists, it should have three items 11 | cy.get('#veggies li').if().should('have.length', 3) 12 | // if the button exists, it should have certain text 13 | // and then we click on it 14 | cy.get('button#load').if().should('have.text', 'Load').click() 15 | // if the button exists, click on it 16 | cy.get('button#does-not-exist').if().click() 17 | }) 18 | 19 | it('clicks on the button if it is visible', () => { 20 | cy.visit('cypress/index.html') 21 | cy.get('button#hidden').if('visible').click() 22 | // but we can click on the visible button 23 | cy.get('button#load').if('visible').click() 24 | }) 25 | 26 | it('works if nothing is attached', () => { 27 | cy.wrap(1).if('equal', 1) 28 | cy.wrap(1) 29 | .if('not.equal', 1) 30 | .then((/** @type number */ subject) => { 31 | expect(subject).to.not.equal(1) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /cypress/e2e/task.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('cy.task support', () => { 7 | before(() => { 8 | // confirm the task yields 42 9 | cy.task('get42').should('equal', 42) 10 | }) 11 | 12 | it('runs if branch after cy.task', () => { 13 | cy.task('get42') 14 | .if('equals', 42) 15 | .then(cy.spy().as('if')) 16 | .else() 17 | .then(cy.spy().as('else')) 18 | cy.get('@if').should('have.been.called') 19 | cy.get('@else').should('not.have.been.called') 20 | }) 21 | 22 | it('runs else branch after cy.task', () => { 23 | cy.task('get42') 24 | .if('equals', 99) 25 | .then(cy.spy().as('if')) 26 | .else() 27 | .then(cy.spy().as('else')) 28 | cy.get('@else').should('have.been.called') 29 | cy.get('@if').should('not.have.been.called') 30 | }) 31 | 32 | it('throws an error', () => { 33 | cy.task('throws') 34 | .if('failed') 35 | .then(cy.spy().as('if')) 36 | .log('cy.task has failed') 37 | .else() 38 | .then(cy.spy().as('else')) 39 | cy.get('@if').should('have.been.called') 40 | cy.get('@else').should('not.have.been.called') 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /cypress/e2e/terms-and-conditions.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import '../../src' 4 | 5 | it('submits the terms forms', () => { 6 | cy.visit('cypress/terms.html') 7 | cy.get('#agreed') 8 | cy.get('#agreed') 9 | .should('be.visible') 10 | .if('not.checked') 11 | .click() 12 | .log('clicked the checkbox') 13 | .else() 14 | .log('The user already agreed') 15 | cy.get('button#submit').click() 16 | }) 17 | 18 | it('submits the terms forms using cy.then', () => { 19 | cy.visit('cypress/terms.html') 20 | cy.get('#agreed').then(($input) => { 21 | if ($input.is(':checked')) { 22 | cy.log('The user already agreed') 23 | } else { 24 | cy.wrap($input).click() 25 | } 26 | }) 27 | cy.get('button#submit').click() 28 | }) 29 | -------------------------------------------------------------------------------- /cypress/e2e/traversal.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | it('traversals by chain id', () => { 7 | cy.visit('cypress/index.html') 8 | cy.get('#does-not-exist', { timeout: 2000 }) 9 | .if() 10 | .log('hmm') // should be skipped 11 | .get('#does-not-exist') // should be skipped 12 | .should('exist') // should be skipped 13 | .then(() => { 14 | throw new Error('Hmm, did not skip me') 15 | }) 16 | .else() 17 | .log('does not exist') 18 | }) 19 | -------------------------------------------------------------------------------- /cypress/e2e/url.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('cy.url support', () => { 7 | it('checks if url contains a give string (it does)', () => { 8 | cy.visit('cypress/index.html') 9 | cy.url().should('include', 'index.html').and('include', 'localhost') 10 | cy.url() 11 | .if('includes', 'index.html') 12 | .log('includes index.html') 13 | .then(cy.spy().as('if')) 14 | .else() 15 | .then(cy.spy().as('else')) 16 | cy.get('@if').should('have.been.called') 17 | cy.get('@else').should('not.have.been.called') 18 | }) 19 | 20 | it('url does not contain a string', () => { 21 | cy.visit('cypress/index.html') 22 | cy.url() 23 | .if('includes', 'acme.co') 24 | .log('running on production') 25 | .then(cy.spy().as('if')) 26 | .else() 27 | .log('running NOT on production') 28 | .then(cy.spy().as('else')) 29 | cy.get('@else').should('have.been.called') 30 | cy.get('@if').should('not.have.been.called') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /cypress/e2e/wrap.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import '../../src' 5 | 6 | describe('wrapped value', () => { 7 | it('performs an action if the wrapped value is equal to 42', () => { 8 | cy.wrap(42).if('equal', 42).then(cy.spy().as('action')).then(cy.log) 9 | cy.get('@action').should('have.been.calledOnce') 10 | }) 11 | 12 | it('does nothing if it is not 42', () => { 13 | cy.wrap(1).if('equal', 42).then(cy.spy().as('action')).then(cy.log) 14 | cy.get('@action').should('not.have.been.called') 15 | }) 16 | 17 | context('.else', () => { 18 | it('passes the subject to the else branch', () => { 19 | cy.wrap(1).if('equal', 42).log('if branch').else().should('equal', 1) 20 | }) 21 | 22 | it('passes the subject if().else()', () => { 23 | cy.wrap(1).if('equal', 42).else().should('equal', 1) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /cypress/enabled-button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

5 | The button might be disabled 6 | 7 |

8 |
9 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /cypress/funky-input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Your name?

4 | 10 | 11 | -------------------------------------------------------------------------------- /cypress/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 |
    14 |
  • Apples
  • 15 |
  • Grapes
  • 16 |
  • Kiwi
  • 17 |
18 | 19 | 20 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /cypress/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |
  • Apples
  • 5 |
  • Grapes
  • 6 |
  • Kiwi
  • 7 |
8 | 9 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /cypress/terms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |
11 | 12 | I agree to the terms & conditions. 13 |
14 | 15 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /img/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-if/26ac2680e308d2c577b2d0373d21dcbb7560f7cb/img/debug.png -------------------------------------------------------------------------------- /img/dialog-closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-if/26ac2680e308d2c577b2d0373d21dcbb7560f7cb/img/dialog-closed.png -------------------------------------------------------------------------------- /img/dialog-open.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-if/26ac2680e308d2c577b2d0373d21dcbb7560f7cb/img/dialog-open.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-if", 3 | "version": "0.0.0-development", 4 | "description": "Easy conditional if-else logic for your Cypress tests", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "scripts": { 8 | "test": "cypress run", 9 | "badges": "npx -p dependency-version-badge update-badge cypress", 10 | "semantic-release": "semantic-release", 11 | "types": "tsc" 12 | }, 13 | "files": [ 14 | "src" 15 | ], 16 | "keywords": [ 17 | "cypress-plugin" 18 | ], 19 | "author": "Gleb Bahmutov ", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "cypress": "14.3.0", 23 | "prettier": "3.2.5", 24 | "semantic-release": "^19.0.5", 25 | "typescript": "^5.5.2" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/bahmutov/cypress-if.git" 30 | }, 31 | "dependencies": { 32 | "debug": "^4.3.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "major": { 5 | "automerge": false 6 | }, 7 | "minor": { 8 | "automerge": true 9 | }, 10 | "prConcurrentLimit": 3, 11 | "prHourlyLimit": 2, 12 | "schedule": ["after 10pm and before 5am on every weekday", "every weekend"], 13 | "masterIssue": true, 14 | "labels": ["type: dependencies", "renovate"], 15 | "packageRules": [ 16 | { 17 | "packagePatterns": ["*"], 18 | "excludePackagePatterns": ["cypress", "debug"], 19 | "enabled": false 20 | } 21 | ], 22 | "ignorePaths": ["**/node_modules/**", "cypress-v9/**"] 23 | } 24 | -------------------------------------------------------------------------------- /src/index-v11.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('cypress-if') 2 | 3 | const isIfCommand = (cmd) => 4 | cmd && cmd.attributes && cmd.attributes.name === 'if' 5 | 6 | const skipCommand = (cmd) => { 7 | cmd.attributes.skip = true 8 | cmd.state = 'skipped' 9 | } 10 | 11 | function skipRestOfTheChain(cmd, chainerId) { 12 | while ( 13 | cmd && 14 | cmd.attributes.chainerId === chainerId && 15 | cmd.attributes.name !== 'finally' 16 | ) { 17 | debug('skipping "%s"', cmd.attributes.name) 18 | skipCommand(cmd) 19 | cmd = cmd.attributes.next 20 | } 21 | } 22 | 23 | function findMyIfSubject(elseCommandAttributes) { 24 | if (!elseCommandAttributes) { 25 | return 26 | } 27 | if (elseCommandAttributes.name === 'if') { 28 | return elseCommandAttributes.ifSubject 29 | } 30 | if ( 31 | !elseCommandAttributes.skip && 32 | !Cypress._.isNil(elseCommandAttributes.subject) 33 | ) { 34 | return elseCommandAttributes.subject 35 | } 36 | if (elseCommandAttributes.prev) { 37 | return findMyIfSubject(elseCommandAttributes.prev.attributes) 38 | } 39 | } 40 | 41 | function getCypressCurrentSubject() { 42 | if (typeof cy.currentSubject === 'function') { 43 | return cy.currentSubject() 44 | } 45 | // fallback for Cypress v9 and some early v10 versions 46 | return cy.state('subject') 47 | } 48 | 49 | // cy.if command 50 | Cypress.Commands.add( 51 | 'if', 52 | { prevSubject: true }, 53 | function (subject, assertion, assertionValue1, assertionValue2) { 54 | const cmd = cy.state('current') 55 | debug('if', cmd.attributes, 'subject', subject, 'assertion?', assertion) 56 | debug('next command', cmd.next) 57 | debug('if() current subject', getCypressCurrentSubject()) 58 | // console.log('subjects', cy.state('subjects')) 59 | // keep the subject, if there is an "else" branch 60 | // it can look it up to use 61 | cmd.attributes.ifSubject = subject 62 | 63 | // let's be friendly and if the user 64 | // wrote "if exists" just go with it 65 | if (assertion === 'exists') { 66 | assertion = 'exist' 67 | } 68 | 69 | let hasSubject = Boolean(subject) 70 | let assertionsPassed = true 71 | 72 | const evaluateAssertion = () => { 73 | try { 74 | if (Cypress._.isFunction(assertion)) { 75 | const result = assertion(subject) 76 | if (Cypress._.isBoolean(result)) { 77 | // function was a predicate 78 | if (!result) { 79 | throw new Error('Predicate function failed') 80 | } 81 | } 82 | } else if ( 83 | assertion.startsWith('not') || 84 | assertion.startsWith('have') 85 | ) { 86 | const parts = assertion.split('.') 87 | let assertionReduced = expect(subject).to 88 | parts.forEach((assertionPart, k) => { 89 | if (k === parts.length - 1) { 90 | if ( 91 | typeof assertionValue1 !== 'undefined' && 92 | typeof assertionValue2 !== 'undefined' 93 | ) { 94 | assertionReduced = assertionReduced[assertionPart]( 95 | assertionValue1, 96 | assertionValue2, 97 | ) 98 | } else if (typeof assertionValue1 !== 'undefined') { 99 | assertionReduced = 100 | assertionReduced[assertionPart](assertionValue1) 101 | } else { 102 | assertionReduced = assertionReduced[assertionPart] 103 | } 104 | } else { 105 | assertionReduced = assertionReduced[assertionPart] 106 | } 107 | }) 108 | } else { 109 | if ( 110 | typeof assertionValue1 !== 'undefined' && 111 | typeof assertionValue2 !== 'undefined' 112 | ) { 113 | expect(subject).to.be[assertion](assertionValue1, assertionValue2) 114 | } else if (typeof assertionValue1 !== 'undefined') { 115 | expect(subject).to.be[assertion](assertionValue1) 116 | } else { 117 | expect(subject).to.be[assertion] 118 | } 119 | } 120 | } catch (e) { 121 | console.error(e) 122 | assertionsPassed = false 123 | if (e.message.includes('Invalid Chai property')) { 124 | throw e 125 | } 126 | } 127 | } 128 | 129 | // check if the previous command was cy.task 130 | // and it has failed and it was expected 131 | if ( 132 | assertion === 'failed' && 133 | Cypress._.get(cmd, 'attributes.prev.attributes.name') === 'task' && 134 | Cypress._.isError(Cypress._.get(cmd, 'attributes.prev.attributes.error')) 135 | ) { 136 | debug('cy.task has failed and it was expected') 137 | // set the subject and the assertions to take the IF branch 138 | hasSubject = Cypress._.get(cmd, 'attributes.prev.attributes.error') 139 | } else { 140 | if (subject === null) { 141 | if (assertion === 'null') { 142 | hasSubject = true 143 | assertionsPassed = true 144 | } else if (assertion === 'not.null') { 145 | hasSubject = true 146 | assertionsPassed = false 147 | } 148 | } else if (hasSubject && assertion) { 149 | evaluateAssertion() 150 | } else if (subject === undefined && assertion) { 151 | evaluateAssertion() 152 | hasSubject = true 153 | } 154 | } 155 | 156 | const chainerId = cmd.attributes.chainerId 157 | if (!chainerId) { 158 | throw new Error('Command is missing chainer id') 159 | } 160 | 161 | if (!hasSubject || !assertionsPassed) { 162 | let nextCommand = cmd.attributes.next 163 | while (nextCommand && nextCommand.attributes.chainerId === chainerId) { 164 | debug( 165 | 'skipping the next "%s" command "%s"', 166 | nextCommand.attributes.type, 167 | nextCommand.attributes.name, 168 | ) 169 | // cy.log(`**skipping ${cmd.attributes.next.attributes.name}**`) 170 | if (nextCommand.attributes.name === 'else') { 171 | debug('else branch starts right away') 172 | nextCommand = null 173 | } else { 174 | debug('am skipping "%s"', nextCommand.attributes.name) 175 | debug(nextCommand.attributes) 176 | skipCommand(nextCommand) 177 | 178 | nextCommand = nextCommand.attributes.next 179 | if (nextCommand && nextCommand.attributes.name === 'else') { 180 | debug('stop skipping command on "else" command') 181 | nextCommand = null 182 | } 183 | } 184 | } 185 | 186 | if (subject) { 187 | debug('wrapping subject', subject) 188 | cy.wrap(subject, { log: false }) 189 | } 190 | return 191 | } else { 192 | // skip possible "else" branch 193 | debug('skipping a possible "else" branch') 194 | let nextCommand = cmd.attributes.next 195 | while (nextCommand && nextCommand.attributes.chainerId === chainerId) { 196 | debug( 197 | 'next command "%s" type "%s"', 198 | nextCommand.attributes.name, 199 | nextCommand.attributes.type, 200 | nextCommand.attributes, 201 | ) 202 | 203 | if (nextCommand.attributes.name === 'else') { 204 | // found the "else" command, start skipping 205 | debug('found the "else" branch command start') 206 | skipRestOfTheChain(nextCommand, chainerId) 207 | nextCommand = null 208 | } else { 209 | nextCommand = nextCommand.attributes.next 210 | } 211 | } 212 | } 213 | return subject 214 | }, 215 | ) 216 | 217 | Cypress.Commands.add('else', { prevSubject: true }, (subject, text) => { 218 | debug('else command, subject', subject) 219 | if (typeof subject === 'undefined') { 220 | // find the subject from the "if()" before 221 | subject = findMyIfSubject(cy.state('current').attributes) 222 | } 223 | if (typeof text !== 'undefined') { 224 | cy.log(text) 225 | } 226 | if (subject) { 227 | cy.wrap(subject, { log: false }) 228 | } 229 | }) 230 | 231 | Cypress.Commands.add('finally', { prevSubject: true }, (subject) => { 232 | debug('finally with the subject', subject) 233 | 234 | // notice: cy.log yields "null" 🤯 235 | // https://github.com/cypress-io/cypress/issues/23400 236 | if (typeof subject === 'undefined' || subject === null) { 237 | // find the subject from the "if()" before 238 | const currentCommand = cy.state('current').attributes 239 | debug('current command is finally', currentCommand) 240 | subject = findMyIfSubject(currentCommand) 241 | debug('found subject', subject) 242 | } 243 | if (subject) { 244 | cy.wrap(subject, { log: false }) 245 | } 246 | }) 247 | 248 | Cypress.Commands.overwrite('get', function (get, selector, options) { 249 | // can we see the next command already? 250 | const cmd = cy.state('current') 251 | debug(cmd) 252 | const next = cmd.attributes.next 253 | 254 | if (isIfCommand(next)) { 255 | if (selector.startsWith('@')) { 256 | try { 257 | return get(selector, options) 258 | } catch (e) { 259 | if (e.message.includes('could not find a registered alias for')) { 260 | return undefined 261 | } 262 | } 263 | } 264 | // disable the built-in assertion 265 | return get(selector, options).then( 266 | (getResult) => { 267 | debug('internal get result', getResult) 268 | return getResult 269 | }, 270 | (noResult) => { 271 | debug('no get result', noResult) 272 | }, 273 | ) 274 | } 275 | 276 | return get(selector, options) 277 | }) 278 | 279 | Cypress.Commands.overwrite( 280 | 'contains', 281 | function (contains, prevSubject, selector, text, options) { 282 | debug('cy.contains arguments number', arguments.length) 283 | if (arguments.length === 3) { 284 | text = selector 285 | selector = undefined 286 | } 287 | debug('cy.contains args', { prevSubject, selector, text, options }) 288 | 289 | const cmd = cy.state('current') 290 | debug(cmd) 291 | const next = cmd.attributes.next 292 | 293 | if (next && next.attributes.name === 'if') { 294 | // disable the built-in assertion 295 | return contains(prevSubject, selector, text, options).then( 296 | (getResult) => { 297 | debug('internal contains result', getResult) 298 | return getResult 299 | }, 300 | (noResult) => { 301 | debug('no contains result', noResult) 302 | }, 303 | ) 304 | } 305 | 306 | return contains(prevSubject, selector, text, options) 307 | }, 308 | ) 309 | 310 | Cypress.Commands.overwrite('not', function (notCommand, prevSubject, selector) { 311 | debug('cy.not args', { prevSubject, selector }) 312 | 313 | const cmd = cy.state('current') 314 | debug(cmd) 315 | const next = cmd.attributes.next 316 | 317 | if (next && next.attributes.name === 'if') { 318 | // disable the built-in assertion 319 | return notCommand(prevSubject, selector).then( 320 | (getResult) => { 321 | debug('internal cy.not result', getResult) 322 | return getResult 323 | }, 324 | (noResult) => { 325 | debug('no cy.not result', noResult) 326 | }, 327 | ) 328 | } 329 | 330 | return notCommand(prevSubject, selector, text, options) 331 | }) 332 | 333 | Cypress.Commands.overwrite( 334 | 'find', 335 | function (find, prevSubject, selector, options) { 336 | debug('cy.find args', { prevSubject, selector, options }) 337 | 338 | const cmd = cy.state('current') 339 | debug(cmd) 340 | const next = cmd.attributes.next 341 | 342 | if (next && next.attributes.name === 'if') { 343 | // disable the built-in assertion 344 | return find(prevSubject, selector, options).then( 345 | (getResult) => { 346 | debug('internal cy.find result', getResult) 347 | return getResult 348 | }, 349 | (noResult) => { 350 | debug('no cy.find result', noResult) 351 | }, 352 | ) 353 | } 354 | 355 | return find(prevSubject, selector, options) 356 | }, 357 | ) 358 | 359 | Cypress.Commands.overwrite('task', function (task, args, options) { 360 | debug('cy.task %o', { args, options }) 361 | 362 | const cmd = cy.state('current') 363 | if (cmd) { 364 | debug(cmd) 365 | const next = cmd.attributes.next 366 | 367 | if (next && next.attributes.name === 'if') { 368 | // disable the built-in assertion 369 | return task(args, options).then( 370 | (taskResult) => { 371 | debug('internal task result', taskResult) 372 | return taskResult 373 | }, 374 | (error) => { 375 | debug('task error', error) 376 | cmd.attributes.error = error 377 | }, 378 | ) 379 | } 380 | } 381 | 382 | return task(args, options) 383 | }) 384 | 385 | Cypress.Commands.add('raise', (x) => { 386 | if (Cypress._.isError(x)) { 387 | throw x 388 | } 389 | const e = new Error( 390 | String(x) + 391 | '\n' + 392 | 'cypress-if tip: pass an error instance to have correct stack', 393 | ) 394 | throw e 395 | }) 396 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A function that returns true if the value is good for "if" branch 3 | * No Cypress commands allowed. 4 | */ 5 | type PredicateFn = (x: any) => boolean 6 | 7 | /** 8 | * A function that uses Chai assertions inside. 9 | * No Cypress commands allowed. 10 | */ 11 | type AssertionFn = (x: any) => void 12 | 13 | declare namespace Cypress { 14 | interface Chainable { 15 | /** 16 | * Child `.if()` command to start an optional chain 17 | * depending on the subject 18 | * @param assertion Chai assertion (optional, existence by default) 19 | * @param value Assertion value 20 | * @example 21 | * cy.get('#close').if('visible').click() 22 | * cy.wrap(1).if('equal', 1).should('equal', 1) 23 | */ 24 | if( 25 | this: Chainable, 26 | assertion?: string, 27 | value1?: any, 28 | value2?: any, 29 | ): Chainable 30 | 31 | /** 32 | * Child `.if()` command to start an optional chain 33 | * depending on the subject 34 | * @param callback Predicate function (returning a Boolean value) 35 | * @example 36 | * cy.wrap(1).if(n => n % 2 === 0)... 37 | */ 38 | if(this: Chainable, callback: PredicateFn): Chainable 39 | 40 | /** 41 | * Child `.if()` command to start an optional chain 42 | * depending on the subject 43 | * @param callback Function with Chai assertions 44 | * @example 45 | * cy.wrap(1).if(n => expect(n).to.equal(1))... 46 | */ 47 | if(this: Chainable, callback: AssertionFn): Chainable 48 | 49 | /** 50 | * Creates new chain of commands that only 51 | * execute if the previous `.if()` command skipped 52 | * the "IF" branch. Note: `.if()` passes its subject 53 | * to the `.else()` 54 | * You can also print a message if the ELSE branch 55 | * is taken 56 | * @param message Message to print to the console. Optional. 57 | * @example 58 | * cy.get('checkox#agree') 59 | * .if('checked').log('Already agreed') 60 | * .else().check() 61 | * @example 62 | * cy.get('...') 63 | * .if('not.visible').log('Not visible') 64 | * .else('visible') 65 | */ 66 | else(this: Chainable, message?: any): Chainable 67 | 68 | /** 69 | * Finishes if/else commands and continues 70 | * with the subject yielded by the original command 71 | * or if/else path taken 72 | * @example 73 | * cy.get('checkbox') 74 | * .if('not.checked').check() 75 | * .else().log('already checked') 76 | * .finally().should('be.checked') 77 | */ 78 | finally(this: Chainable): Chainable 79 | 80 | /** 81 | * A simple way to throw an error 82 | * @example 83 | * cy.get('li') 84 | * .if('not.have.length', 3) 85 | * .raise('wrong number of todo items') 86 | */ 87 | raise(x: string | Error): Chainable 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('cypress-if') 2 | 3 | const [major] = Cypress.version.split('.') 4 | if (major < 12) { 5 | require('./index-v11') 6 | } else { 7 | const isIfCommand = (cmd) => 8 | cmd && cmd.attributes && cmd.attributes.name === 'if' 9 | 10 | const skipCommand = (cmd) => { 11 | cmd.attributes.skip = true 12 | cmd.state = 'skipped' 13 | } 14 | 15 | function skipRestOfTheChain(cmd, chainerId) { 16 | while ( 17 | cmd && 18 | cmd.attributes.chainerId === chainerId && 19 | cmd.attributes.name !== 'finally' 20 | ) { 21 | debug('skipping "%s"', cmd.attributes.name) 22 | skipCommand(cmd) 23 | cmd = cmd.attributes.next 24 | } 25 | } 26 | 27 | function findMyIfSubject(elseCommandAttributes) { 28 | if (!elseCommandAttributes) { 29 | return 30 | } 31 | if (elseCommandAttributes.name === 'if') { 32 | return elseCommandAttributes.ifSubject 33 | } 34 | if ( 35 | !elseCommandAttributes.skip && 36 | !Cypress._.isNil(elseCommandAttributes.subject) 37 | ) { 38 | return elseCommandAttributes.subject 39 | } 40 | if (elseCommandAttributes.prev) { 41 | return findMyIfSubject(elseCommandAttributes.prev.attributes) 42 | } 43 | } 44 | 45 | function getCypressCurrentSubject() { 46 | if (typeof cy.currentSubject === 'function') { 47 | return cy.currentSubject() 48 | } 49 | // fallback for Cypress v9 and some early v10 versions 50 | return cy.state('subject') 51 | } 52 | 53 | // cy.if command 54 | Cypress.Commands.add( 55 | 'if', 56 | { prevSubject: true }, 57 | function (subject, assertion, assertionValue1, assertionValue2) { 58 | const cmd = cy.state('current') 59 | debug('if', cmd.attributes, 'subject', subject, 'assertion?', assertion) 60 | debug('next command', cmd.next) 61 | debug('if() current subject', getCypressCurrentSubject()) 62 | // console.log('subjects', cy.state('subjects')) 63 | // keep the subject, if there is an "else" branch 64 | // it can look it up to use 65 | cmd.attributes.ifSubject = subject 66 | 67 | // let's be friendly and if the user 68 | // wrote "if exists" just go with it 69 | if (assertion === 'exists') { 70 | assertion = 'exist' 71 | } 72 | 73 | let hasSubject = Boolean(subject) 74 | let assertionsPassed = true 75 | 76 | const evaluateAssertion = () => { 77 | try { 78 | if (Cypress._.isFunction(assertion)) { 79 | const result = assertion(subject) 80 | if (Cypress._.isBoolean(result)) { 81 | // function was a predicate 82 | if (!result) { 83 | throw new Error('Predicate function failed') 84 | } 85 | } 86 | } else if ( 87 | assertion.startsWith('not') || 88 | assertion.startsWith('have') 89 | ) { 90 | const parts = assertion.split('.') 91 | let assertionReduced = expect(subject).to 92 | parts.forEach((assertionPart, k) => { 93 | if (k === parts.length - 1) { 94 | if ( 95 | typeof assertionValue1 !== 'undefined' && 96 | typeof assertionValue2 !== 'undefined' 97 | ) { 98 | assertionReduced = assertionReduced[assertionPart]( 99 | assertionValue1, 100 | assertionValue2, 101 | ) 102 | } else if (typeof assertionValue1 !== 'undefined') { 103 | assertionReduced = 104 | assertionReduced[assertionPart](assertionValue1) 105 | } else { 106 | assertionReduced = assertionReduced[assertionPart] 107 | } 108 | } else { 109 | assertionReduced = assertionReduced[assertionPart] 110 | } 111 | }) 112 | } else { 113 | if ( 114 | typeof assertionValue1 !== 'undefined' && 115 | typeof assertionValue2 !== 'undefined' 116 | ) { 117 | expect(subject).to.be[assertion](assertionValue1, assertionValue2) 118 | } else if (typeof assertionValue1 !== 'undefined') { 119 | expect(subject).to.be[assertion](assertionValue1) 120 | } else { 121 | expect(subject).to.be[assertion] 122 | } 123 | } 124 | } catch (e) { 125 | console.error(e) 126 | assertionsPassed = false 127 | if (e.message.includes('Invalid Chai property')) { 128 | throw e 129 | } 130 | } 131 | } 132 | 133 | // check if the previous command was cy.task 134 | // and it has failed and it was expected 135 | if ( 136 | assertion === 'failed' && 137 | Cypress._.get(cmd, 'attributes.prev.attributes.name') === 'task' && 138 | Cypress._.isError( 139 | Cypress._.get(cmd, 'attributes.prev.attributes.error'), 140 | ) 141 | ) { 142 | debug('cy.task has failed and it was expected') 143 | // set the subject and the assertions to take the IF branch 144 | hasSubject = Cypress._.get(cmd, 'attributes.prev.attributes.error') 145 | } else { 146 | if (subject === null) { 147 | if (assertion === 'null') { 148 | hasSubject = true 149 | assertionsPassed = true 150 | } else if (assertion === 'not.null') { 151 | hasSubject = true 152 | assertionsPassed = false 153 | } 154 | } else if (hasSubject && assertion) { 155 | evaluateAssertion() 156 | } else if (subject === undefined && assertion) { 157 | evaluateAssertion() 158 | hasSubject = true 159 | } 160 | } 161 | 162 | const chainerId = cmd.attributes.chainerId 163 | if (!chainerId) { 164 | throw new Error('Command is missing chainer id') 165 | } 166 | 167 | if (!hasSubject || !assertionsPassed) { 168 | let nextCommand = cmd.attributes.next 169 | while (nextCommand && nextCommand.attributes.chainerId === chainerId) { 170 | debug( 171 | 'skipping the next "%s" command "%s"', 172 | nextCommand.attributes.type, 173 | nextCommand.attributes.name, 174 | ) 175 | // cy.log(`**skipping ${cmd.attributes.next.attributes.name}**`) 176 | if (nextCommand.attributes.name === 'else') { 177 | debug('else branch starts right away') 178 | nextCommand = null 179 | } else { 180 | debug('am skipping "%s"', nextCommand.attributes.name) 181 | debug(nextCommand.attributes) 182 | skipCommand(nextCommand) 183 | 184 | nextCommand = nextCommand.attributes.next 185 | if (nextCommand && nextCommand.attributes.name === 'else') { 186 | debug('stop skipping command on "else" command') 187 | nextCommand = null 188 | } 189 | } 190 | } 191 | 192 | if (subject) { 193 | debug('wrapping subject', subject) 194 | cy.wrap(subject, { log: false }) 195 | } 196 | return 197 | } else { 198 | // skip possible "else" branch 199 | debug('skipping a possible "else" branch') 200 | let nextCommand = cmd.attributes.next 201 | while (nextCommand && nextCommand.attributes.chainerId === chainerId) { 202 | debug( 203 | 'next command "%s" type "%s"', 204 | nextCommand.attributes.name, 205 | nextCommand.attributes.type, 206 | nextCommand.attributes, 207 | ) 208 | 209 | if (nextCommand.attributes.name === 'else') { 210 | // found the "else" command, start skipping 211 | debug('found the "else" branch command start') 212 | skipRestOfTheChain(nextCommand, chainerId) 213 | nextCommand = null 214 | } else { 215 | nextCommand = nextCommand.attributes.next 216 | } 217 | } 218 | } 219 | return subject 220 | }, 221 | ) 222 | 223 | Cypress.Commands.add('else', { prevSubject: true }, (subject, text) => { 224 | debug('else command, subject', subject) 225 | if (typeof subject === 'undefined') { 226 | // find the subject from the "if()" before 227 | subject = findMyIfSubject(cy.state('current').attributes) 228 | } 229 | if (typeof text !== 'undefined') { 230 | cy.log(text) 231 | } else { 232 | debug('nothing to log for else branch') 233 | } 234 | if (subject) { 235 | cy.wrap(subject, { log: false }) 236 | } 237 | }) 238 | 239 | Cypress.Commands.add('finally', { prevSubject: true }, (subject) => { 240 | debug('finally with the subject', subject) 241 | 242 | // notice: cy.log yields "null" 🤯 243 | // https://github.com/cypress-io/cypress/issues/23400 244 | if (typeof subject === 'undefined' || subject === null) { 245 | // find the subject from the "if()" before 246 | const currentCommand = cy.state('current').attributes 247 | debug('current command is finally', currentCommand) 248 | subject = findMyIfSubject(currentCommand) 249 | debug('found subject', subject) 250 | } 251 | if (subject) { 252 | cy.wrap(subject, { log: false }) 253 | } 254 | }) 255 | 256 | Cypress.Commands.overwriteQuery('get', function (get, selector, options) { 257 | // can we see the next command already? 258 | const cmd = cy.state('current') 259 | debug(cmd) 260 | const next = cmd.attributes.next 261 | 262 | const innerFn = get.call(this, selector, options) 263 | 264 | if (isIfCommand(next)) { 265 | if (selector.startsWith('@')) { 266 | return (subject) => { 267 | try { 268 | return innerFn(subject) 269 | } catch (e) { 270 | if (e.message.includes('could not find a registered alias for')) { 271 | return undefined 272 | } 273 | } 274 | } 275 | } 276 | // disable the built-in assertion 277 | return (subject) => { 278 | const res = innerFn(subject) 279 | if (res && res.length) { 280 | debug('internal get result', res) 281 | return res 282 | } 283 | debug('no get result') 284 | } 285 | } 286 | 287 | return (subject) => innerFn(subject) 288 | }) 289 | 290 | Cypress.Commands.overwriteQuery( 291 | 'contains', 292 | function (contains, selector, text, options) { 293 | debug('cy.contains arguments number', arguments.length) 294 | if (arguments.length === 2) { 295 | text = selector 296 | selector = undefined 297 | } 298 | debug('cy.contains args', { selector, text, options }) 299 | 300 | const cmd = cy.state('current') 301 | debug(cmd) 302 | const next = cmd.attributes.next 303 | const innerFn = contains.call(this, selector, text, options) 304 | 305 | if (isIfCommand(next)) { 306 | // disable the built-in assertion 307 | return (subject) => { 308 | const res = innerFn(subject) 309 | if (res && res.length) { 310 | debug('internal contains result', res) 311 | return res 312 | } 313 | debug('no contains result') 314 | } 315 | } 316 | 317 | return (subject) => innerFn(subject) 318 | }, 319 | ) 320 | 321 | Cypress.Commands.overwriteQuery('find', function (find, selector, options) { 322 | debug('cy.find args', { selector, options }) 323 | 324 | const cmd = cy.state('current') 325 | debug(cmd) 326 | const next = cmd.attributes.next 327 | const innerFn = find.call(this, selector, options) 328 | 329 | if (isIfCommand(next)) { 330 | // disable the built-in assertion 331 | return (subject) => { 332 | const res = innerFn(subject) 333 | if (res && res.length) { 334 | debug('internal cy.find result', res) 335 | return res 336 | } 337 | debug('no cy.find result') 338 | } 339 | } 340 | 341 | return (subject) => innerFn(subject) 342 | }) 343 | 344 | Cypress.Commands.overwrite('task', function (task, args, options) { 345 | debug('cy.task %o', { args, options }) 346 | 347 | const cmd = cy.state('current') 348 | if (cmd) { 349 | debug(cmd) 350 | const next = cmd.attributes.next 351 | 352 | if (isIfCommand(next)) { 353 | // disable the built-in assertion 354 | return task(args, options).then( 355 | (taskResult) => { 356 | debug('internal task result', taskResult) 357 | return taskResult 358 | }, 359 | (error) => { 360 | debug('task error', error) 361 | cmd.attributes.error = error 362 | }, 363 | ) 364 | } 365 | } 366 | 367 | return task(args, options) 368 | }) 369 | 370 | Cypress.Commands.add('raise', (x) => { 371 | if (Cypress._.isError(x)) { 372 | throw x 373 | } 374 | const e = new Error( 375 | String(x) + 376 | '\n' + 377 | 'cypress-if tip: pass an error instance to have correct stack', 378 | ) 379 | throw e 380 | }) 381 | 382 | Cypress.Commands.overwriteQuery('not', function (notCommand, selector) { 383 | debug('cy.not args', { selector }) 384 | 385 | const cmd = cy.state('current') 386 | debug(cmd) 387 | const next = cmd.attributes.next 388 | const innerFn = notCommand.call(this, selector) 389 | 390 | if (isIfCommand(next)) { 391 | // disable the built-in assertion 392 | return (subject) => { 393 | const res = innerFn(subject) 394 | if (res && res.length) { 395 | debug('internal not result', res) 396 | return res 397 | } 398 | debug('no not result') 399 | } 400 | } 401 | 402 | return (subject) => innerFn(subject) 403 | }) 404 | } 405 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6", "DOM"], 4 | "types": ["cypress"], 5 | "noEmit": true, 6 | "allowJs": true 7 | }, 8 | "include": ["src/index-v11.js", "src/index.d.ts", "cypress/**/*.js"] 9 | } 10 | --------------------------------------------------------------------------------