├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yaml ├── .gitignore ├── .prettierrc ├── .vscode ├── extentions.json └── settings.json ├── CHANGELOG.md ├── DEVELOP.md ├── LICENSE ├── README.md ├── app ├── index.html ├── index.js └── lib │ └── jquery-3.2.1.js ├── cypress.config.js ├── cypress ├── e2e │ ├── attribute.cy.js │ ├── copied │ │ └── request.cy.js │ ├── request.cy.js │ ├── text.cy.js │ ├── then.cy.js │ ├── to.cy.js │ └── utils │ │ └── whitespace.cy.js └── support │ ├── bundle.js │ ├── common.js │ └── source.js ├── docs ├── attribute.md ├── request.md ├── text.md ├── then.md └── to.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── attribute.js ├── index.js ├── request.js ├── text.js ├── then.js ├── to.js └── utils │ ├── commandError.js │ ├── commandQueue.js │ ├── errorMessages.js │ ├── isJquery.js │ ├── optionValidator.js │ └── whitespace.js ├── tsconfig.json └── types ├── attribute.d.ts ├── generic.d.ts ├── index.d.ts ├── text.d.ts ├── then.d.ts └── to.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | # Used by various tools. Among which: VS Code, Prettier 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | end_of_line = lf 12 | 13 | [*.ts] 14 | quote_type = single 15 | 16 | [*.js] 17 | quote_type = single 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.yaml] 25 | indent_style = space 26 | indent_size = 2 27 | 28 | [{package.json,package-lock.json}] 29 | indent_style = space 30 | indent_size = 2 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | app 3 | dist 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | module.exports = { 3 | plugins: ['sonarjs', 'cypress'], 4 | extends: ['google', 'plugin:sonarjs/recommended', 'plugin:cypress/recommended'], 5 | parserOptions: { 6 | sourceType: 'module', 7 | ecmaVersion: 8, 8 | }, 9 | env: { 10 | node: true, 11 | es6: true, 12 | }, 13 | rules: { 14 | 'indent': ['error', 4], 15 | 'max-len': ['error', 100], 16 | 'linebreak-style': 'off', 17 | 'no-multi-spaces': [ 18 | 'error', 19 | { 20 | exceptions: { 21 | VariableDeclarator: true, 22 | }, 23 | }, 24 | ], 25 | 'object-curly-spacing': ['error', 'always'], 26 | 'comma-dangle': [ 27 | 'error', 28 | { 29 | arrays: 'always-multiline', 30 | objects: 'always-multiline', 31 | imports: 'always-multiline', 32 | exports: 'always-multiline', 33 | functions: 'never', 34 | }, 35 | ], 36 | 'space-before-function-paren': [ 37 | 'error', 38 | { 39 | anonymous: 'always', 40 | named: 'never', 41 | asyncArrow: 'always', 42 | }, 43 | ], 44 | 'quotes': ['error', 'single', { avoidEscape: true }], 45 | }, 46 | overrides: [ 47 | { 48 | files: ['cypress/e2e/**/*'], 49 | rules: { 50 | 'sonarjs/no-identical-functions': 'warn', 51 | 'sonarjs/no-duplicate-string': ['warn', 5], 52 | 'no-invalid-this': 'off', 53 | }, 54 | }, 55 | ], 56 | }; 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | # To Reproduce 14 | Steps or code to reproduce the behavior. 15 | 16 | # Expected behavior 17 | A clear and concise description of what you expected to happen. 18 | 19 | # Screenshots 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | # Versions and such: 23 | - OS: 24 | - Cypress version: 25 | - Cypress browser: 26 | - Cypress-commands version: 27 | 28 | # Additional context 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Suggest improvements to the documentation 4 | title: '' 5 | labels: documentation 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Changes should be made to 11 | 12 | - [ ] General documentation (ex. README.md) 13 | - [ ] Command documentation (ex. docs/then.md) 14 | - [ ] Command type definitions / Intellisense 15 | 16 | # Describe the improvement 17 | Which file and which part should be changed? 18 | 19 | # Why? 20 | Why do you think there should be a change? 21 | 22 | # Possible improvements 23 | How can we improve it? You probably don't need to answer this question if you are writing the improvements yourself. 24 | 25 | # Additional context 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a command or feature 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Type of feature 11 | 12 | - [ ] Add a new command 13 | - [ ] Extend a default Cypress command 14 | - [ ] Change a Cypress-commands command 15 | - [ ] Other 16 | 17 | # What do you want to accomplish? 18 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 19 | 20 | 21 | # Why? 22 | A short description of why you want to accomplish the things mentioned above. 23 | 24 | 25 | # Describe possible implementations 26 | 27 | A clear and concise description of what you want to happen. 28 | 29 | 30 | # Additional context 31 | Add any other context or screenshots about the request here. 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Closes # 2 | 3 | - [ ] Tests 4 | - [ ] Documentation 5 | - [ ] Type definitions 6 | - [ ] Ready for merge 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [develop] 9 | pull_request: 10 | branches: [develop] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | node-version: [16.x, 18.x] 21 | 22 | # Runs with `package.json` use the version defined in `~/package.lock.json`. 23 | cypress-version: [package.json] 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | cache: 'npm' 32 | - run: npm ci 33 | - if: ${{ matrix.cypress-version != 'package.json' }} 34 | run: npm install cypress@${{ matrix.cypress-version }} 35 | - run: npm run lint 36 | - run: npm run test:source 37 | - run: npm run bundle 38 | - run: npm run test:bundle 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cypress things 2 | screenshots 3 | videos 4 | 5 | # MacOS 6 | .DS_Store 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | 67 | # next.js build output 68 | .next 69 | 70 | # Generated files 71 | dist 72 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "bracketSpacing": true, 5 | "trailingComma": "es5", 6 | "proseWrap": "always", 7 | "printWidth": 100, 8 | "quoteProps": "consistent" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extentions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## 3.0.0 - 2022-06-06 6 | 7 | ## Breaking changes 8 | 9 | ### Dropped support for node@10 10 | 11 | Cypress 10 supports node@16 or up, to simplify CI we're no longer testing with Node@10. It should 12 | still work, but it's no longer guaranteed. 13 | 14 | ### Dropped support for Cypress@<=9 15 | 16 | Cypress 10 introduces a new configuration format. To simplify CI we're no longer testing with 17 | Cypress@9 or lower. It should still work, but it's no longer guaranteed. 18 | 19 | If you're not ready to upgrade to Cypress 10 yet, stick to cypress-commands@2. 20 | 21 | ## Changes 22 | 23 | ### Added support for Cypress@10 24 | 25 | Cypress 10 introduces breaking changes to the plugin system. None of these breaking changes have 26 | impact on `cypress-commands`. Changes to ensure support for Cypress@10 are mainly documentation 27 | changes. 28 | 29 | ## 2.0.1 - 2021-10-08 30 | 31 | ### Fixes 32 | 33 | - Type definition for `.attribute()` no longer requires you to pass an `options` object. 34 | 35 | ## 2.0.0 - 2021-09-13 36 | 37 | ### Breaking changes 38 | 39 | #### Whitespace handling for zero-width whitespace in `.text()` and `.attribute()` 40 | 41 | Whitespace handling in `.text()` and `.attribute()` has been changed to no longer consider 42 | zero-width whitespace to be whitespace in modes `{whitespace: 'simplify'}` and 43 | `{whitespace: 'keep-newline'}`. Mode `{whitespace: 'keep'}` has not changed. 44 | 45 | 46 | ```html 47 |
super\u200Bcalifragilistic\u200Bexpialidocious
48 | ``` 49 | 50 | ```javascript 51 | // Old situation 52 | cy.get('div').text().should('equal', 'super califragilistic expialidocious'); 53 | ``` 54 | 55 | ```javascript 56 | // New situation 57 | cy.get('div').text().should('equal', 'supercalifragilisticexpialidocious'); 58 | ``` 59 | 60 | When using `.text()` on elements containing the `` tag: `` is now considered a zero-width 61 | space and will thus be removed with whitespace `simplify` and `keep-newline` as described above. 62 | 63 | 64 | ```html 65 |
supercalifragilisticexpialidocious
66 | ``` 67 | 68 | ```javascript 69 | // Old situation 70 | cy.get('div').text().should('equal', 'super califragilistic expialidocious'); 71 | ``` 72 | 73 | ```javascript 74 | // New situation 75 | cy.get('div').text().should('equal', 'supercalifragilisticexpialidocious'); 76 | ``` 77 | 78 | #### Output order of `.text()` 79 | 80 | When using `.text({ depth: Number })` the order of texts has been changed to better reflect what the 81 | user sees. It will now first traverse all the way to the deepest point, before going sideways. This 82 | will make `.text()` behave much better with inline styling and links. 83 | 84 | 85 | ```html 86 |
87 | parent div top 88 |
89 | child div 90 |
91 | parent div middle 92 |
93 | second-child div 94 |
95 | parent div bottom 96 |
97 | ``` 98 | 99 | ```javascript 100 | // Old situation 101 | // Note how the first part of the string is the various parts of `div.parent` 102 | cy.get('parent') 103 | .text({ depth: 1 }) 104 | .should('equal', 'parent div top parent div middle parent div bottom child div second-child div'); 105 | ``` 106 | 107 | ```javascript 108 | // New situation 109 | cy.get('div') 110 | .text({ depth: 1 }) 111 | .should('equal', 'parent div top child div parent div middle second-child div parent div bottom'); 112 | ``` 113 | 114 | Inline text formatting: 115 | 116 | 117 | ```html 118 |
119 | Text with some styling and a link. 120 |
121 | ``` 122 | 123 | ```javascript 124 | // Old situation 125 | cy.get('div').text({ depth: 1 }).should('equal', 'Text with styling and . some a link'); 126 | ``` 127 | 128 | ```javascript 129 | // New situation 130 | cy.get('div').text({ depth: 1 }).should('equal', 'Text with some styling and a link.'); 131 | ``` 132 | 133 | #### Stricter types 134 | 135 | Types have been made stricter for `.attribute()`, `text()`, and `.to()`. This is a great improvement 136 | for TypeScript users as it reduces any manual casting. It allows for things like: 137 | 138 | ```typescript 139 | cy.get('div') 140 | .text() // yields type 'string | string[]' 141 | .to('array') // yields type 'string[]' 142 | .then((texts: string[]) => ...); 143 | ``` 144 | 145 | ```typescript 146 | cy.get('div') 147 | .attribute('class') // yields type 'string | string[]' 148 | .to('array') // yields type 'string[]' 149 | .then((texts: string[]) => ...); 150 | ``` 151 | 152 | ### Fixes 153 | 154 | - Support for Cypress 8.3.0 and above. There was a change in an internal API used for the 155 | `.attribute()` command. This internal API allows us to do some complex stuff with 156 | `{strict: true}`. The fix does not impact versions <= 8.2.0. See #60 for details. 157 | 158 | - `.attribute()` would not work properly in situations where it finds one attribute with a string 159 | length longer than the number of elements. For example: 160 | 161 | 162 | ```html 163 |
164 |
165 |
166 | ``` 167 | 168 | ```javascript 169 | cy.get('div').attribute('data-foo'); // Throws error because `hello`.length > $elements.length 170 | ``` 171 | 172 | This change also prompted some refactoring. 173 | 174 | - Updated docs based on changed made upstream in the Cypress docs. 175 | 176 | - Added config for Prettier/editorconfig and Eslint rules to match them. Reformatted a lot of files 177 | because of this. 178 | 179 | - Moved CI from Travis to Github. Now tests on multiple versions of NodeJS and multiple versions of 180 | Cypress. 181 | 182 | - Updated a lot of dependencies. It was over due. 183 | 184 | - Switched use of `path` to `path-browserify` to reduce config overhead for TypeScript users. 185 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # How to develop 2 | 3 | ## Node version 4 | 5 | Anything above Node 8 6 | 7 | ## Running tests 8 | 9 | There are many ways to run tests. Here is a list 10 | 11 | | Command | Headless | Starts server | Code under test | 12 | | ------------------------------ | ------------------ | ------------------ | ------------------- | 13 | | `npm test` | :heavy_check_mark: | :heavy_check_mark: | Source files | 14 | | `npm run test:source` | :heavy_check_mark: | :heavy_check_mark: | Source files | 15 | | `npm run test:bundle` | :heavy_check_mark: | :heavy_check_mark: | Distribution bundle | 16 | | `npm run run:cypress` | :heavy_check_mark: | :x: | Source files | 17 | | `npm run run:cypress:source` | :heavy_check_mark: | :x: | Source files | 18 | | `npm run run:cypress:bundle` | :heavy_check_mark: | :x: | Distribution bundle | 19 | | `npm start` | :x: | :heavy_check_mark: | Source files | 20 | | `npm run start:source` | :x: | :heavy_check_mark: | Source files | 21 | | `npm run start:bundle` | :x: | :heavy_check_mark: | Distribution bundle | 22 | | `npm run start:cypress` | :x: | :x: | Source files | 23 | | `npm run start:cypress:source` | :x: | :x: | Source files | 24 | | `npm run start:cypress:bundle` | :x: | :x: | Distribution bundle | 25 | 26 | For commands that don't start their own server you'll need to run `npm run start:server` in a second 27 | command line. 28 | 29 | ## Publishing 30 | 31 | `npm publish` will run tests that have to pass before allowing you to publish. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sander van Beek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cypress commands 2 | 3 | [![npm version](https://badge.fury.io/js/cypress-commands.svg)](https://badge.fury.io/js/cypress-commands) 4 | 5 | A collection of high-quality Cypress commands to complement and extend the defaults. 6 | 7 | This repository is not maintained by the Cypress developers. This means we can choose to ignore 8 | parts of their vision. 9 | 10 | Documentation is a cornerstone of Cypress, the commands in this repository will try to keep these 11 | documentation standards. 12 | 13 | ## Cypress version 14 | 15 | `cypress-commands` should work with the latest version of Cypress. If this is not the case, please 16 | open an issue. 17 | 18 | It's tested against multiple versions of Cypress. See the 19 | [CI definition](./.github/workflows/ci.yaml) for the most up-to-date list. 20 | 21 | ## Installation 22 | 23 | Install the module. 24 | 25 | ```shell 26 | npm install cypress-commands 27 | ``` 28 | 29 | Add the following line to `cypress/support/index.js`. 30 | 31 | ```javascript 32 | require('cypress-commands'); 33 | ``` 34 | 35 | ### Type definitions 36 | 37 | Import typescript definitions by adding them to your `tsconfig.json`. Add the cypress-commands types 38 | before the Cypress types so intellisense will prefer the cypress-commands versions of extended 39 | commands. 40 | 41 | ```json 42 | { 43 | "compilerOptions": { 44 | "types": ["cypress-commands", "cypress"] 45 | } 46 | } 47 | ``` 48 | 49 | #### Known issue: `cypress.config.ts` limitation 50 | 51 | Due to the way Cypress defines its types, it's currently not possible for plugin authors to extend 52 | the Cypress config types. 53 | 54 | Because of this limitation, it's not possible to set the `requestBaseUrl` option in 55 | `cypress.config.ts`. For the time being, you can work around this limitation by using 56 | `cypress.config.js` instead. 57 | 58 | See https://github.com/cypress-io/cypress/issues/22127 for more details. 59 | 60 | ## Extended commands 61 | 62 | These commands have been extended to be able to do more than originally intended. For these 63 | commands, all tests that exist in the Cypress repository are copied to make sure the default 64 | behaviour stays identical unless we want it changed. 65 | 66 | - [`.request()`](./docs/request.md) 67 | - [`.then()`](./docs/then.md) 68 | 69 | ## Added commands 70 | 71 | These commands do not exist in Cypress by default. 72 | 73 | - [`.attribute()`](./docs/attribute.md) 74 | - [`.text()`](./docs/text.md) 75 | - [`.to()`](./docs/to.md) 76 | 77 | ## Contributing 78 | 79 | Contributors are always welcome! I don't care if you are a beginner or an expert, all help is 80 | welcome. 81 | 82 | ## Running tests 83 | 84 | First clone the repository and install the dependencies. 85 | 86 | ### GUI mode 87 | 88 | ```shell 89 | npm start 90 | ``` 91 | 92 | ### CLI mode 93 | 94 | ```shell 95 | npm test 96 | ``` 97 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jQuery 3.2.1 Fixture 5 | 6 | 7 | 14 | 15 | 16 | foo 17 |
div
18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 |
0
26 | 27 | a 28 |
33 | div cont​aining    some
34 | complex whitespace 35 |
36 |
37 | Some text with inline 38 | formatting
applied to it.
39 |
40 |
41 |
42 | parent div top 43 |
44 | child div 45 |
46 | grandchild div 47 |
48 | great-grandchild div 49 |
50 | great-great-grandchild div 51 |
52 |
53 |
54 |
55 | parent div middle 56 |
57 | second-child div 58 |
second-grand-child div
59 |
60 | parent div bottom 61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | setInterval(() => { 2 | const count = $('#list li').length; 3 | if (count < 5) { 4 | $('#list').append(`
  • li ${count}
  • `); 5 | } 6 | 7 | const elems = [$('.counter'), $('input')]; 8 | 9 | elems.forEach((elem) => { 10 | const val = +elem.text(); 11 | if (val < 5) { 12 | elem.text(val + 1); 13 | elem.val(val + 1); 14 | elem.attr('data-attr', val + 1); 15 | } 16 | }); 17 | }, 100); 18 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | video: false, 5 | 6 | e2e: { 7 | baseUrl: 'http://localhost:1337/', 8 | requestBaseUrl: 'http://api.localhost:1337', 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /cypress/e2e/attribute.cy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | const COMMAND_TIMEOUT = 4000; 4 | 5 | describe('The added command `attribute`', function () { 6 | before(function () { 7 | cy.visit('/'); 8 | }); 9 | 10 | beforeEach(function () { 11 | Cypress.config('defaultCommandTimeout', COMMAND_TIMEOUT); 12 | }); 13 | 14 | it('considers empty attributes to be existing, but empty', function () { 15 | cy.get('.parent') 16 | .attribute('data-foo') 17 | .should('equal', '') 18 | .should('exist') 19 | .should('be.empty'); 20 | }); 21 | 22 | it('retries', function () { 23 | cy.get('form input').attribute('data-attr').should('equal', '5'); 24 | }); 25 | 26 | describe('handles implicit assertions correctly', function () { 27 | let __logs; 28 | let __lastLog; 29 | 30 | beforeEach(function () { 31 | Cypress.config('defaultCommandTimeout', 50); 32 | 33 | __logs = []; 34 | 35 | cy.on('log:added', (_, log) => { 36 | __lastLog = log; 37 | __logs.push(log); 38 | }); 39 | 40 | return null; 41 | }); 42 | 43 | describe('implicit assertion `to.exist`', function () { 44 | it('throws when the attribute does not exist', function (done) { 45 | cy.on('fail', (err) => { 46 | const lastLog = __lastLog; 47 | 48 | expect(__logs.length).to.eq(2); 49 | expect(lastLog.get('error')).to.eq(err); 50 | expect(err.message).to.include( 51 | "Expected element to have attribute 'id', but never found it." 52 | ); 53 | done(); 54 | }); 55 | 56 | cy.get('.whitespace').attribute('id'); 57 | }); 58 | 59 | it('does not throw when the attribute does exist', function () { 60 | cy.get('.whitespace').attribute('class'); 61 | }); 62 | }); 63 | 64 | describe('Overwriting implicit assertions', function () { 65 | it('can explicitly assert existence', function (done) { 66 | cy.on('fail', (err) => { 67 | expect(__logs.length).to.eq(3); 68 | expect(err.message).to.include( 69 | "Expected element to have attribute 'id', but never found it." 70 | ); 71 | done(); 72 | }); 73 | 74 | cy.get('.whitespace').attribute('id').should('exist'); 75 | }); 76 | 77 | it('overwrites implicit assertion when testing for non-existence', function (done) { 78 | cy.on('fail', (err) => { 79 | expect(__logs.length).to.eq(3); 80 | expect(err.message).to.include( 81 | "Expected element to not have attribute 'class', " + 82 | 'but it was continuously found.' 83 | ); 84 | done(); 85 | }); 86 | 87 | cy.get('.whitespace').attribute('class').should('not.exist'); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('The attribute of a single element', function () { 93 | it('yields a string', function () { 94 | cy.get('.whitespace').attribute('class').should('equal', 'whitespace'); 95 | }); 96 | 97 | it('yields the first when an attribute exists twice', function () { 98 | cy.get('.great-great-grandchild').attribute('data-foo').should('equal', 'bar'); 99 | }); 100 | }); 101 | 102 | describe('The attribute of a multiple elements', function () { 103 | it('yields an array of strings', function () { 104 | cy.get('.parent > div') 105 | .attribute('data-relation') 106 | .should('have.lengthOf', 2) 107 | .should('deep.equal', ['child', 'child']); 108 | }); 109 | }); 110 | 111 | describe('options', function () { 112 | let __logs; 113 | let __lastLog; 114 | 115 | beforeEach(function () { 116 | Cypress.config('defaultCommandTimeout', 50); 117 | 118 | __logs = []; 119 | 120 | cy.on('log:added', (_, log) => { 121 | __lastLog = log; 122 | __logs.push(log); 123 | }); 124 | 125 | return null; 126 | }); 127 | 128 | it('does not mind flipping the order of properties', function () { 129 | cy.get('.whitespace').attribute({}, 'class').should('equal', 'whitespace'); 130 | 131 | cy.get('.whitespace').attribute('class', {}).should('equal', 'whitespace'); 132 | }); 133 | 134 | it('does not log with `log: false`', function () { 135 | cy.get('.whitespace') 136 | .attribute('class', { log: false }) 137 | .then(() => { 138 | const lastLog = __lastLog; 139 | 140 | expect(__logs.length).to.equal(1); 141 | expect(lastLog.get().name).to.equal('get'); 142 | }) 143 | .should('equal', 'whitespace'); 144 | }); 145 | 146 | describe('strict', function () { 147 | it('throws when not all subjects have the attribute', function (done) { 148 | cy.on('fail', (err) => { 149 | expect(__logs.length).to.eq(2); 150 | expect(err.message).to.include( 151 | 'Expected all 4 elements to have attribute ' + 152 | "'data-relation', but never found it on 1 elements." 153 | ); 154 | done(); 155 | }); 156 | 157 | cy.get('.parent > div > div, .parent > div') 158 | // strict: true is default 159 | .attribute('data-relation', {}); 160 | }); 161 | 162 | it('throws when only 1 subject has the attribute', function (done) { 163 | cy.on('fail', (err) => { 164 | expect(__logs.length).to.eq(2); 165 | expect(err.message).to.include( 166 | 'Expected all 4 elements to have attribute ' + 167 | "'data-hello', but never found it on 3 elements." 168 | ); 169 | done(); 170 | }); 171 | 172 | cy.get('.parent > div > div, .parent > div') 173 | // strict: true is default 174 | .attribute('data-hello', {}); 175 | }); 176 | 177 | it( 178 | 'does not throw when not all subjects have the attribute ' + 'and `strict: false`', 179 | function () { 180 | cy.get('.parent div').attribute('data-relation', { strict: false }); 181 | } 182 | ); 183 | 184 | it( 185 | 'only yields the values of elements with the attribute when' + '`strict: false`', 186 | function () { 187 | cy.get('.parent div') 188 | .attribute('data-relation', { strict: false }) 189 | .should('be.lengthOf', 3) 190 | .and('deep.equal', ['child', 'grandchild', 'child']); 191 | } 192 | ); 193 | 194 | context('Upcoming assertions', function () { 195 | describe('should exist', function () { 196 | it('throws when not all subjects have the attribute', function (done) { 197 | cy.on('fail', (err) => { 198 | expect(__logs.length).to.eq(3); 199 | expect(err.message).to.include( 200 | 'Expected all 4 elements to have attribute ' + 201 | "'data-relation', but never found it on 1 elements." 202 | ); 203 | done(); 204 | }); 205 | 206 | cy.get('.parent > div > div, .parent > div') 207 | .attribute('data-relation', { strict: true }) 208 | .should('exist'); 209 | }); 210 | 211 | it('does not throw when all subjects have the attribute', function () { 212 | cy.get('.parent > div') 213 | .attribute('data-relation', { strict: true }) 214 | .should('exist'); 215 | }); 216 | }); 217 | 218 | describe('should not exist', function () { 219 | it('does not throw when none of the subjects have attribute', function () { 220 | cy.get('.parent > div > div, .parent > div') 221 | .attribute('data-nonExistent', { strict: true }) 222 | .should('not.exist'); 223 | }); 224 | 225 | it('throws when some of the subjects have attribute', function (done) { 226 | cy.on('fail', (err) => { 227 | expect(__logs.length).to.eq(3); 228 | expect(err.message).to.include( 229 | 'Expected all 4 elements to not have attribute ' + 230 | "'data-relation', but it was continuously found on 3 " + 231 | 'elements.' 232 | ); 233 | done(); 234 | }); 235 | 236 | cy.get('.parent > div > div, .parent > div') 237 | .attribute('data-relation', { strict: true }) 238 | .should('not.exist'); 239 | }); 240 | 241 | /** 242 | * Initial support for negating existence in strict mode depended on Cypress' 243 | * logging framework. This resulted in unexpected behaviour when `log: false`. 244 | */ 245 | it( 246 | 'throws when some of the subjects have attribute and ' + '`log: false`', 247 | function (done) { 248 | cy.on('fail', (err) => { 249 | expect(__logs.length).to.eq(2); 250 | expect(err.message).to.include( 251 | 'Expected all 4 elements to not have attribute ' + 252 | "'data-relation', but it was continuously found on 3 " + 253 | 'elements.' 254 | ); 255 | done(); 256 | }); 257 | 258 | cy.get('.parent > div > div, .parent > div') 259 | .attribute('data-relation', { strict: true, log: false }) 260 | .should('not.exist'); 261 | } 262 | ); 263 | 264 | /** 265 | * Bug with checking if the upcoming assertions negate existence 266 | */ 267 | it( 268 | 'throws when in the second call to attribute some of ' + 269 | 'the subjects have attribute', 270 | function (done) { 271 | cy.on('fail', (err) => { 272 | expect(__logs.length).to.eq(6); 273 | expect(err.message).to.include( 274 | 'Expected all 4 elements to have attribute ' + 275 | "'data-relation', but never found it on 1 elements" 276 | ); 277 | done(); 278 | }); 279 | 280 | cy.get('.parent > div > div, .parent > div') 281 | .attribute('data-rel', { strict: true }) 282 | .should('not.exist'); 283 | 284 | cy.get('.parent > div > div, .parent > div') 285 | .attribute('data-relation', { strict: true }) 286 | .should('exist'); 287 | } 288 | ); 289 | }); 290 | }); 291 | }); 292 | 293 | describe('whitespace', function () { 294 | it('`keep` is the default value', function () { 295 | cy.get('div.whitespace') 296 | .attribute('data-complex') 297 | .should('equal', ' some    \t very\n complex\twhitespace'); 298 | }); 299 | 300 | it('`simplify` simplifies all whitespace', function () { 301 | cy.get('div.whitespace') 302 | .attribute('data-complex', { whitespace: 'simplify' }) 303 | .should('equal', 'some very complex whitespace'); 304 | }); 305 | 306 | it('`keep-newline` simplifies all whitespace except newlines', function () { 307 | cy.get('div.whitespace') 308 | .attribute('data-complex', { whitespace: 'keep-newline' }) 309 | .should('equal', 'some very\ncomplex whitespace'); 310 | }); 311 | 312 | it('`keep` does not change whitespace at all', function () { 313 | cy.get('div.whitespace') 314 | .attribute('data-complex', { whitespace: 'keep' }) 315 | .should('equal', ' some    \t very\n complex\twhitespace'); 316 | }); 317 | }); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /cypress/e2e/request.cy.js: -------------------------------------------------------------------------------- 1 | const _ = Cypress._; 2 | const RESPONSE_TIMEOUT = 22222; 3 | const initialBaseUrl = Cypress.config().baseUrl; 4 | const initialRequestBaseUrl = Cypress.config().requestBaseUrl; 5 | 6 | describe('Overwritten command request', function () { 7 | afterEach(function () { 8 | Cypress.config('baseUrl', initialBaseUrl); 9 | Cypress.config('requestBaseUrl', initialRequestBaseUrl); 10 | }); 11 | 12 | beforeEach(function () { 13 | cy.stub(Cypress, 'backend').callThrough(); 14 | Cypress.config('responseTimeout', RESPONSE_TIMEOUT); 15 | }); 16 | 17 | describe('argument signature', function () { 18 | beforeEach(function () { 19 | const backend = Cypress.backend.withArgs('http:request').resolves({ 20 | isOkStatusCode: true, 21 | status: 200, 22 | }); 23 | 24 | this.expectOptionsToBe = function (opts) { 25 | _.defaults(opts, { 26 | failOnStatusCode: true, 27 | retryOnNetworkFailure: true, 28 | retryOnStatusCodeFailure: false, 29 | gzip: true, 30 | followRedirect: true, 31 | timeout: RESPONSE_TIMEOUT, 32 | method: 'GET', 33 | encoding: 'utf8', 34 | retryIntervals: [0, 100, 200, 200], 35 | }); 36 | 37 | const options = backend.firstCall.args[1]; 38 | 39 | _.each(options, function (value, key) { 40 | expect(options[key]).to.deep.eq(opts[key], `failed on property: (${key})`); 41 | }); 42 | 43 | _.each(opts, function (value, key) { 44 | expect(opts[key]).to.deep.eq(options[key], `failed on property: (${key})`); 45 | }); 46 | }; 47 | }); 48 | 49 | it('prefixes with requestBaseUrl set in cypress config, origin url is empty', function () { 50 | cy.stub(cy, 'getRemoteLocation').withArgs('origin').returns(''); 51 | 52 | // Use requestBaseUrl from Cypress config 53 | Cypress.config('baseUrl', 'http://localhost:8080/app'); 54 | 55 | cy.request('/foo/bar?cat=1').then(() => { 56 | this.expectOptionsToBe({ 57 | url: 'http://api.localhost:1337/foo/bar?cat=1', 58 | method: 'GET', 59 | gzip: true, 60 | followRedirect: true, 61 | timeout: RESPONSE_TIMEOUT, 62 | }); 63 | }); 64 | }); 65 | 66 | it('prefixes with requestBaseUrl when origin url is empty', function () { 67 | cy.stub(cy, 'getRemoteLocation').withArgs('origin').returns(''); 68 | 69 | Cypress.config('requestBaseUrl', 'http://api.localhost:8080/app'); 70 | Cypress.config('baseUrl', 'http://localhost:8080/app'); 71 | 72 | cy.request('/foo/bar?cat=1').then(() => { 73 | this.expectOptionsToBe({ 74 | url: 'http://api.localhost:8080/app/foo/bar?cat=1', 75 | method: 'GET', 76 | gzip: true, 77 | followRedirect: true, 78 | timeout: RESPONSE_TIMEOUT, 79 | }); 80 | }); 81 | }); 82 | 83 | it('prefixes baseUrl when originUrl is empty and the requestBaseUrl is empty', function () { 84 | cy.stub(cy, 'getRemoteLocation').withArgs('origin').returns(''); 85 | 86 | Cypress.config('requestBaseUrl', ''); 87 | Cypress.config('baseUrl', 'http://localhost:8080/app'); 88 | 89 | cy.request('/foo/bar?cat=1').then(() => { 90 | this.expectOptionsToBe({ 91 | url: 'http://localhost:8080/app/foo/bar?cat=1', 92 | method: 'GET', 93 | gzip: true, 94 | followRedirect: true, 95 | timeout: RESPONSE_TIMEOUT, 96 | }); 97 | }); 98 | }); 99 | 100 | it('prefixes baseUrl when originUrl is empty and the requestBaseUrl is null', function () { 101 | cy.stub(cy, 'getRemoteLocation').withArgs('origin').returns(''); 102 | 103 | Cypress.config('requestBaseUrl', null); 104 | Cypress.config('baseUrl', 'http://localhost:8080/app'); 105 | 106 | cy.request('/foo/bar?cat=1').then(() => { 107 | this.expectOptionsToBe({ 108 | url: 'http://localhost:8080/app/foo/bar?cat=1', 109 | method: 'GET', 110 | gzip: true, 111 | followRedirect: true, 112 | timeout: RESPONSE_TIMEOUT, 113 | }); 114 | }); 115 | }); 116 | 117 | it('prefixes baseUrl when originUrl is empty and requestBaseUrl is undefined', function () { 118 | cy.stub(cy, 'getRemoteLocation').withArgs('origin').returns(''); 119 | 120 | Cypress.config('requestBaseUrl', undefined); 121 | Cypress.config('baseUrl', 'http://localhost:8080/app'); 122 | 123 | cy.request('/foo/bar?cat=1').then(() => { 124 | this.expectOptionsToBe({ 125 | url: 'http://localhost:8080/app/foo/bar?cat=1', 126 | method: 'GET', 127 | gzip: true, 128 | followRedirect: true, 129 | timeout: RESPONSE_TIMEOUT, 130 | }); 131 | }); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /cypress/e2e/text.cy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | const $ = Cypress.$; 4 | const COMMAND_TIMEOUT = 4000; 5 | 6 | describe('The added command `text`', function () { 7 | let body; 8 | 9 | before(function () { 10 | cy.visit('/').then((win) => { 11 | body = win.document.body.outerHTML; 12 | }); 13 | }); 14 | 15 | beforeEach(function () { 16 | Cypress.config('defaultCommandTimeout', COMMAND_TIMEOUT); 17 | 18 | const doc = cy.state('document'); 19 | $(doc.body).empty().html(body); 20 | }); 21 | 22 | it('yields text from a generic DOM element', function () { 23 | cy.get('div').first().text().should('equal', 'div'); 24 | }); 25 | 26 | it('yields the value of a button element', function () { 27 | cy.get('button').text().should('equal', 'Some button'); 28 | }); 29 | 30 | it('yields the value of a textarea element', function () { 31 | cy.get('textarea').text().should('equal', 'A filled textarea'); 32 | }); 33 | 34 | it('retries the value of a generic DOM element', function () { 35 | cy.get('.counter').text().should('equal', '5'); 36 | }); 37 | 38 | it('retries the value of an input element', function () { 39 | cy.get('input').text().should('equal', '5'); 40 | }); 41 | 42 | describe('returns', function () { 43 | it('returns a string if the input is a single element', function () { 44 | cy.get('#foo').text().should('equal', 'foo'); 45 | }); 46 | 47 | it('returns an array if the input is multiple elements', function () { 48 | cy.get('.parent') 49 | .children() 50 | .text() 51 | .should((texts) => { 52 | expect(texts).to.be.lengthOf(2); 53 | expect(texts[0]).to.equal('child div'); 54 | expect(texts[1]).to.equal('second-child div'); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('The option `depth`', function () { 60 | it('`0` is the default value', function () { 61 | cy.get('div.parent') 62 | .text() 63 | .should( 64 | 'equal', 65 | ['parent div top', 'parent div middle', 'parent div bottom'].join(' ') 66 | ); 67 | }); 68 | 69 | it('`0` results in only the contents of the element itself', function () { 70 | cy.get('div.parent') 71 | .text({ depth: 0 }) 72 | .should( 73 | 'equal', 74 | ['parent div top', 'parent div middle', 'parent div bottom'].join(' ') 75 | ); 76 | }); 77 | 78 | it('`1` results in the contents of the element and its direct children', function () { 79 | cy.get('div.parent') 80 | .text({ depth: 1 }) 81 | .should( 82 | 'equal', 83 | [ 84 | 'parent div top', 85 | 'child div', 86 | 'parent div middle', 87 | 'second-child div', 88 | 'parent div bottom', 89 | ].join(' ') 90 | ); 91 | }); 92 | 93 | it('`2` results in the contents of the element and its direct children', function () { 94 | cy.get('div.parent') 95 | .text({ depth: 2 }) 96 | .should( 97 | 'equal', 98 | [ 99 | 'parent div top', 100 | 'child div', 101 | 'grandchild div', 102 | 'parent div middle', 103 | 'second-child div', 104 | 'second-grand-child div', 105 | 'parent div bottom', 106 | ].join(' ') 107 | ); 108 | }); 109 | 110 | it('`Infinity` results in the contents of the element and all its children', function () { 111 | cy.get('div.parent') 112 | .text({ depth: Infinity }) 113 | .should( 114 | 'equal', 115 | [ 116 | 'parent div top', 117 | 'child div', 118 | 'grandchild div', 119 | 'great-grandchild div', 120 | 'great-great-grandchild div', 121 | 'parent div middle', 122 | 'second-child div', 123 | 'second-grand-child div', 124 | 'parent div bottom', 125 | ].join(' ') 126 | ); 127 | }); 128 | 129 | it('gets all values of form elements', function () { 130 | cy.get('form').text({ depth: 1 }).should('equal', '5 A filled textarea Some button'); 131 | }); 132 | }); 133 | 134 | describe('The option `whitespace`', function () { 135 | it('`simplify` is the default value', function () { 136 | cy.get('div.whitespace') 137 | .text() 138 | .should('equal', 'div containing some complex whitespace'); 139 | 140 | cy.get('div.formatted') 141 | .text({ depth: 9 }) 142 | .should('equal', 'Some text with inline formatting applied to it.'); 143 | }); 144 | 145 | it('`simplify` simplifies all whitespace', function () { 146 | cy.get('div.whitespace') 147 | .text({ whitespace: 'simplify' }) 148 | .should('equal', 'div containing some complex whitespace'); 149 | 150 | cy.get('div.formatted') 151 | .text({ depth: 9, whitespace: 'simplify' }) 152 | .should('equal', 'Some text with inline formatting applied to it.'); 153 | }); 154 | 155 | it('`keep-newline` simplifies all whitespace except newlines', function () { 156 | cy.get('div.whitespace') 157 | .text({ whitespace: 'keep-newline' }) 158 | .should('equal', 'div containing some\ncomplex whitespace'); 159 | 160 | cy.get('div.formatted') 161 | .text({ depth: 9, whitespace: 'keep-newline' }) 162 | .should('equal', 'Some text with inline\nformatting applied to it.'); 163 | }); 164 | 165 | it('`keep` does not change whitespace at all', function () { 166 | cy.get('div.whitespace') 167 | .text({ whitespace: 'keep' }) 168 | .should((text) => { 169 | const lines = text.split('\n'); 170 | console.log(lines); 171 | 172 | expect(lines[0]).to.equal('div cont\u200Baining\xa0 \xa0 \t some '); 173 | expect(lines[1]).to.equal(' complex\twhite\u200Bspace'); 174 | }); 175 | 176 | cy.get('div.formatted') 177 | .text({ depth: 9, whitespace: 'keep' }) 178 | .should((text) => { 179 | expect(text.trim()).to.equal( 180 | 'Some text with inline\n formatting applied to it.' 181 | ); 182 | }); 183 | }); 184 | }); 185 | 186 | describe('errors', function () { 187 | let __logs; 188 | let __lastLog; 189 | 190 | beforeEach(function () { 191 | Cypress.config('defaultCommandTimeout', 50); 192 | 193 | __logs = []; 194 | 195 | cy.on('log:added', (_, log) => { 196 | __lastLog = log; 197 | __logs.push(log); 198 | }); 199 | 200 | return null; 201 | }); 202 | 203 | it('is called as a parent command', function (done) { 204 | cy.on('fail', (err) => { 205 | const lastLog = __lastLog; 206 | 207 | expect(__logs.length).to.eq(1); 208 | expect(lastLog.get('error')).to.eq(err); 209 | expect(err.message).to.include( 210 | 'Oops, it looks like you are trying to call a child ' + 211 | 'command before running a parent command.' 212 | ); 213 | done(); 214 | }); 215 | 216 | cy.text(); 217 | }); 218 | 219 | it('not preceded with an element', function (done) { 220 | cy.on('fail', (err) => { 221 | const lastLog = __lastLog; 222 | 223 | expect(__logs.length).to.eq(2); 224 | expect(lastLog.get('error')).to.eq(err); 225 | expect(err.message).to.include( 226 | '`cy.text()` failed because it requires a DOM element.' 227 | ); 228 | done(); 229 | }); 230 | 231 | cy.wrap('foo').text(); 232 | }); 233 | 234 | it('wrong value for the option `depth`', function (done) { 235 | cy.on('fail', (err) => { 236 | const lastLog = __lastLog; 237 | 238 | expect(__logs.length).to.eq(2); 239 | expect(lastLog.get('error')).to.eq(err); 240 | expect(err.message).to.include( 241 | 'Bad value for the option "depth" of the command "text"' 242 | ); 243 | done(); 244 | }); 245 | 246 | cy.get('#foo').text({ depth: -1 }); 247 | }); 248 | 249 | it('wrong value for the option `whitespace`', function (done) { 250 | cy.on('fail', (err) => { 251 | const lastLog = __lastLog; 252 | 253 | expect(__logs.length).to.eq(2); 254 | expect(lastLog.get('error')).to.eq(err); 255 | expect(err.message) 256 | .to.include('Bad value for the option "whitespace" of the command "text"') 257 | .and.include('["simplify","keep","keep-newline"]'); 258 | done(); 259 | }); 260 | 261 | cy.get('#foo').text({ whitespace: 1 }); 262 | }); 263 | 264 | it('wrong value for the option `log`', function (done) { 265 | cy.on('fail', (err) => { 266 | const lastLog = __lastLog; 267 | 268 | expect(__logs.length).to.eq(2); 269 | expect(lastLog.get('error')).to.eq(err); 270 | expect(err.message) 271 | .to.include('Bad value for the option "log" of the command "text"') 272 | .and.include('[true,false]'); 273 | done(); 274 | }); 275 | 276 | cy.get('#foo').text({ log: 1 }); 277 | }); 278 | }); 279 | }); 280 | -------------------------------------------------------------------------------- /cypress/e2e/then.cy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable cypress/no-assigning-return-values */ 2 | /* eslint-disable cypress/no-unnecessary-waiting */ 3 | 4 | const $ = Cypress.$; 5 | const Promise = Cypress.Promise; 6 | 7 | describe('The overwritten command `then`', function () { 8 | let body; 9 | 10 | before(function () { 11 | cy.visit('/').then((win) => { 12 | body = win.document.body.outerHTML; 13 | }); 14 | }); 15 | 16 | describe('Tests copied from Cypress repo', function () { 17 | beforeEach(function () { 18 | const doc = cy.state('document'); 19 | $(doc.body).empty().html(body); 20 | }); 21 | 22 | it('converts raw DOM elements', function () { 23 | const div = cy.$$('div:first').get(0); 24 | 25 | cy.wrap(div).then(($div) => { 26 | expect($div.get(0)).to.eq(div); 27 | }); 28 | }); 29 | 30 | it('does not insert a mocha callback', function () { 31 | cy.noop().then(() => { 32 | expect(cy.queue.length).to.eq(2); 33 | }); 34 | }); 35 | 36 | it('passes timeout option to then', function () { 37 | cy.timeout(50); 38 | 39 | cy.then({ timeout: 150 }, function () { 40 | return Promise.delay(100); 41 | }); 42 | }); 43 | 44 | it('can resolve nested thens', function () { 45 | cy.get('div:first').then(() => { 46 | cy.get('div:first').then(() => { 47 | cy.get('div:first'); 48 | }); 49 | }); 50 | }); 51 | 52 | it('can resolve cypress commands inside of a promise', function () { 53 | let _then = false; 54 | 55 | cy.wrap(null) 56 | .then(() => { 57 | return Promise.delay(10).then(() => { 58 | cy.then(() => { 59 | _then = true; 60 | }); 61 | }); 62 | }) 63 | .then(() => { 64 | expect(_then).to.be.true; 65 | }); 66 | }); 67 | 68 | it('can resolve chained cypress commands inside of a promise', function () { 69 | let _then = false; 70 | 71 | cy.wrap(null) 72 | .then(() => { 73 | return Promise.delay(10).then(() => { 74 | cy.get('div:first').then(() => { 75 | _then = true; 76 | }); 77 | }); 78 | }) 79 | .then(() => { 80 | expect(_then).to.be.true; 81 | }); 82 | }); 83 | 84 | it('can resolve cypress instance inside of a promise', function () { 85 | cy.then(() => { 86 | Promise.delay(10).then(() => cy); 87 | }); 88 | }); 89 | 90 | it('passes values to the next command', function () { 91 | cy.wrap({ foo: 'bar' }) 92 | .then((obj) => obj.foo) 93 | .then((val) => { 94 | expect(val).to.eq('bar'); 95 | }); 96 | }); 97 | 98 | it('does not throw when returning thenables with cy command', function () { 99 | cy.wrap({ foo: 'bar' }).then((obj) => { 100 | return new Promise((resolve) => { 101 | cy.wait(10); 102 | 103 | resolve(obj.foo); 104 | }); 105 | }); 106 | }); 107 | 108 | it('should pass the eventual resolved thenable value downstream', function () { 109 | cy.wrap({ foo: 'bar' }) 110 | .then((obj) => { 111 | cy.wait(10) 112 | .then(() => obj.foo) 113 | .then((value) => { 114 | expect(value).to.eq('bar'); 115 | 116 | return value; 117 | }); 118 | }) 119 | .then((val) => { 120 | expect(val).to.eq('bar'); 121 | }); 122 | }); 123 | 124 | it( 125 | 'should not pass the eventual resolve thenable value downstream because ' + 126 | 'thens are not connected', 127 | function () { 128 | cy.wrap({ foo: 'bar' }).then((obj) => { 129 | cy.wait(10) 130 | .then(() => obj.foo) 131 | .then((value) => { 132 | expect(value).to.eq('bar'); 133 | 134 | return value; 135 | }); 136 | }); 137 | cy.then((val) => { 138 | expect(val).to.be.undefined; 139 | }); 140 | } 141 | ); 142 | 143 | it('passes the existing subject if ret is undefined', function () { 144 | cy.wrap({ foo: 'bar' }) 145 | .then(() => undefined) 146 | .then((obj) => { 147 | expect(obj).to.deep.eq({ foo: 'bar' }); 148 | }); 149 | }); 150 | 151 | it('sets the subject to null when given null', function () { 152 | cy.wrap({ foo: 'bar' }) 153 | .then(() => null) 154 | .then((obj) => { 155 | expect(obj).to.be.null; 156 | }); 157 | }); 158 | 159 | describe('errors', function () { 160 | let __logs; 161 | let __lastLog; 162 | 163 | beforeEach(function () { 164 | Cypress.config('defaultCommandTimeout', 50); 165 | 166 | __logs = []; 167 | 168 | cy.on('log:added', (_, log) => { 169 | __lastLog = log; 170 | __logs.push(log); 171 | }); 172 | 173 | return null; 174 | }); 175 | 176 | it('throws when promise timeout', function (done) { 177 | cy.on('fail', (err) => { 178 | const lastLog = __lastLog; 179 | 180 | expect(__logs.length).to.eq(1); 181 | expect(lastLog.get('error')).to.eq(err); 182 | expect(err.message).to.include('`cy.then()` timed out after waiting `150ms`.'); 183 | done(); 184 | }); 185 | 186 | cy.then({ timeout: 150 }, () => { 187 | return new Promise(() => {}); 188 | }); 189 | }); 190 | 191 | it('throws when mixing up async + sync return values', function (done) { 192 | cy.on('fail', (err) => { 193 | const lastLog = __lastLog; 194 | 195 | expect(__logs.length).to.eq(1); 196 | expect(lastLog.get('error')).to.eq(err); 197 | expect(err.message).to.include( 198 | '`cy.then()` failed because you are mixing up async and sync code.' 199 | ); 200 | done(); 201 | }); 202 | 203 | cy.then(() => { 204 | cy.wait(5000); 205 | return 'foo'; 206 | }); 207 | }); 208 | 209 | it('unbinds command:enqueued in the case of an error thrown', function (done) { 210 | const listeners = []; 211 | 212 | cy.on('fail', () => { 213 | listeners.push(cy.listeners('command:enqueued').length); 214 | 215 | expect(__logs.length).to.eq(1); 216 | expect(listeners).to.deep.eq([1, 0]); 217 | done(); 218 | }); 219 | 220 | cy.then(() => { 221 | listeners.push(cy.listeners('command:enqueued').length); 222 | 223 | throw new Error('foo'); 224 | }); 225 | }); 226 | }); 227 | 228 | describe('yields to remote jQuery subject', function () { 229 | let __remoteWindow; 230 | beforeEach(function () { 231 | __remoteWindow = cy.state('window'); 232 | }); 233 | 234 | it('calls the callback function with the remote jQuery subject', function () { 235 | __remoteWindow.$.fn.foo = () => {}; 236 | 237 | cy.get('div:first') 238 | .then(($div) => { 239 | expect($div).to.be.instanceof(__remoteWindow.$); 240 | return $div; 241 | }) 242 | .then(($div) => { 243 | expect($div).to.be.instanceof(__remoteWindow.$); 244 | }); 245 | }); 246 | 247 | it('does not store the remote jQuery object as the subject', function () { 248 | cy.get('div:first') 249 | .then(($div) => { 250 | expect($div).to.be.instanceof(__remoteWindow.$); 251 | return $div; 252 | }) 253 | .then(() => { 254 | expect(cy.state('subject')).to.not.be.instanceof(__remoteWindow.$); 255 | }); 256 | }); 257 | }); 258 | }); 259 | 260 | describe('Retryability', function () { 261 | before(function () { 262 | Cypress.config('defaultCommandTimeout', 1000); 263 | }); 264 | 265 | beforeEach(function () { 266 | const doc = cy.state('document'); 267 | $(doc.body).empty().html(body); 268 | }); 269 | 270 | it('retries until the upcoming assertion passes', function () { 271 | let c = 0; 272 | 273 | cy.then(() => ++c, { retry: true }).should('equal', 5); 274 | }); 275 | 276 | it('retries correctly when handling dom elements', function () { 277 | let initalResult = null; 278 | 279 | cy.get('ul#list') 280 | .then( 281 | (list) => { 282 | const result = list.children().length; 283 | if (initalResult === null) { 284 | initalResult = result; 285 | } 286 | return result; 287 | }, 288 | { retry: true } 289 | ) 290 | .should('equal', 3) 291 | .then((result) => { 292 | expect(initalResult).to.below(result); 293 | }); 294 | }); 295 | 296 | describe('errors', function () { 297 | let __logs; 298 | let __lastLog; 299 | 300 | beforeEach(function () { 301 | Cypress.config('defaultCommandTimeout', 50); 302 | __logs = []; 303 | 304 | cy.on('log:added', (_, log) => { 305 | __lastLog = log; 306 | __logs.push(log); 307 | }); 308 | }); 309 | 310 | it('throws on timeout', function (done) { 311 | cy.on('fail', (err) => { 312 | const lastLog = __lastLog; 313 | 314 | expect(__logs.length).to.eq(2); 315 | expect(err.message).to.contain(lastLog.get('error').message); 316 | expect(err.message).to.equal( 317 | 'Timed out retrying after 50ms: expected 5 to equal 4' 318 | ); 319 | done(); 320 | }); 321 | 322 | cy.then({ retry: true }, () => 5).should('equal', 4); 323 | }); 324 | 325 | it('logs and fails on a thrown error', function (done) { 326 | cy.on('fail', (err) => { 327 | const lastLog = __lastLog; 328 | 329 | expect(__logs.length).to.eq(2); 330 | expect(err.message).to.contain(lastLog.get('error').message); 331 | expect(err.message).to.equal('foo'); 332 | done(); 333 | }); 334 | 335 | cy.then({ retry: true }, () => { 336 | throw new Error('foo'); 337 | }).should('equal', 4); 338 | }); 339 | }); 340 | }); 341 | 342 | describe('Callback context', function () { 343 | it('passes aliasses to the callback function (fat-arrow notation)', function () { 344 | const outerThis = this; 345 | 346 | cy.wrap('fooBar').as('someAlias'); 347 | 348 | cy.then(() => { 349 | expect(this.someAlias).to.equal('fooBar'); 350 | expect(this).to.equal(outerThis); 351 | }); 352 | }); 353 | 354 | it('passes aliasses to the callback function (function notation)', function () { 355 | const outerThis = this; 356 | 357 | cy.wrap('fooBar').as('someAlias'); 358 | 359 | cy.then(function () { 360 | expect(this.someAlias).to.equal('fooBar'); 361 | expect(this).to.equal(outerThis); 362 | }); 363 | }); 364 | }); 365 | }); 366 | -------------------------------------------------------------------------------- /cypress/e2e/to.cy.js: -------------------------------------------------------------------------------- 1 | const COMMAND_TIMEOUT = 4000; 2 | const COMMAND_TIMEOUT_FAST = 100; 3 | 4 | describe('The added command `to`', function () { 5 | before(function () { 6 | cy.visit('/'); 7 | }); 8 | 9 | beforeEach(function () { 10 | Cypress.config('defaultCommandTimeout', COMMAND_TIMEOUT); 11 | }); 12 | 13 | context('Input validation', function () { 14 | let __logs; 15 | 16 | beforeEach(function () { 17 | __logs = []; 18 | 19 | cy.on('log:added', (_, log) => { 20 | __logs.push(log); 21 | }); 22 | }); 23 | 24 | it('throws when the subject is undefined', function (done) { 25 | cy.on('fail', (err) => { 26 | expect(__logs.length).to.eq(2); 27 | expect(err.message).to.include("Can't cast subject of type undefined"); 28 | done(); 29 | }); 30 | 31 | cy.wrap(undefined).to('string'); 32 | }); 33 | 34 | it('throws when the subject is null', function (done) { 35 | cy.on('fail', (err) => { 36 | expect(__logs.length).to.eq(2); 37 | expect(err.message).to.include("Can't cast subject of type null"); 38 | done(); 39 | }); 40 | 41 | cy.wrap(null).to('string'); 42 | }); 43 | 44 | it('throws when the subject is NaN', function (done) { 45 | cy.on('fail', (err) => { 46 | expect(__logs.length).to.eq(2); 47 | expect(err.message).to.include("Can't cast subject of type NaN"); 48 | done(); 49 | }); 50 | 51 | cy.wrap(NaN).to('string'); 52 | }); 53 | 54 | it('throws when a faulty value is provided for the option `log`', function (done) { 55 | cy.on('fail', (err) => { 56 | expect(__logs.length).to.eq(2); 57 | expect(err.message).to.include( 58 | 'Bad value for the option "log" of the command "to".' 59 | ); 60 | done(); 61 | }); 62 | 63 | cy.wrap(123).to('string', { log: 'foo' }); 64 | }); 65 | 66 | it('throws when a faulty type is provided', function (done) { 67 | cy.on('fail', (err) => { 68 | expect(__logs.length).to.eq(2); 69 | expect(err.message).to.include("Can't cast subject to type badType."); 70 | done(); 71 | }); 72 | 73 | cy.wrap(123).to('badType'); 74 | }); 75 | 76 | it('requires types to be case sensitive', function (done) { 77 | cy.on('fail', (err) => { 78 | expect(__logs.length).to.eq(2); 79 | expect(err.message).to.include("Can't cast subject to type STRing."); 80 | done(); 81 | }); 82 | 83 | cy.wrap(123).to('STRing'); 84 | }); 85 | }); 86 | 87 | context('String', function () { 88 | describe('In isolation', function () { 89 | /* eslint-disable-next-line sonarjs/no-unused-collection */ 90 | let __logs; 91 | 92 | beforeEach(function () { 93 | Cypress.config('defaultCommandTimeout', COMMAND_TIMEOUT_FAST); 94 | __logs = []; 95 | 96 | cy.on('log:added', (_, log) => { 97 | __logs.push(log); 98 | }); 99 | }); 100 | 101 | it('casts a number', function () { 102 | cy.wrap(123456).to('string').should('equal', '123456'); 103 | }); 104 | 105 | it('passes a string unmodified', function () { 106 | cy.wrap('foo bar baz').to('string').should('equal', 'foo bar baz'); 107 | }); 108 | 109 | it('casts all items in an array', function () { 110 | cy.wrap([132, 7, { foo: 'bar' }]) 111 | .to('string') 112 | .should('deep.equal', ['132', '7', '{"foo":"bar"}']); 113 | }); 114 | 115 | it('casts an object to JSON', function () { 116 | cy.wrap({ foo: 'bar', baz: true }) 117 | .to('string') 118 | .should('equal', '{"foo":"bar","baz":true}'); 119 | }); 120 | }); 121 | }); 122 | 123 | context('Number', function () { 124 | describe('In isolation', function () { 125 | let __logs; 126 | 127 | beforeEach(function () { 128 | Cypress.config('defaultCommandTimeout', COMMAND_TIMEOUT_FAST); 129 | __logs = []; 130 | 131 | cy.on('log:added', (_, log) => { 132 | __logs.push(log); 133 | }); 134 | }); 135 | 136 | it('casts a numberlike string', function () { 137 | cy.wrap('007').to('number').should('equal', 7); 138 | }); 139 | 140 | it('throws on a non-numberlike string', function (done) { 141 | cy.on('fail', (err) => { 142 | expect(__logs.length).to.eq(2); 143 | expect(err.message).to.include("Can't cast 'Five' to type number"); 144 | done(); 145 | }); 146 | 147 | cy.wrap('Five').to('number'); 148 | }); 149 | 150 | it('throws on a non-array object', function (done) { 151 | cy.on('fail', (err) => { 152 | expect(__logs.length).to.eq(2); 153 | expect(err.message).to.include( 154 | "Can't cast subject of type object to type number" 155 | ); 156 | done(); 157 | }); 158 | 159 | cy.wrap({ number: '123' }).to('number'); 160 | }); 161 | 162 | it('passes a subject of type number without modification', function () { 163 | cy.wrap(7).to('number').should('equal', 7); 164 | }); 165 | 166 | it('casts all items in an array of numberlike strings', function () { 167 | cy.wrap(['1234', '009', '564864']) 168 | .to('number') 169 | .should('deep.equal', [1234, 9, 564864]); 170 | }); 171 | 172 | it('throws on an array containing a non-numberlike string', function (done) { 173 | cy.on('fail', (err) => { 174 | expect(__logs.length).to.eq(2); 175 | expect(err.message).to.include("Can't cast 'foo' to type number"); 176 | done(); 177 | }); 178 | 179 | cy.wrap(['foo', '1234', '009', 'a125']).to('number'); 180 | }); 181 | }); 182 | }); 183 | 184 | context('Array', function () { 185 | describe('In isolation', function () { 186 | it('does not cast an Array', function () { 187 | cy.wrap(['lorum', 'ipsum']).to('array').should('deep.equal', ['lorum', 'ipsum']); 188 | }); 189 | 190 | it('casts a string', function () { 191 | cy.wrap('foo').to('array').should('deep.equal', ['foo']); 192 | }); 193 | 194 | it('casts a number', function () { 195 | cy.wrap(123).to('array').should('deep.equal', [123]); 196 | }); 197 | 198 | it('casts an object', function () { 199 | cy.wrap({ foo: 123 }) 200 | .to('array') 201 | .should('deep.equal', [{ foo: 123 }]); 202 | }); 203 | }); 204 | 205 | describe('When interacting with page', function () { 206 | beforeEach(function () { 207 | cy.get('#list > *').as('list').should('have.length', 5); 208 | }); 209 | 210 | it('does not cast a single element', function () { 211 | // Elements are jQuery objects, which are iterable. 212 | cy.get('@list') 213 | .first() 214 | .to('array') 215 | .should((result) => { 216 | expect(Array.isArray(result)).to.be.false; 217 | expect(result.length).to.equal(1); 218 | }) 219 | .each((val) => { 220 | expect(val.jquery).to.not.be.undefined; 221 | }); 222 | }); 223 | 224 | it('does not cast multiple elements', function () { 225 | // Elements are jQuery objects, which are iterable. 226 | cy.get('@list') 227 | .to('array') 228 | .should((result) => { 229 | expect(Array.isArray(result)).to.be.false; 230 | expect(result.length).to.equal(5); 231 | }) 232 | .each((val) => { 233 | expect(val.jquery).to.not.be.undefined; 234 | }); 235 | }); 236 | 237 | it('does cast the text result of a single element', function () { 238 | cy.get('@list') 239 | .first() 240 | .text() 241 | .to('array') 242 | .should((result) => { 243 | expect(Array.isArray(result)).to.be.true; 244 | expect(result.length).to.equal(1); 245 | }) 246 | .each((val) => { 247 | expect(typeof val).to.equal('string'); 248 | }); 249 | }); 250 | 251 | it('does not cast the text result of multiple elements', function () { 252 | cy.get('@list') 253 | .text() 254 | .to('array') 255 | .should((result) => { 256 | expect(Array.isArray(result)).to.be.true; 257 | expect(result.length).to.equal(5); 258 | }) 259 | .each((val) => { 260 | expect(typeof val).to.equal('string'); 261 | }); 262 | }); 263 | }); 264 | }); 265 | }); 266 | -------------------------------------------------------------------------------- /cypress/e2e/utils/whitespace.cy.js: -------------------------------------------------------------------------------- 1 | import whitespace from '../../../src/utils/whitespace'; 2 | const _ = Cypress._; 3 | 4 | describe('Whitespace options for commands yielding strings', function () { 5 | it('returns a function', function () { 6 | expect(_.isFunction(whitespace('mode'))).to.be.true; 7 | }); 8 | 9 | context('mode = `simplify`', function () { 10 | beforeEach(function () { 11 | this.ws = whitespace('simplify'); 12 | }); 13 | 14 | it('simplifies whitespace in the middle of the string', function () { 15 | expect(this.ws('Lorum ipsum\n\xa0dolor\tsit \r \n\tamet')).to.equal( 16 | 'Lorum ipsum dolor sit amet' 17 | ); 18 | }); 19 | 20 | it('removes whitespace at the ends of the string', function () { 21 | expect(this.ws(' Lorum ipsum dolor sit amet\n')).to.equal('Lorum ipsum dolor sit amet'); 22 | 23 | expect(this.ws('\tLorum ipsum dolor sit amet\r')).to.equal( 24 | 'Lorum ipsum dolor sit amet' 25 | ); 26 | 27 | expect(this.ws('\t \r \n\xa0 Lorum ipsum dolor sit amet')).to.equal( 28 | 'Lorum ipsum dolor sit amet' 29 | ); 30 | }); 31 | 32 | it('removes zero-width whitespace', function () { 33 | expect(this.ws('Lorum\u200Bipsum dol\uFEFFor sit amet')).to.equal( 34 | 'Lorumipsum dolor sit amet' 35 | ); 36 | 37 | expect(this.ws('Lorum\u200Cips\uFEFFum dolor sit amet')).to.equal( 38 | 'Lorumipsum dolor sit amet' 39 | ); 40 | 41 | expect(this.ws('Lorum\u200Dipsum dolor sit am\uFEFFet')).to.equal( 42 | 'Lorumipsum dolor sit amet' 43 | ); 44 | }); 45 | }); 46 | 47 | context('mode = `keep-newline`', function () { 48 | beforeEach(function () { 49 | this.ws = whitespace('keep-newline'); 50 | }); 51 | 52 | it('simplifies non-newline whitespace in the middle of the string', function () { 53 | expect(this.ws('Lorum ipsum dolor\tsit \r \tamet')).to.equal( 54 | 'Lorum ipsum dolor sit amet' 55 | ); 56 | }); 57 | 58 | it('keeps newline characters in the middle of a string', function () { 59 | expect(this.ws('Lorum \n ipsum\xa0 dolor\tsit \r \n\tamet')).to.equal( 60 | 'Lorum\nipsum dolor sit\namet' 61 | ); 62 | }); 63 | 64 | it('removes non-newline whitespace at the ends of the string', function () { 65 | expect(this.ws(' Lorum ipsum dolor sit amet')).to.equal('Lorum ipsum dolor sit amet'); 66 | 67 | expect(this.ws('\tLorum ipsum dolor sit amet\r')).to.equal( 68 | 'Lorum ipsum dolor sit amet' 69 | ); 70 | 71 | expect(this.ws('\t\xa0 \r Lorum ipsum dolor sit amet')).to.equal( 72 | 'Lorum ipsum dolor sit amet' 73 | ); 74 | }); 75 | 76 | it('keeps newline characters at the ends of a string', function () { 77 | expect(this.ws(' \n Lorum ipsum dolor sit amet\t\n')).to.equal( 78 | '\nLorum ipsum dolor sit amet\n' 79 | ); 80 | 81 | expect(this.ws('\nLorum ipsum dolor sit amet')).to.equal( 82 | '\nLorum ipsum dolor sit amet' 83 | ); 84 | }); 85 | 86 | it('removes zero-width whitespace', function () { 87 | expect(this.ws('Lorum\u200Bipsum dol\uFEFFor sit amet')).to.equal( 88 | 'Lorumipsum dolor sit amet' 89 | ); 90 | 91 | expect(this.ws('Lorum\u200Cips\uFEFFum dolor sit amet')).to.equal( 92 | 'Lorumipsum dolor sit amet' 93 | ); 94 | 95 | expect(this.ws('Lorum\u200Dipsum dolor sit am\uFEFFet')).to.equal( 96 | 'Lorumipsum dolor sit amet' 97 | ); 98 | }); 99 | }); 100 | 101 | context('mode = `keep`', function () { 102 | beforeEach(function () { 103 | this.ws = whitespace('keep'); 104 | }); 105 | 106 | it('does not change the string at all', function () { 107 | const string = 'Lorum \t\r\xa0 ipsum dolor \n\nsit amet\n\r'; 108 | 109 | expect(this.ws(string)).to.equal(string); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /cypress/support/bundle.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | import './common'; 17 | import '../../'; 18 | -------------------------------------------------------------------------------- /cypress/support/common.js: -------------------------------------------------------------------------------- 1 | import chaiString from 'chai-string'; 2 | chai.use(chaiString); 3 | -------------------------------------------------------------------------------- /cypress/support/source.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | import './common'; 17 | import '../../src'; 18 | -------------------------------------------------------------------------------- /docs/attribute.md: -------------------------------------------------------------------------------- 1 | # attribute 2 | 3 | This is a command that does not exist as a default command. 4 | 5 | --- 6 | 7 | Enables you to get the value of an elements attributes. 8 | 9 | > **Note:** When using `.attribute()` you should be aware about how Cypress 10 | > [only retries the last command](https://docs.cypress.io/guides/core-concepts/retry-ability#Only-the-last-command-is-retried). 11 | 12 | ## Syntax 13 | 14 | ```javascript 15 | .attribute(attribute) 16 | .attribute(attribute, options) 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### :heavy_check_mark: Correct Usage 22 | 23 | ```javascript 24 | cy.get('a').attribute('href'); // Yields the value of the `href` attribute 25 | ``` 26 | 27 | ### :x: Incorrect Usage 28 | 29 | ```javascript 30 | cy.attribute('foo'); // Errors, cannot be chained off 'cy' 31 | cy.location().attribute('foo'); // Errors, 'location' does not yield DOM element 32 | ``` 33 | 34 | ## Arguments 35 | 36 | **> attribute** **_(String)_** 37 | 38 | The name of the attribute to be yielded by `.attribute()` 39 | 40 | **> options** **_(Object)_** 41 | 42 | Pass in an options object to change the default behavior of `.attribute()`. 43 | 44 | | Option | Default | Description | 45 | | ------------ | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | 46 | | `timeout` | [`defaultCommandTimeout`](https://docs.cypress.io/guides/references/configuration.html#Timeouts) | Time to wait for `.attribute()` to resolve before [timing out](https://docs.cypress.io/api/commands/then.html#Timeouts) | 47 | | `log` | `false` | Displays the command in the [Command log](https://docs.cypress.io/guides/core-concepts/test-runner.html#Command-Log) | 48 | | `whitespace` | `keep` | Replace complex whitespace with a single regular space.
    Accepted values: `simplify`, `keep-newline` & `keep` | 49 | | `strict` | `true` | Implicitly assert that all subjects have the requested attribute | 50 | 51 | ## Yields 52 | 53 | - `.attribute()` yields the value of a subjects given attribute. 54 | - `.attribute()` yields an array of the values of multiple subjects given attribute. 55 | 56 | ## Examples 57 | 58 | ### An alt attribute 59 | 60 | 61 | ```html 62 | Teriffic tiger 63 | ``` 64 | 65 | ```javascript 66 | // yields "Teriffic Tiger" 67 | cy.get('img').attribute('alt'); 68 | ``` 69 | 70 | ### Multiple subjects 71 | 72 | 73 | ```html 74 | 75 | 76 | ``` 77 | 78 | ```javascript 79 | // yields [ 80 | // "text", 81 | // "submit" 82 | // ] 83 | cy.get('input').attribute('type'); 84 | ``` 85 | 86 | ### Whitespace handling 87 | 88 | By default all whitespace will be kept intact. 89 | 90 | 91 | ```html 92 |
    94 | ``` 95 | 96 | #### Do not simplify whitespace (default) 97 | 98 | ```javascript 99 | // yields " Extravagant \n Eagle " 100 | cy.get('div').attribute('data-attribute'); 101 | ``` 102 | 103 | The default value of `whitespace` is `keep` so the following yields the same. 104 | 105 | ```javascript 106 | // yields " Extravagant \n Eagle " 107 | cy.get('div').attribute('data-attribute', { whitespace: 'keep' }); 108 | ``` 109 | 110 | #### Simplify whitespace 111 | 112 | ```javascript 113 | // yields "Extravagant Eagle" 114 | cy.get('div').attribute('data-attribute', { whitespace: 'simplify' }); 115 | ``` 116 | 117 | #### Simplify whitespace but keep new line characters 118 | 119 | ```javascript 120 | // yields "Extravagant\nEagle" 121 | cy.get('div').attribute('data-attribute', { whitespace: 'keep-newline' }); 122 | ``` 123 | 124 | ### Strict mode 125 | 126 | Strict mode comes into play when using `.attribute()` with multiple subjects. By default strict mode 127 | is enabled. 128 | 129 | 130 | ```html 131 | Amazing armadillo 132 | Everlasting eel 133 | ``` 134 | 135 | #### Strict mode `true` (default) 136 | 137 | Throws an error, because some subjects don't have the `target` attribute. 138 | 139 | ```javascript 140 | // Throws error: Expected all 2 elements to have attribute 'target', but never found it on 1 elements. 141 | cy.get('a').attribute('target'); 142 | ``` 143 | 144 | Yields two values because both subjects have the `href` attribute. 145 | 146 | ```javascript 147 | // yields [ 148 | // "#armadillo", 149 | // "#eel" 150 | // ] 151 | cy.get('a').attribute('href'); 152 | ``` 153 | 154 | #### Strict mode `false` 155 | 156 | Does not throw an error because it's possible to yield a value, even though not all subjects have a 157 | `target` attribute. Any subject that does not have the `target` attribute is ignored. 158 | 159 | ```javascript 160 | // yields "_blank" 161 | cy.get('a').attribute('target', { strict: false }); 162 | ``` 163 | 164 | ## Notes 165 | 166 | ### Empty attributes 167 | 168 | `.attribute()` considers an empty attribute like below as existing, but empty. 169 | 170 | 171 | ```html 172 | 173 | ``` 174 | 175 | ```javascript 176 | cy.get('p').attribute('hidden').should('exist').should('be.empty'); 177 | ``` 178 | 179 | ## Rules 180 | 181 | ### Requirements 182 | 183 | - `.attribute()` requires being chained off a command that yields DOM element(s). 184 | 185 | ### Assertions 186 | 187 | - `.attribute()` will automatically retry until the attribute exist on the subject(s). 188 | - `.attribute()` will automatically retry itself until assertions you've chained all pass. 189 | 190 | ### Timeouts 191 | 192 | - `.attribute()` can time out waiting for a chained assertion to pass. 193 | 194 | ## Command Log 195 | 196 | `.attribute()` will output to the command log. 197 | -------------------------------------------------------------------------------- /docs/request.md: -------------------------------------------------------------------------------- 1 | # request 2 | 3 | This command has been extended with: 4 | 5 | - `.request()` uses the global configuration `requestBaseUrl` over `baseUrl`. This allows you to set 6 | a base url for `.request()` that is ignored by `.visit()`. [See arguments](#arguments) 7 | 8 | > Note: 9 | > [It's not possible to use `requestBaseUrl` in `cypress.config.ts` due to a limitation](../README.md#cypressconfigts-limitation). 10 | 11 | See [original documentation](https://docs.cypress.io/api/commands/request) 12 | 13 | --- 14 | 15 | Make an HTTP request. 16 | 17 | ## Syntax 18 | 19 | ```javascript 20 | cy.request(url); 21 | cy.request(url, body); 22 | cy.request(method, url); 23 | cy.request(method, url, body); 24 | cy.request(options); 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### :heavy_check_mark: Correct Usage 30 | 31 | ```javascript 32 | cy.request('http://dev.local/seed'); 33 | ``` 34 | 35 | ## Arguments 36 | 37 | **> url** **_(string)_** 38 | 39 | The URL to make the request to. 40 | 41 | If you provide a non fully qualified domain name (FQDN), Cypress will make its best guess as to 42 | which host you want `cy.request()` to use in the URL. 43 | 44 | 1. If you make a `cy.request()` after visiting a page, Cypress assumes the url used for the 45 | `cy.visit()` is the host. 46 | 47 | ```javascript 48 | cy.visit('http://localhost:8080/app'); 49 | cy.request('users/1.json'); // url is http://localhost:8080/users/1.json 50 | ``` 51 | 52 | 2. If you make a `cy.request()` prior to visiting a page, Cypress uses the host configured as the 53 | `requestBaseUrl` property inside of your 54 | [configuration file](https://docs.cypress.io/guides/references/configuration). 55 | 56 | ```javascript 57 | // cypress.config.js 58 | const { defineConfig } = require('cypress'); 59 | 60 | module.exports = defineConfig({ 61 | e2e: { 62 | requestBaseUrl: 'http://localhost:1234', 63 | }, 64 | }); 65 | ``` 66 | 67 | ```javascript 68 | cy.request('seed/admin'); // url is http://localhost:1234/seed/admin 69 | ``` 70 | 71 | If the `requestBaseUrl` is empty Cypress will use `baseUrl` instead. 72 | 73 | ```javascript 74 | // cypress.config.js 75 | const { defineConfig } = require('cypress'); 76 | 77 | module.exports = defineConfig({ 78 | e2e: { 79 | requestBaseUrl: '', 80 | baseUrl: 'http://localhost:1234', 81 | }, 82 | }); 83 | ``` 84 | 85 | ```javascript 86 | cy.request('seed/admin'); // url is http://localhost:1234/seed/admin 87 | ``` 88 | 89 | 3. If Cypress cannot determine the host it will throw an error. 90 | 91 | **> body** **_(String, Object)_** 92 | 93 | A request `body` to be sent in the request. Cypress sets the `Accepts` request header and serializes 94 | the response body by the encoding option. 95 | 96 | **> method** **_(String)_** 97 | 98 | Make a request using a specific method. If no method is defined, Cypress uses the `GET` method by 99 | default. 100 | 101 | Supported methods include: 102 | 103 | - `GET` 104 | - `POST` 105 | - `PUT` 106 | - `DELETE` 107 | - `PATCH` 108 | - `HEAD` 109 | - `OPTIONS` 110 | - `TRACE` 111 | - `COPY` 112 | - `LOCK` 113 | - `MKCOL` 114 | - `MOVE` 115 | - `PURGE` 116 | - `PROPFIND` 117 | - `PROPPATCH` 118 | - `UNLOCK` 119 | - `REPORT` 120 | - `MKACTIVITY` 121 | - `CHECKOUT` 122 | - `MERGE` 123 | - `M-SEARCH` 124 | - `NOTIFY` 125 | - `SUBSCRIBE` 126 | - `UNSUBSCRIBE` 127 | - `SEARCH` 128 | - `CONNECT` 129 | 130 | **> options** **_(Object)_** 131 | 132 | Pass in an options object to change the default behavior of `cy.request`. 133 | 134 | | Option | Default | Description | 135 | | -------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 136 | | `log` | `true` | Displays the command in the [Command log](https://docs.cypress.io/guides/core-concepts/test-runner.html#Command-Log) | 137 | | `url` | `null` | The URL to make the request to | 138 | | `method` | `GET` | The HTTP method to use in the request | 139 | | `auth` | `null` | Adds Authorization headers. ['Accepts these options.'](https://github.com/request/request#http-authentication) | 140 | | `body` | `null` | Body to send along with the request | 141 | | `failOnStatusCode` | `true` | Whether to fail on response codes other than `2xx` and `3xx` | 142 | | `followRedirect` | `true` | Whether to automatically follow redirects | 143 | | `form` | `false` | Whether to convert the `body` values to url encoded content and set the `x-www-form-urlencoded` header | 144 | | `encoding` | `utf8` | The encoding to be used when serializing the response body. The following encodings are supported: `ascii`, `base64`, `binary`, `hex`, `latin1`, `utf8`, `utf-8`, `ucs2`, `ucs-2`, `utf16le`, `utf-16le` | 145 | | `gzip` | `true` | Whether to accept the `gzip` encoding | 146 | | `headers` | `null` | Additional headers to send; Accepts object literal | 147 | | `qs` | `null` | Query parameters to append to the `url` of the request | 148 | | `retryOnStatusCodeFailure` | `false` | Whether Cypress should automatically retry status code errors under the hood. Cypress will retry a request up to 4 times if this is set to true. | 149 | | `retryOnNetworkFailure` | `true` | Whether Cypress should automatically retry transient network errors under the hood. Cypress will retry a request up to 4 times if this is set to true. | 150 | | `timeout` | [`responseTimeout`](https://docs.cypress.io/guides/references/configuration.html#Timeouts) | Time to wait for `cy.request()` to resolve before [timing out](https://docs.cypress.io/api/commands/request.html#Timeouts) | 151 | 152 | You can also set options for `cy.request`'s `requestBaseUrl`, `baseUrl` and `responseTimeout` 153 | globally in the [Cypress configuration](https://docs.cypress.io/guides/references/configuration). 154 | 155 | ## Yields 156 | 157 | `cy.request()` yields the `response` as an object literal containing properties such as: 158 | 159 | - `status` 160 | - `body` 161 | - `headers` 162 | - `duration` 163 | 164 | ## Examples 165 | 166 | ### URL 167 | 168 | #### Make a `GET` request 169 | 170 | `cy.request()` is great for talking to an external endpoint before your tests to seed a database. 171 | 172 | ```javascript 173 | beforeEach(function () { 174 | cy.request('http://localhost:8080/db/seed'); 175 | }); 176 | ``` 177 | 178 | #### Issue an HTTP request 179 | 180 | Sometimes it's quicker to test the contents of a page rather than 181 | [`cy.visit()`](https://docs.cypress.io/api/commands/visit.html) and wait for the entire page and all 182 | of its resources to load. 183 | 184 | ```javascript 185 | cy.request('/admin').its('body').should('include', '

    Admin

    '); 186 | ``` 187 | 188 | ### Method and URL 189 | 190 | #### Send a `DELETE` request 191 | 192 | ```javascript 193 | cy.request('DELETE', 'http://localhost:8888/users/827'); 194 | ``` 195 | 196 | #### Alias the request using [`.as()`](https://docs.cypress.io/api/commands/as) 197 | 198 | ```javascript 199 | cy.request('https://jsonplaceholder.cypress.io/comments').as('comments'); 200 | 201 | cy.get('@comments').should((response) => { 202 | expect(response.body).to.have.length(500); 203 | expect(response).to.have.property('headers'); 204 | expect(response).to.have.property('duration'); 205 | }); 206 | ``` 207 | 208 | ### Method, URL, and Body 209 | 210 | #### Send a `POST` request with a JSON body 211 | 212 | ```javascript 213 | cy.request('POST', 'http://localhost:8888/users/admin', { name: 'Jane' }).then((response) => { 214 | // response.body is automatically serialized into JSON 215 | expect(response.body).to.have.property('name', 'Jane'); // true 216 | }); 217 | ``` 218 | 219 | ### Options 220 | 221 | #### Request a page while disabling auto redirect 222 | 223 | To test the redirection behavior of a login without a session, `cy.request` can be used to check the 224 | `status` and `redirectedToUrl` property. 225 | 226 | The `redirectedToUrl` property is a special Cypress property that normalizes the URL the browser 227 | would normally follow during a redirect. 228 | 229 | ```javascript 230 | cy.request({ 231 | url: '/dashboard', 232 | followRedirect: false, // turn off following redirects 233 | }).then((resp) => { 234 | // redirect status code is 302 235 | expect(resp.status).to.eq(302); 236 | expect(resp.redirectedToUrl).to.eq('http://localhost:8082/unauthorized'); 237 | }); 238 | ``` 239 | 240 | #### Download a PDF file 241 | 242 | By passing the `encoding: binary` option, the `response.body` will be serialized binary content of 243 | the file. You can use this to access various file types via `.request()` like `.pdf`, `.zip`, or 244 | `.doc` files. 245 | 246 | ```javascript 247 | cy.request({ 248 | url: 'http://localhost:8080/some-document.pdf', 249 | encoding: 'binary', 250 | }).then((response) => { 251 | cy.writeFile('path/to/save/document.pdf', response.body, 'binary'); 252 | }); 253 | ``` 254 | 255 | #### Get Data URL of an image 256 | 257 | By passing the `encoding: base64` option, the `response.body` will be base64-encoded content of the 258 | image. You can use this to construct a 259 | [Data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) for use 260 | elsewhere. 261 | 262 | ```javascript 263 | cy.request({ 264 | url: 'https://docs.cypress.io/img/logo.png', 265 | encoding: 'base64', 266 | }).then((response) => { 267 | const base64Content = response.body; 268 | const mime = response.headers['content-type']; // or 'image/png' 269 | // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 270 | const imageDataUrl = `data:${mime};base64,${base64Content}`; 271 | }); 272 | ``` 273 | 274 | #### HTML form submissions using form option 275 | 276 | Oftentimes, once you have a proper e2e test around logging in, there's no reason to continue to 277 | `cy.visit()` the login and wait for the entire page to load all associated resources before running 278 | any other commands. Doing so can slow down our entire test suite. 279 | 280 | Using `cy.request()`, we can bypass all of this because it automatically gets and sets cookies as if 281 | the requests had come from the browser. 282 | 283 | ```javascript 284 | cy.request({ 285 | method: 'POST', 286 | url: '/login_with_form', // baseUrl is prepend to URL 287 | form: true, // indicates the body should be form urlencoded and sets Content-Type: application/x-www-form-urlencoded headers 288 | body: { 289 | username: 'jane.lane', 290 | password: 'password123', 291 | }, 292 | }); 293 | 294 | // to prove we have a session 295 | cy.getCookie('cypress-session-cookie').should('exist'); 296 | ``` 297 | 298 | #### Using `cy.request()` for HTML Forms 299 | 300 | > [Check out our example recipe using cy.request() for HTML web forms](https://docs.cypress.io/examples/examples/recipes#Logging-In) 301 | 302 | ### Request Polling 303 | 304 | #### Call `cy.request()` over and over again 305 | 306 | This is useful when you're polling a server for a response that may take awhile to complete. 307 | 308 | All we're really doing here is creating a recursive function. Nothing more complicated than that. 309 | 310 | ```javascript 311 | // a regular ol' function folks 312 | function req () { 313 | cy 314 | .request(...) 315 | .then((resp) => { 316 | // if we got what we wanted 317 | 318 | if (resp.status === 200 && resp.body.ok === true) 319 | // break out of the recursive loop 320 | return 321 | 322 | // else recurse 323 | req() 324 | }) 325 | } 326 | 327 | cy 328 | // do the thing causing the side effect 329 | .get('button').click() 330 | 331 | // now start the requests 332 | .then(req) 333 | ``` 334 | 335 | ## Notes 336 | 337 | ### Debugging 338 | 339 | #### Request is not displayed in the Network Tab of Developer Tools 340 | 341 | Cypress does not _actually_ make an XHR request from the browser. We are actually making the HTTP 342 | request from the Cypress Test Runner (in Node). So, you won't see the request inside of your 343 | Developer Tools. 344 | 345 | ### Cors 346 | 347 | #### CORS is bypassed 348 | 349 | Normally when the browser detects a cross-origin HTTP request, it will send an `OPTIONS` preflight 350 | check to ensure the server allows cross-origin requests, but `cy.request()` bypasses CORS entirely. 351 | 352 | ```javascript 353 | // we can make requests to any external server, no problem. 354 | cy.request('https://www.google.com/webhp?#q=cypress.io+cors') 355 | .its('body') 356 | .should('include', 'Testing, the way it should be'); // true 357 | ``` 358 | 359 | ### Cookies 360 | 361 | #### Cookies are automatically sent and received 362 | 363 | Before sending the HTTP request, we automatically attach cookies that would have otherwise been 364 | attached had the request come from the browser. Additionally, if a response has a `Set-Cookie` 365 | header, these are automatically set back on the browser cookies. 366 | 367 | In other words, `cy.request()` transparently performs all of the underlying functions as if it came 368 | from the browser. 369 | 370 | ### [`cy.intercept()`](https://docs.cypress.io/api/commands/intercept), [`cy.server()`](https://docs.cypress.io/api/commands/server), and [`cy.route()`](https://docs.cypress.io/api/commands/route) 371 | 372 | #### `cy.request()` sends requests to actual endpoints, bypassing those defined using `cy.route()` or `cy.intercept()` 373 | 374 | `cy.server()` and any configuration passed to 375 | [`cy.server()`](https://docs.cypress.io/api/commands/server) has no effect on `cy.request()`. 376 | 377 | The intention of `cy.request()` is to be used for checking endpoints on an actual, running server 378 | without having to start the front end application. 379 | 380 | ## Rules 381 | 382 | ### Requirements 383 | 384 | - `cy.request()` requires being chained off of `cy`. 385 | - `cy.request()` requires that the server send a response. 386 | - `cy.request()` requires that the response status code be `2xx` or `3xx` when `failOnStatusCode` is 387 | `true`. 388 | 389 | ### Assertions 390 | 391 | - `cy.request()` will only run assertions you've chained once, and will not retry. 392 | 393 | ### Timeouts 394 | 395 | - `cy.request()` can time out waiting for the server to respond. 396 | 397 | ## Command Log 398 | 399 | ### Request comments endpoint and test response 400 | 401 | ```javascript 402 | cy.request('https://jsonplaceholder.typicode.com/comments').then((response) => { 403 | expect(response.status).to.eq(200); 404 | expect(response.body).to.have.length(500); 405 | expect(response).to.have.property('headers'); 406 | expect(response).to.have.property('duration'); 407 | }); 408 | ``` 409 | 410 | The commands above will display in the Command Log as: 411 | 412 | ![Command Log request](https://docs.cypress.io/_nuxt/img/testing-request-url-and-its-response-body-headers.84336c4.png) 413 | 414 | When clicking on `request` within the command log, the console outputs the following: 415 | 416 | ![Console log request](https://docs.cypress.io/_nuxt/img/console-log-request-response-body-headers-status-url.2727bc9.png) 417 | 418 | ## History 419 | 420 | | Version | Changes | 421 | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 422 | | 4.7.0 | Added support for `encoding` option. | 423 | | 3.3.0 | Added support for options `retryOnStatusCodeFailure` and `retryOnNetworkFailure`. | 424 | | 3.2.0 | Added support for any valid HTTP `method` argument including `TRACE`, `COPY`, `LOCK`, `MKCOL`, `MOVE`, `PURGE`, `PROPFIND`, `PROPPATCH`, `UNLOCK`, `REPORT`, `MKACTIVITY`, `CHECKOUT`, `MERGE`, `M-SEARCH`, `NOTIFY`, `SUBSCRIBE`, `UNSUBSCRIBE`, `SEARCH`, and `CONNECT`. | 425 | 426 | ## See also 427 | 428 | - [`cy.exec()`](https://docs.cypress.io/api/commands/exec) 429 | - [`cy.task()`](https://docs.cypress.io/api/commands/task) 430 | - [`cy.visit()`](https://docs.cypress.io/api/commands/visit) 431 | - [Recipe: Logging In - Single Sign on](https://docs.cypress.io/examples/examples/recipes#Logging-In) 432 | - [Recipe: Logging In - HTML Web Form](https://docs.cypress.io/examples/examples/recipes#Logging-In) 433 | - [Recipe: Logging In - XHR Web Form](https://docs.cypress.io/examples/examples/recipes#Logging-In) 434 | - [Recipe: Logging In - CSRF Tokens](https://docs.cypress.io/examples/examples/recipes#Logging-In) 435 | -------------------------------------------------------------------------------- /docs/text.md: -------------------------------------------------------------------------------- 1 | # text 2 | 3 | This is a command that does not exist as a default command. 4 | 5 | --- 6 | 7 | Enables you to get the text contents of the subject yielded from the previous command. 8 | 9 | `.text()` allows you to be more specific than you can be with `.contains()` or `.should('contain')`. 10 | 11 | > **Note:** When using `.text()` you should be aware about how Cypress 12 | > [only retries the last command](https://docs.cypress.io/guides/core-concepts/retry-ability#Only-the-last-command-is-retried). 13 | 14 | ## Syntax 15 | 16 | ```javascript 17 | .text() 18 | .text(options) 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### :heavy_check_mark: Correct Usage 24 | 25 | ```javascript 26 | cy.get('nav').text(); // Yields the text inside the `nav` element 27 | ``` 28 | 29 | ### :x: Incorrect Usage 30 | 31 | ```javascript 32 | cy.text(); // Errors, cannot be chained off 'cy' 33 | cy.location().text(); // Errors, 'location' does not yield DOM element 34 | ``` 35 | 36 | ## Arguments 37 | 38 | **> options** **_(Object)_** 39 | 40 | Pass in an options object to change the default behavior of `.text()`. 41 | 42 | | Option | Default | Description | 43 | | ------------ | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | 44 | | `timeout` | [`defaultCommandTimeout`](https://docs.cypress.io/guides/references/configuration.html#Timeouts) | Time to wait for `.text()` to resolve before [timing out](https://docs.cypress.io/api/commands/then.html#Timeouts) | 45 | | `log` | `true` | Displays the command in the [Command log](https://docs.cypress.io/guides/core-concepts/test-runner.html#Command-Log) | 46 | | `whitespace` | `simplify` | Replace complex whitespace with a single regular space.
    Accepted values: `simplify`, `keep-newline` & `keep` | 47 | | `depth` | `0` | Include the text contents of child elements upto `n` levels | 48 | 49 | ## Yields 50 | 51 | - `.text()` yields the text inside the subject. 52 | - `.text()` yields an array of the texts inside multiple subjects. 53 | 54 | ## Examples 55 | 56 | ### No Args 57 | 58 | #### Get the text of a div 59 | 60 | 61 | ```html 62 |
    Teriffic Tiger
    63 | ``` 64 | 65 | ```javascript 66 | // yields "Teriffic Tiger" 67 | cy.get('div').text(); 68 | ``` 69 | 70 | #### Get the text of multiple divs 71 | 72 | 73 | ```html 74 |
    Catastrophic Cat
    75 |
    Dramatic Dog
    76 |
    Amazing Ant
    77 | ``` 78 | 79 | ```javascript 80 | // yields [ 81 | // "Catastrophic Cat", 82 | // "Dramatic Dog", 83 | // "Amazing Ant" 84 | // ] 85 | cy.get('div').text(); 86 | ``` 87 | 88 | ### Whitespace handling 89 | 90 | By default all whitespace will be simplified. 91 | 92 | 93 | ```html 94 |
    Extravagant   95 | Eagle
    96 | ``` 97 | 98 | #### Simplify whitespace by default 99 | 100 | ```javascript 101 | // yields "Extravagant Eagle" 102 | cy.get('div').text(); 103 | ``` 104 | 105 | The default value of `whitespace` is `simplify` so the following yields the same. 106 | 107 | ```javascript 108 | // yields "Extravagant Eagle" 109 | cy.get('div').text({ whitespace: 'simplify' }); 110 | ``` 111 | 112 | #### Simplify whitespace but keep newline characters 113 | 114 | ```javascript 115 | // yields "Extravagant\nEagle" 116 | cy.get('div').text({ whitespace: 'keep-newline' }); 117 | ``` 118 | 119 | #### Do not simplify whitespace 120 | 121 | ```javascript 122 | // yields "Extravagant \n Eagle" 123 | cy.get('div').text({ whitespace: 'keep' }); 124 | ``` 125 | 126 | Note that the whitespace at the beginning and end of the string is still removed. 127 | 128 | ### Depth of elements 129 | 130 | By default only the text of the subject itself will be yielded. Use this option to also get the text 131 | of underlying elements. 132 | 133 | 134 | ```html 135 |
    136 | Grandma Gazelle 137 |
    138 | Mother Meerkat 139 |
    140 | Son Scorpion 141 |
    142 |
    143 |
    144 | Father Fox 145 |
    146 |
    147 | ``` 148 | 149 | #### Only the subject by default 150 | 151 | ```javascript 152 | // yields "Grandma Gazelle" 153 | cy.get('.grandparent').text(); 154 | ``` 155 | 156 | The default value of `depth` is `0` so the following yields the same. 157 | 158 | ```javascript 159 | // yields "Grandma Gazelle" 160 | cy.get('.grandparent').text({ depth: 0 }); 161 | ``` 162 | 163 | #### Include the direct children 164 | 165 | The text of the child elements are concatenated and yielded as a single string. 166 | 167 | ```javascript 168 | // yields "Grandma Gazelle Mother Meerkat Father Fox" 169 | cy.get('.grandparent').text({ depth: 1 }); 170 | ``` 171 | 172 | #### Multiple elements with depth 173 | 174 | Selecting multiple elements will yield an array of concatenated strings. 175 | 176 | ```javascript 177 | // yields [ 178 | // "Mother Meerkat Son Scorpion", 179 | // "Father Fox" 180 | // ] 181 | cy.get('.parent').text({ depth: 1 }); 182 | ``` 183 | 184 | #### Remove all depth limitations 185 | 186 | To infinity and beyond! 187 | 188 | ```javascript 189 | // yields "Grandma Gazelle Mother Meerkat Son Scorpion Father Fox" 190 | cy.get('.grandparent').text({ depth: Infinity }); 191 | ``` 192 | 193 | ## Notes 194 | 195 | ### Form elements 196 | 197 | `.text()` also gets text from form elements like `input` and `textarea`. 198 | 199 | ```javascript 200 | cy.get('input').text(); 201 | ``` 202 | 203 | ## Rules 204 | 205 | ### Requirements 206 | 207 | - `.text()` requires being chained off a command that yields DOM element(s). 208 | 209 | ### Assertions 210 | 211 | - `.text()` will automatically retry itself until assertions you've chained all pass. 212 | 213 | ### Timeouts 214 | 215 | - `.text()` can time out waiting for a chained assertion to pass. 216 | 217 | ## Command Log 218 | 219 | `.text()` will output to the command log. 220 | 221 | ## See also 222 | 223 | - [`.contains()`](https://docs.cypress.io/api/commands/contains.html) 224 | -------------------------------------------------------------------------------- /docs/then.md: -------------------------------------------------------------------------------- 1 | # then 2 | 3 | This command has been extended with: 4 | 5 | - The option `retry` which allows you to retry the function passed to `.then()` untill all 6 | assertions pass. 7 | - The option `log` which allows you to output `.then()` to the command log. 8 | 9 | See [original documentation](https://docs.cypress.io/api/commands/then) 10 | 11 | --- 12 | 13 | Enables you to work with the subject yielded from the previous command. 14 | 15 | > **Note:** `.then()` assumes you are already familiar with core concepts such as 16 | > [closures](https://docs.cypress.io/guides/core-concepts/variables-and-aliases#Closures). 17 | 18 | > **Note:** Prefer 19 | > ['`.should()` with callback](https://docs.cypress.io/api/commands/should#Function) over `.then()` 20 | > for assertions as they are automatically rerun until no assertions throw within it but be aware of 21 | > [differences](https://docs.cypress.io/api/commands/should#Differences). 22 | 23 | ## Syntax 24 | 25 | ```javascript 26 | .then(callbackFn) 27 | .then(options, callbackFn) 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### :heavy_check_mark: Correct Usage 33 | 34 | ```javascript 35 | cy.get('.nav').then(($nav) => {}); // Yields .nav as first arg 36 | cy.location().then((loc) => {}); // Yields location object as first arg 37 | ``` 38 | 39 | ## Arguments 40 | 41 | **> options** **_(Object)_** 42 | 43 | Pass in an options object to change the default behavior of `.then()`. 44 | 45 | | Option | Default | Description | 46 | | --------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | 47 | | `timeout` | [`defaultCommandTimeout`](https://docs.cypress.io/guides/references/configuration.html#Timeouts) | Time to wait for `.then()` to resolve before [timing out](https://docs.cypress.io/api/commands/then.html#Timeouts) | 48 | | `retry` | `false` | Retry itself until chained assertions pass | 49 | | `log` | `false` | Displays the command in the [Command log](https://docs.cypress.io/guides/core-concepts/test-runner.html#Command-Log) | 50 | 51 | **> callbackFn** **_(Function)_** 52 | 53 | Pass a function that takes the previously yielded subject as its first argument. 54 | 55 | ## Yields 56 | 57 | `.then()` is modeled identically to the way Promises work in JavaScript. Whatever is returned from 58 | the callback function becomes the new subject and will flow into the next command (with the 59 | exception of `undefined`). 60 | 61 | Additionally, the result of the last Cypress command in the callback function will be yielded as the 62 | new subject and flow into the next command if there is no `return`. 63 | 64 | When `undefined` is returned by the callback function, the subject will not be modified and will 65 | instead carry over to the next command. 66 | 67 | Just like Promises, you can return any compatible "thenable" (anything that has a `.then()` 68 | interface) and Cypress will wait for that to resolve before continuing forward through the chain of 69 | commands. 70 | 71 | ## Examples 72 | 73 | > We have several more examples in our 74 | > [Core Concepts Guide](https://docs.cypress.io/guides/core-concepts/variables-and-aliases) which go 75 | > into the various ways you can use `.then()` to store, compare, and debug values. 76 | 77 | ### DOM element 78 | 79 | #### The `button` element is yielded 80 | 81 | ```javascript 82 | cy.get('button').then(($btn) => { 83 | const cls = $btn.attr('class'); 84 | 85 | cy.wrap($btn).click().should('not.have.class', cls); 86 | }); 87 | ``` 88 | 89 | #### The number is yielded from previous command 90 | 91 | ```javascript 92 | cy.wrap(1) 93 | .then((num) => { 94 | cy.wrap(num).should('equal', 1); // true 95 | }) 96 | .should('equal', 1); // true 97 | ``` 98 | 99 | ### Change subject 100 | 101 | #### The el subject is changed with another command 102 | 103 | ```javascript 104 | cy.get('button') 105 | .then(($btn) => { 106 | const cls = $btn.attr('class'); 107 | 108 | cy.wrap($btn).click().should('not.have.class', cls).find('i'); 109 | // since there is no explicit return 110 | // the last Cypress command's yield is yielded 111 | }) 112 | .should('have.class', 'spin'); // assert on i element 113 | ``` 114 | 115 | #### The number subject is changed with another command 116 | 117 | ```javascript 118 | cy.wrap(1).then((num) => { 119 | cy.wrap(num)).should('equal', 1) // true 120 | cy.wrap(2) 121 | }).should('equal', 2) // true 122 | ``` 123 | 124 | #### The number subject is changed by returning 125 | 126 | ```javascript 127 | cy.wrap(1) 128 | .then((num) => { 129 | cy.wrap(num).should('equal', 1); // true 130 | 131 | return 2; 132 | }) 133 | .should('equal', 2); // true 134 | ``` 135 | 136 | #### Returning `undefined` will not modify the yielded subject 137 | 138 | ```javascript 139 | cy.get('form') 140 | .then(($form) => { 141 | console.log('form is:', $form); 142 | // undefined is returned here, but $form will be 143 | // yielded to allow for continued chaining 144 | }) 145 | .find('input') 146 | .then(($input) => { 147 | // we have our $input element here since 148 | // our form element was yielded and we called 149 | // .find('input') on it 150 | }); 151 | ``` 152 | 153 | ### Raw HTMLElements are wrapped with jQuery 154 | 155 | ```javascript 156 | cy.get('div') 157 | .then(($div) => { 158 | return $div[0]; // type => HTMLDivElement 159 | }) 160 | .then(($div) => { 161 | $div; // type => JQuery 162 | }); 163 | ``` 164 | 165 | ### Promises 166 | 167 | Cypress waits for Promises to resolve before continuing 168 | 169 | #### Example using Q 170 | 171 | ```javascript 172 | cy.get('button') 173 | .click() 174 | .then(($button) => { 175 | const p = Q.defer(); 176 | 177 | setTimeout(() => { 178 | p.resolve(); 179 | }, 1000); 180 | 181 | return p.promise; 182 | }); 183 | ``` 184 | 185 | #### Example using bluebird 186 | 187 | ```javascript 188 | cy.get('button') 189 | .click() 190 | .then(($button) => { 191 | return Promise.delay(1000); 192 | }); 193 | ``` 194 | 195 | #### Example using jQuery deferred's 196 | 197 | ```javascript 198 | cy.get('button') 199 | .click() 200 | .then(($button) => { 201 | const df = $.Deferred(); 202 | 203 | setTimeout(() => { 204 | df.resolve(); 205 | }, 1000); 206 | 207 | return df; 208 | }); 209 | ``` 210 | 211 | ### Retryability 212 | 213 | The default Cypress command has been extended to allow you to retry a function until chained 214 | assertions pass. Use this sparsely. If you find yourself using this all the time you are probably 215 | doing it wrong. In most cases there are more suitable commands to get what you need. 216 | 217 | ```javascript 218 | cy.get('form') 219 | .then({ retry: true }, ($form) => { 220 | // We have acces to the jQuery object describing the form 221 | // here. We can now do some operations on it without using 222 | // Cypress commands, while we maintain retryability. 223 | 224 | // Note that this function might be executed multiple 225 | // times. Keep it light and make sure you know what 226 | // you're doing. 227 | 228 | return 'foo'; 229 | }) 230 | .should('equal', 'foo'); 231 | ``` 232 | 233 | ## Notes 234 | 235 | ### Differences 236 | 237 | #### What’s the difference between `.then()` and `.should()`/`.and()`? 238 | 239 | Using `.then()` allows you to use the yielded subject in a callback function and should be used when 240 | you need to manipulate some values or do some actions. 241 | 242 | When using a callback function with `.should()`, `.and()` or `.then({ retry: true })`, on the other 243 | hand, there is special logic to rerun the callback function until no assertions throw within it. You 244 | should be careful of side affects in a `.should()`, `.and()`, or `.then({ retry: true })` callback 245 | function that you would not want performed multiple times. 246 | 247 | ## Rules 248 | 249 | ### Requirements 250 | 251 | - `.then()` requires being chained off a previous command. 252 | 253 | ### Assertions 254 | 255 | - `.then()` will only run assertions you've chained once, and will not 256 | [retry](https://docs.cypress.io/guides/core-concepts/retry-ability) (unless you use the 257 | `retry: true` option). 258 | 259 | ### Timeouts 260 | 261 | - `.then()` can time out waiting for a promise you've returned to resolve. 262 | - `.then()` can time out waiting for a chained assertion to pass. (when using the `retry: true` 263 | option) 264 | 265 | ## Command Log 266 | 267 | - `.then()` only logs in the Command Log in the following situations: 268 | - the option `log` is set to `true` 269 | - the option `retry` is set to `true` and the option `log` is not set 270 | 271 | ## History 272 | 273 | | Version | Changes | 274 | | ------- | ----------------------- | 275 | | 0.14.0 | Added timeout option | 276 | | < 0.3.3 | `.then()` command added | 277 | 278 | ## See also 279 | 280 | - [`.and()`](https://docs.cypress.io/api/commands/and) 281 | - [`.each()`](https://docs.cypress.io/api/commands/each) 282 | - [`.invoke()`](https://docs.cypress.io/api/commands/invoke) 283 | - [`.its()`](https://docs.cypress.io/api/commands/its) 284 | - [`.should()`](https://docs.cypress.io/api/commands/should) 285 | - [`.spread()`](https://docs.cypress.io/api/commands/spread) 286 | - [Guide: Using Closures to compare values](https://docs.cypress.io/guides/core-concepts/variables-and-aliases#Closures) 287 | - [Guide: Chains of Commands](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Chains-of-Commands) 288 | -------------------------------------------------------------------------------- /docs/to.md: -------------------------------------------------------------------------------- 1 | # to 2 | 3 | This is a command that does not exist as a default command. 4 | 5 | --- 6 | 7 | Cast the subject to another type. Will do nothing if the subject is already of that type. 8 | 9 | > **Note:** When using `.to()` you should be aware about how Cypress 10 | > [only retries the last command](https://docs.cypress.io/guides/core-concepts/retry-ability#Only-the-last-command-is-retried). 11 | 12 | ## Syntax 13 | 14 | ```javascript 15 | .to(type) 16 | .to(type, options) 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### :heavy_check_mark: Correct Usage 22 | 23 | ```javascript 24 | cy.wrap('00123').to('number'); // Yields 123 25 | cy.wrap(42).to('string'); // Yields '42' 26 | cy.wrap({ passive: 'Parakeet' }).to('string'); // Yields '{"passive":"Parakeet"}' 27 | cy.wrap('Underwhelming Uakari').to('array'); // Yields ['Underwhelming Uakari'] 28 | ``` 29 | 30 | ### :x: Incorrect Usage 31 | 32 | ```javascript 33 | cy.to('string'); // Errors, cannot be chained off 'cy' 34 | cy.wrap('Dangerous dog').to('number'); // Errors, string can't be casted to number 35 | ``` 36 | 37 | ## Arguments 38 | 39 | **> type** **_(string)_** 40 | 41 | The type you want to cast the subject to. Must be one of `number`, `string` or `array`. 42 | 43 | **> options** **_(Object)_** 44 | 45 | Pass in an options object to change the default behavior of `.to()`. 46 | 47 | | Option | Default | Description | 48 | | --------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | 49 | | `timeout` | [`defaultCommandTimeout`](https://docs.cypress.io/guides/references/configuration.html#Timeouts) | Time to wait for `.to()` to resolve before [timing out](https://docs.cypress.io/api/commands/then.html#Timeouts) | 50 | | `log` | `true` | Displays the command in the [Command log](https://docs.cypress.io/guides/core-concepts/test-runner.html#Command-Log) | 51 | 52 | ## Yields 53 | 54 | - `.to('array')` yields an array 55 | - `.to('string')` yields a string or array of strings 56 | - `.to('number')` yields a number or array of numbers 57 | 58 | ## Examples 59 | 60 | ### Casting a string to a number 61 | 62 | ```javascript 63 | // yields 42 64 | cy.wrap('042').to('number'); 65 | ``` 66 | 67 | ### Casting a sring to an array 68 | 69 | ```javascript 70 | // yields ['042'] 71 | cy.wrap('042').to('array'); 72 | ``` 73 | 74 | ### Casting an object to a string 75 | 76 | Uses `JSON.stringify`. 77 | 78 | ```javascript 79 | // yields '{"foo":"bar"}' 80 | cy.wrap({ foo: 'bar' }).to('string'); 81 | ``` 82 | 83 | ### Casting an array of numbers to an array of strings 84 | 85 | ```javascript 86 | // yields [ '123', '456', '789' ] 87 | cy.wrap([123, 456, 789]).to('string'); 88 | ``` 89 | 90 | ### Casting an array to an array 91 | 92 | When trying to cast to the type of the subject `.to()` will do nothing. 93 | 94 | ```javascript 95 | // yields [ 'foo' ] 96 | cy.wrap(['foo']).to('array'); 97 | ``` 98 | 99 | ## Notes 100 | 101 | ### Ensuring iterability 102 | 103 | Some commands, like `.text()`, yield a string when there is only a single subject, but an array when 104 | there are multiple subjects. You can use `.to('array')` to ensure you can loop over the results of 105 | `.text()` without the risk of an error. 106 | 107 | ```javascript 108 | cy.get('.maybeOneElement') 109 | .text() 110 | .to('array') 111 | .each((text) => { 112 | // ... 113 | }); 114 | ``` 115 | 116 | ## Rules 117 | 118 | ### Requirements 119 | 120 | - `.to()` requires being chained off a previous command. 121 | 122 | ### Assertions 123 | 124 | - `.to()` will automatically retry itself until the subject can be casted. 125 | - `.to()` will automatically retry itself until assertions you've chained all pass. 126 | 127 | ### Timeouts 128 | 129 | - `.to()` can time out waiting for a chained assertion to pass. 130 | 131 | ## Command Log 132 | 133 | `.to()` will output to the command log. 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-commands", 3 | "version": "3.0.0", 4 | "description": "A collection of Cypress commands to extend and complement the default commands", 5 | "license": "MIT", 6 | "main": "dist/cypress-commands.js", 7 | "module": "dist/cypress-commands.mjs", 8 | "types": "dist/types/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "test": "npm run test:source", 14 | "test:source": "start-server-and-test start:server http://localhost:1337 run:cypress", 15 | "test:bundle": "start-server-and-test start:server http://localhost:1337 run:cypress:bundle", 16 | "run:cypress": "npm run run:cypress:source", 17 | "run:cypress:source": "cypress run --config supportFile=cypress/support/source.js", 18 | "run:cypress:bundle": "cypress run --config supportFile=cypress/support/bundle.js", 19 | "start": "npm run start:source", 20 | "start:server": "static-server app -p 1337", 21 | "start:source": "start-server-and-test start:server http://localhost:1337 start:cypress", 22 | "start:bundle": "start-server-and-test start:server http://localhost:1337 start:cypress:bundle", 23 | "start:cypress": "npm run start:cypress:source", 24 | "start:cypress:source": "cypress open --config supportFile=cypress/support/source.js", 25 | "start:cypress:bundle": "cypress open --config supportFile=cypress/support/bundle.js", 26 | "lint": "eslint ./", 27 | "bundle": "rollup -c", 28 | "prepublishOnly": "npm run lint && npm run bundle && npm run test:bundle" 29 | }, 30 | "author": { 31 | "name": "Sander van Beek" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/Lakitna/cypress-commands" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/Lakitna/cypress-commands/issues" 39 | }, 40 | "keywords": [ 41 | "Cypress", 42 | "command", 43 | "attribute", 44 | "text", 45 | "to" 46 | ], 47 | "devDependencies": { 48 | "chai-string": "^1.5.0", 49 | "cypress": "^11.0.1", 50 | "eslint": "^8.27.0", 51 | "eslint-config-google": "^0.14.0", 52 | "eslint-plugin-cypress": "^2.12.1", 53 | "eslint-plugin-import": "^2.26.0", 54 | "eslint-plugin-sonarjs": "^0.16.0", 55 | "request": "^2.88.2", 56 | "rollup": "^2.75.6", 57 | "rollup-plugin-commonjs": "^10.1.0", 58 | "rollup-plugin-copy": "^3.4.0", 59 | "rollup-plugin-delete": "^2.0.0", 60 | "rollup-plugin-json": "^4.0.0", 61 | "start-server-and-test": "^1.14.0", 62 | "static-server": "^2.2.1", 63 | "typescript": "^4.7.3" 64 | }, 65 | "dependencies": { 66 | "path-browserify": "^1.0.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json'; 2 | import del from 'rollup-plugin-delete'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import json from 'rollup-plugin-json'; 5 | import copy from 'rollup-plugin-copy'; 6 | 7 | const config = [ 8 | { 9 | input: './src/index.js', 10 | output: [ 11 | { 12 | file: pkg.main, 13 | format: 'cjs', 14 | globals: { 15 | Cypress: 'cypress', 16 | }, 17 | }, 18 | { file: pkg.module, format: 'es' }, 19 | ], 20 | plugins: [ 21 | // Delete contents of target folder 22 | del({ 23 | targets: pkg.files, 24 | }), 25 | 26 | // Resolve JSON files 27 | json(), 28 | 29 | // Compile to commonjs and bundle 30 | commonjs(), 31 | 32 | // Copy type definitions to target folder 33 | copy({ 34 | targets: [{ src: './types/**/*.d.ts', dest: './dist/types' }], 35 | }), 36 | ], 37 | 38 | /** 39 | * Mark all dependencies as external to prevent Rollup from 40 | * including them in the bundle. We'll let the package manager 41 | * take care of dependency resolution and stuff so we don't 42 | * have to download the exact same code multiple times, once 43 | * in this bundle and also as a dependency of another package. 44 | */ 45 | external: [...Object.keys(pkg.dependencies || {})], 46 | }, 47 | ]; 48 | 49 | module.exports = config; 50 | -------------------------------------------------------------------------------- /src/attribute.js: -------------------------------------------------------------------------------- 1 | const _ = Cypress._; 2 | const $ = Cypress.$; 3 | 4 | import { markCurrentCommand, upcomingAssertionNegatesExistence } from './utils/commandQueue'; 5 | import { command } from './utils/errorMessages'; 6 | const errMsg = command.attribute; 7 | 8 | import whitespace from './utils/whitespace'; 9 | import OptionValidator from './utils/optionValidator'; 10 | const validator = new OptionValidator('attribute'); 11 | 12 | /** 13 | * Get the value of an attribute of the subject 14 | * 15 | * @example 16 | * cy.get('a').attribute('href'); 17 | * 18 | * @param {Object} [options] 19 | * @param {boolean} [options.log=true] 20 | * Log the command to the Cypress command log 21 | * 22 | * @yields {string|string[]} 23 | * @since 0.2.0 24 | */ 25 | Cypress.Commands.add( 26 | 'attribute', 27 | { prevSubject: 'element' }, 28 | (subject, attribute, options = {}) => { 29 | subject = $(subject); 30 | 31 | // Make sure the order of input can be flipped 32 | if (_.isObject(attribute)) { 33 | [attribute, options] = [options, attribute]; 34 | } 35 | 36 | // Handle options 37 | validator.check('log', options.log, [true, false]); 38 | validator.check('whitespace', options.whitespace, ['simplify', 'keep', 'keep-newline']); 39 | validator.check('strict', options.strict, [true, false]); 40 | _.defaults(options, { 41 | log: true, 42 | strict: true, 43 | whitespace: 'keep', 44 | }); 45 | 46 | options._whitespace = whitespace(options.whitespace); 47 | 48 | const consoleProps = { 49 | 'Applied to': subject, 50 | }; 51 | if (options.log) { 52 | options._log = Cypress.log({ 53 | $el: subject, 54 | name: 'attribute', 55 | message: attribute, 56 | consoleProps: () => consoleProps, 57 | }); 58 | } 59 | 60 | // Mark this newly invoked command in the command queue to be able to find it later. 61 | markCurrentCommand('attribute'); 62 | 63 | /** 64 | * @param {Array.|string} result 65 | */ 66 | function updateLog(result) { 67 | consoleProps.Yielded = result; 68 | } 69 | 70 | /** 71 | * Get the attribute and do the upcoming assertion 72 | * @return {Promise} 73 | */ 74 | function resolveAttribute() { 75 | const attr = subject 76 | .map((i, element) => { 77 | return $(element).attr(attribute); 78 | }) 79 | // Deconstruct jQuery object to normal array 80 | .toArray() 81 | .map(options._whitespace); 82 | 83 | let result = attr; 84 | 85 | if (options.strict && subject.length > result.length) { 86 | const negate = upcomingAssertionNegatesExistence(); 87 | if (!negate) { 88 | // Empty result to we fail on missing attributes 89 | result = []; 90 | } 91 | } 92 | 93 | if (result.length === 0) { 94 | // Empty JQuery object is Cypress' way of saying that something does not exist 95 | result = $(); 96 | } else if (result.length === 1) { 97 | // Only one result, so unwrap the array 98 | result = result[0]; 99 | } 100 | 101 | if (options.log) { 102 | updateLog(result); 103 | } 104 | 105 | return cy.verifyUpcomingAssertions(result, options, { 106 | onFail: (err) => onFail(err, subject, attribute, attr), 107 | // Retry untill the upcoming assertion passes 108 | onRetry: resolveAttribute, 109 | }); 110 | } 111 | 112 | return resolveAttribute().then((attribute) => { 113 | // The upcoming assertion passed, finish up the log 114 | if (options.log) { 115 | options._log.snapshot().end(); 116 | } 117 | return attribute; 118 | }); 119 | } 120 | ); 121 | 122 | /** 123 | * Overwrite the error message of implicit assertions 124 | * @param {AssertionError} err 125 | * @param {jQuery} subject 126 | * @param {string} attribute 127 | * @param {string | string[]} result 128 | */ 129 | function onFail(err, subject, attribute, result) { 130 | const negate = err.message.includes(' not '); 131 | result = _.isArray(result) ? result : [result]; 132 | 133 | if (err.type === 'existence' && subject.length == 1) { 134 | const errorMessage = errMsg.existence.single; 135 | 136 | if (negate) { 137 | err.displayMessage = errorMessage.negated(attribute); 138 | } else { 139 | err.displayMessage = errorMessage.normal(attribute); 140 | } 141 | } else if (err.type === 'existence' && subject.length > 1) { 142 | const errorMessage = errMsg.existence.multiple; 143 | 144 | if (negate) { 145 | err.displayMessage = errorMessage.negated(attribute, subject.length, result.length); 146 | } else { 147 | err.displayMessage = errorMessage.normal(attribute, subject.length, result.length); 148 | } 149 | 150 | err.displayMessage += '\n\n' + errMsg.disable_strict; 151 | } 152 | 153 | err.message = err.displayMessage; 154 | } 155 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './attribute'; 2 | import './then'; 3 | import './text'; 4 | import './to'; 5 | import './request'; 6 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | import path from 'path-browserify'; 2 | const _ = Cypress._; 3 | 4 | const methods = [ 5 | 'GET', 6 | 'POST', 7 | 'PUT', 8 | 'DELETE', 9 | 'PATCH', 10 | 'HEAD', 11 | 'OPTIONS', 12 | 'TRACE', 13 | 'COPY', 14 | 'LOCK', 15 | 'MKCOL', 16 | 'MOVE', 17 | 'PURGE', 18 | 'PROPFIND', 19 | 'PROPPATCH', 20 | 'UNLOCK', 21 | 'REPORT', 22 | 'MKACTIVITY', 23 | 'CHECKOUT', 24 | 'MERGE', 25 | 'M-SEARCH', 26 | 'NOTIFY', 27 | 'SUBSCRIBE', 28 | 'UNSUBSCRIBE', 29 | 'SEARCH', 30 | 'CONNECT', 31 | ]; 32 | 33 | /** 34 | * @yields {any} 35 | * @since 0.2.0 36 | */ 37 | Cypress.Commands.overwrite('request', (originalCommand, ...args) => { 38 | const options = {}; 39 | 40 | if (_.isObject(args[0])) { 41 | _.extend(options, args[0]); 42 | } else if (args.length === 1) { 43 | options.url = args[0]; 44 | } else if (args.length === 2) { 45 | if (methods.includes(args[0].toUpperCase())) { 46 | options.method = args[0]; 47 | options.url = args[1]; 48 | } else { 49 | options.url = args[0]; 50 | options.body = args[1]; 51 | } 52 | } else if (args.length === 3) { 53 | options.method = args[0]; 54 | options.url = args[1]; 55 | options.body = args[2]; 56 | } 57 | 58 | options.url = parseUrl(options.url); 59 | 60 | return originalCommand(options); 61 | }); 62 | 63 | /** 64 | * @param {string} url 65 | * @return {string} 66 | */ 67 | function parseUrl(url) { 68 | if ( 69 | typeof url === 'string' && 70 | !url.includes('://') && 71 | !url.startsWith('localhost') && 72 | !url.startsWith('www.') 73 | ) { 74 | // It's a relative url 75 | const config = Cypress.config(); 76 | const requestBaseUrl = config.requestBaseUrl; 77 | 78 | if (_.isString(requestBaseUrl) && requestBaseUrl.length > 0) { 79 | const split = requestBaseUrl.split('://'); 80 | const protocol = split[0] + '://'; 81 | const baseUrl = split[1]; 82 | 83 | url = protocol + path.join(baseUrl, url); 84 | } 85 | } 86 | return url; 87 | } 88 | -------------------------------------------------------------------------------- /src/text.js: -------------------------------------------------------------------------------- 1 | const _ = Cypress._; 2 | const $ = Cypress.$; 3 | 4 | import whitespace from './utils/whitespace'; 5 | import OptionValidator from './utils/optionValidator'; 6 | const validator = new OptionValidator('text'); 7 | 8 | /** 9 | * Get the text contents of the subject 10 | * 11 | * @example 12 | * cy.get('footer').text(); 13 | * 14 | * @param {Object} [options] 15 | * @param {boolean} [options.log=true] 16 | * Log the command to the Cypress command log 17 | * @param {'simplify'|'keep-newline'|'keep'} [options.whitespace='simplify'] 18 | * Replace complex whitespace (` `, `\t`, `\n`, multiple spaces and more 19 | * obscure whitespace characters) with a single regular space. 20 | * @param {number} [options.depth=0] 21 | * Include the text contents of child elements up to a depth of `n` 22 | * 23 | * @yields {string|string[]} 24 | * @since 0.1.0 25 | */ 26 | Cypress.Commands.add('text', { prevSubject: 'element' }, (element, options = {}) => { 27 | validator.check('log', options.log, [true, false]); 28 | validator.check('whitespace', options.whitespace, ['simplify', 'keep', 'keep-newline']); 29 | validator.check('depth', options.depth, '>= 0'); 30 | 31 | _.defaults(options, { 32 | log: true, 33 | whitespace: 'simplify', 34 | depth: 0, 35 | }); 36 | 37 | options._whitespace = whitespace(options.whitespace); 38 | 39 | const consoleProps = { 40 | 'Applied to': $(element), 41 | 'Whitespace': options.whitespace, 42 | 'Depth': options.depth, 43 | }; 44 | if (options.log) { 45 | options._log = Cypress.log({ 46 | $el: $(element), 47 | name: 'text', 48 | message: '', 49 | consoleProps: () => consoleProps, 50 | }); 51 | } 52 | 53 | /** 54 | * @param {Array.|string} result 55 | */ 56 | function updateLog(result) { 57 | consoleProps.Yielded = result; 58 | if (_.isArray(result)) { 59 | options._log.set('message', JSON.stringify(result)); 60 | } else { 61 | options._log.set('message', result); 62 | } 63 | } 64 | 65 | /** 66 | * Get the text and do the upcoming assertion 67 | * @return {Promise} 68 | */ 69 | function resolveText() { 70 | let text = []; 71 | element.each((_, elem) => { 72 | text.push(getTextOfElement($(elem), options.depth).trim()); 73 | }); 74 | 75 | text = text.map(options._whitespace); 76 | 77 | if (text.length == 1) { 78 | text = text[0]; 79 | } 80 | 81 | if (options.log) updateLog(text); 82 | 83 | return cy.verifyUpcomingAssertions(text, options, { 84 | // Retry untill the upcoming assertion passes 85 | onRetry: resolveText, 86 | }); 87 | } 88 | 89 | return resolveText().then((text) => { 90 | // The upcoming assertion passed, finish up the log 91 | if (options.log) { 92 | options._log.snapshot().end(); 93 | } 94 | return text; 95 | }); 96 | }); 97 | 98 | /** 99 | * @param {JQuery} element 100 | * @param {number} depth 101 | * @return {string} 102 | */ 103 | function getTextOfElement(element, depth) { 104 | const TAG_REPLACEMENT = { 105 | WBR: '\u200B', 106 | BR: ' ', 107 | }; 108 | 109 | let text = ''; 110 | element.contents().each((i, content) => { 111 | if (content.nodeType === Node.TEXT_NODE) { 112 | return (text += content.data); 113 | } 114 | if (content.nodeType === Node.ELEMENT_NODE) { 115 | if (_.has(TAG_REPLACEMENT, content.nodeName)) { 116 | return (text += TAG_REPLACEMENT[content.nodeName]); 117 | } 118 | 119 | if (depth > 0) { 120 | return (text += getTextOfElement($(content), depth - 1)); 121 | } 122 | } 123 | }); 124 | 125 | return text; 126 | } 127 | -------------------------------------------------------------------------------- /src/then.js: -------------------------------------------------------------------------------- 1 | const _ = Cypress._; 2 | const $ = Cypress.$; 3 | 4 | import isJquery from './utils/isJquery'; 5 | import OptionValidator from './utils/optionValidator'; 6 | const validator = new OptionValidator('then'); 7 | 8 | /** 9 | * Enables you to work with the subject yielded from the previous command. 10 | * 11 | * @example 12 | * cy.then((subject) => { 13 | * // ... 14 | * }); 15 | * 16 | * @param {function} fn 17 | * @param {Object} options 18 | * @param {boolean} [options.log=false] 19 | * Log to Cypress bar 20 | * @param {boolean} [options.retry=false] 21 | * Retry when an upcomming assertion fails 22 | * 23 | * @yields {any} 24 | * @since 0.0.0 25 | */ 26 | Cypress.Commands.overwrite('then', (originalCommand, subject, fn, options = {}) => { 27 | if (_.isFunction(options)) { 28 | // Flip the values of `fn` and `options` 29 | [fn, options] = [options, fn]; 30 | } 31 | 32 | validator.check('log', options.log, [true, false]); 33 | validator.check('retry', options.retry, [true, false]); 34 | 35 | if (options.retry && typeof options.log === 'undefined') { 36 | options.log = true; 37 | } 38 | 39 | _.defaults(options, { 40 | log: false, 41 | retry: false, 42 | }); 43 | 44 | // Setup logging 45 | const consoleProps = {}; 46 | if (options.log) { 47 | options._log = Cypress.log({ 48 | name: 'then', 49 | message: '', 50 | consoleProps: () => consoleProps, 51 | }); 52 | 53 | if (isJquery(subject)) { 54 | // Link the DOM element to the logger 55 | options._log.set('$el', $(subject)); 56 | consoleProps['Applied to'] = $(subject); 57 | } else { 58 | consoleProps['Applied to'] = String(subject); 59 | } 60 | 61 | if (options.retry) { 62 | options._log.set('message', 'retry'); 63 | } 64 | } 65 | 66 | /** 67 | * This function is recursively called untill timeout or the upcomming 68 | * assertion passes. Keep this function as fast as possible. 69 | * 70 | * @return {Promise} 71 | */ 72 | async function executeFnAndRetry() { 73 | const result = await executeFn(); 74 | 75 | return cy.verifyUpcomingAssertions(result, options, { 76 | // Try again by calling itself 77 | onRetry: executeFnAndRetry, 78 | }); 79 | } 80 | 81 | /** 82 | * Execute the provided callback function 83 | * 84 | * @return {*} 85 | */ 86 | async function executeFn() { 87 | // Execute using the original `then` to not reinvent the wheel 88 | return await originalCommand(subject, options, fn).then((value) => { 89 | if (options.log) { 90 | consoleProps.Yielded = value; 91 | } 92 | return value; 93 | }); 94 | } 95 | 96 | if (options.retry) { 97 | return executeFnAndRetry(); 98 | } 99 | return executeFn(); 100 | }); 101 | -------------------------------------------------------------------------------- /src/to.js: -------------------------------------------------------------------------------- 1 | const _ = Cypress._; 2 | 3 | import { command } from './utils/errorMessages'; 4 | const errMsg = command.to; 5 | 6 | import OptionValidator from './utils/optionValidator'; 7 | const validator = new OptionValidator('to'); 8 | 9 | const types = { 10 | array: castArray, 11 | string: castString, 12 | number: castNumber, 13 | }; 14 | 15 | /** 16 | * @param {string} type Target type 17 | * @param {Object} [options] 18 | * @param {boolean} [options.log=true] 19 | * Log the command to the Cypress command log 20 | * 21 | * @yields {any} 22 | * @since 0.3.0 23 | */ 24 | Cypress.Commands.add('to', { prevSubject: true }, (subject, type, options = {}) => { 25 | validator.check('log', options.log, [true, false]); 26 | 27 | _.defaults(options, { 28 | log: true, 29 | }); 30 | 31 | if (_.isUndefined(subject)) { 32 | throw new Error(errMsg.cantCastType('undefined')); 33 | } 34 | if (_.isNull(subject)) { 35 | throw new Error(errMsg.cantCastType('null')); 36 | } 37 | if (_.isNaN(subject)) { 38 | throw new Error(errMsg.cantCastType('NaN')); 39 | } 40 | 41 | if (!_.keys(types).includes(type)) { 42 | // We don't know the given type, so we can't cast to it 43 | throw new Error(`${errMsg.cantCast('subject', type)} ${errMsg.expected(_.keys(types))}`); 44 | } 45 | 46 | const consoleProps = { 47 | 'Applied to': subject, 48 | }; 49 | if (options.log) { 50 | options._log = Cypress.log({ 51 | name: 'to', 52 | message: type, 53 | consoleProps: () => consoleProps, 54 | }); 55 | } 56 | 57 | /** 58 | * Cast the subject and do the upcoming assertion 59 | * @return {Promise} 60 | */ 61 | function castSubject() { 62 | try { 63 | return cy.verifyUpcomingAssertions(types[type](subject), options, { 64 | // Retry untill the upcoming assertion passes 65 | onRetry: castSubject, 66 | }); 67 | } catch (err) { 68 | // The casting function threw an error, let's try again 69 | options.error = err; 70 | return cy.retry(castSubject, options, options._log); 71 | } 72 | } 73 | 74 | return castSubject().then((result) => { 75 | // Everything passed, finish up the log 76 | consoleProps.Yielded = result; 77 | return result; 78 | }); 79 | }); 80 | 81 | /** 82 | * @param {any|any[]} subject 83 | * @return {any[]} 84 | */ 85 | function castArray(subject) { 86 | if (_.isArrayLikeObject(subject)) { 87 | return subject; 88 | } 89 | return [subject]; 90 | } 91 | 92 | /** 93 | * @param {any|any[]} subject 94 | * @return {string} 95 | */ 96 | function castString(subject) { 97 | if (_.isArrayLikeObject(subject)) { 98 | return subject.map(castString); 99 | } 100 | if (_.isObject(subject)) { 101 | return JSON.stringify(subject); 102 | } 103 | return `${subject}`; 104 | } 105 | 106 | /** 107 | * @param {any|any[]} subject 108 | * @return {number|number[]} 109 | */ 110 | function castNumber(subject) { 111 | if (_.isArrayLikeObject(subject)) { 112 | return subject.map(castNumber); 113 | } else if (_.isObject(subject)) { 114 | throw new Error(errMsg.cantCastType('object', 'number')); 115 | } 116 | 117 | const casted = Number(subject); 118 | if (isNaN(casted)) { 119 | throw new Error(errMsg.cantCastVal(subject, 'number')); 120 | } 121 | return casted; 122 | } 123 | -------------------------------------------------------------------------------- /src/utils/commandError.js: -------------------------------------------------------------------------------- 1 | const _ = Cypress._; 2 | 3 | /** 4 | * Error namespace for command related issues 5 | */ 6 | export default class CommandError extends Error { 7 | /** 8 | * @param {string} message The error message 9 | */ 10 | constructor(message) { 11 | if (_.isArray(message)) { 12 | message = message.join(''); 13 | } 14 | 15 | super(message); 16 | 17 | this.name = 'CommandError'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/commandQueue.js: -------------------------------------------------------------------------------- 1 | import { notInProduction } from './errorMessages'; 2 | 3 | /** 4 | * By marking the current command we can retrieve it later in any 5 | * context, including retried context. 6 | * @param {string} commandName 7 | */ 8 | export function markCurrentCommand(commandName) { 9 | const queue = Cypress.cy.queue; 10 | const currentCommand = queue 11 | .get() 12 | .filter((command) => { 13 | return command.get('name') === commandName && !command.get('invoked'); 14 | }) 15 | .shift(); 16 | 17 | // The mark 18 | currentCommand.attributes.invoked = true; 19 | } 20 | 21 | /** 22 | * Find out of the last marked command in the command queue has upcoming 23 | * assertions that negate existence. 24 | * @return {boolean} 25 | */ 26 | export function upcomingAssertionNegatesExistence() { 27 | const currentCommand = getLastMarkedCommand(); 28 | if (!currentCommand) { 29 | return false; 30 | } 31 | 32 | const upcomingAssertions = getUpcomingAssertions(currentCommand); 33 | 34 | return upcomingAssertions.some((c) => { 35 | let args = c.get('args'); 36 | if (typeof args[0] === 'string') { 37 | args = args[0].split('.'); 38 | return args.includes('exist') && args.includes('not'); 39 | } 40 | return false; 41 | }); 42 | } 43 | 44 | /** 45 | * @return {Command|false} 46 | */ 47 | function getLastMarkedCommand() { 48 | const queue = Cypress.cy.queue; 49 | const cmd = queue 50 | .get() 51 | .filter((command) => command.get('invoked')) 52 | .pop(); 53 | 54 | if (cmd === undefined) { 55 | console.error( 56 | 'Could not find any marked commands in the queue. ' + 57 | 'Did you forget to mark the command during its invokation?' + 58 | `\n\n${notInProduction}` 59 | ); 60 | 61 | return false; 62 | } 63 | 64 | return cmd; 65 | } 66 | 67 | /** 68 | * Recursively find all direct upcoming assertions 69 | * @param {Command} command 70 | * @return {Command[]} 71 | */ 72 | function getUpcomingAssertions(command) { 73 | const next = command.get('next'); 74 | let ret = []; 75 | if (next && next.get('type') === 'assertion') { 76 | ret = [next, ...getUpcomingAssertions(next)]; 77 | } 78 | return ret; 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/errorMessages.js: -------------------------------------------------------------------------------- 1 | import { repository } from '../../package.json'; 2 | 3 | export const notInProduction = 4 | 'This message should never show ' + 5 | "if you're a user of cypress-commands. If it does, please open " + 6 | `an issue at ${repository.url}.`; 7 | 8 | export const command = { 9 | attribute: { 10 | disable_strict: 11 | 'This behaviour can be disabled by calling ' + 12 | "'.attribute()' with the option 'strict: false'.", 13 | existence: { 14 | single: { 15 | negated: (attribute) => { 16 | return ( 17 | 'Expected element to not have attribute ' + 18 | `'${attribute}', but it was continuously found.` 19 | ); 20 | }, 21 | normal: (attribute) => { 22 | return ( 23 | 'Expected element to have attribute ' + 24 | `'${attribute}', but never found it.` 25 | ); 26 | }, 27 | }, 28 | multiple: { 29 | negated: (attribute, inputCount, outputCount) => { 30 | return ( 31 | `Expected all ${inputCount} elements to not have ` + 32 | `attribute '${attribute}', but it was continuously found on ` + 33 | `${outputCount} elements.` 34 | ); 35 | }, 36 | normal: (attribute, inputCount, outputCount) => { 37 | return ( 38 | `Expected all ${inputCount} elements to have ` + 39 | `attribute '${attribute}', but never found it on ` + 40 | `${inputCount - outputCount} elements.` 41 | ); 42 | }, 43 | }, 44 | }, 45 | }, 46 | to: { 47 | cantCast: (description, target) => { 48 | let message = `Can't cast ${description}`; 49 | if (target) { 50 | message += ` to type ${target}`; 51 | } 52 | return `${message}.`; 53 | }, 54 | cantCastType: (type, target) => { 55 | return command.to.cantCast(`subject of type ${type}`, target); 56 | }, 57 | cantCastVal: (val, target) => { 58 | return command.to.cantCast(`'${val}'`, target); 59 | }, 60 | expected: (expected) => { 61 | return `Expected one of [${expected.join(', ')}]`; 62 | }, 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/utils/isJquery.js: -------------------------------------------------------------------------------- 1 | const _ = Cypress._; 2 | 3 | /** 4 | * Find out if a given value is a jQuery object 5 | * @param {*} value 6 | * @return {boolean} 7 | */ 8 | export default function isJquery(value) { 9 | if (_.isUndefined(value) || _.isNull(value)) { 10 | return false; 11 | } 12 | return !!value.jquery; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/optionValidator.js: -------------------------------------------------------------------------------- 1 | const _ = Cypress._; 2 | 3 | import CommandError from './commandError'; 4 | import { repository } from '../../package.json'; 5 | 6 | /** 7 | * Validate user set options 8 | */ 9 | export default class OptionValidator { 10 | /** 11 | * @param {string} commandName 12 | */ 13 | constructor(commandName) { 14 | /** 15 | * @type {string} 16 | */ 17 | this.command = commandName; 18 | 19 | /** 20 | * Url to the full documentation of the command 21 | * @type {string} 22 | */ 23 | this.docUrl = `${repository.url}/blob/master/docs/${commandName}.md`; 24 | } 25 | 26 | /** 27 | * Validate a user set option 28 | * @param {string} option 29 | * @param {*} actual 30 | * @param {string[]|string} expected 31 | * @throws {CommandError} 32 | */ 33 | check(option, actual, expected) { 34 | if (_.isUndefined(actual)) { 35 | // The option is not set. This is fine. 36 | return; 37 | } 38 | 39 | const errMessage = { 40 | start: `Bad value for the option "${option}" of the command "${this.command}".\n\n`, 41 | received: `Command received the value "${actual}" but `, 42 | end: `\n\nFor details refer to the documentation at ${this.docUrl}`, 43 | }; 44 | 45 | if (_.isArray(expected)) { 46 | if (!expected.includes(actual)) { 47 | throw new CommandError([ 48 | errMessage.start, 49 | errMessage.received, 50 | `expected one of ${JSON.stringify(expected)}`, 51 | errMessage.end, 52 | ]); 53 | } 54 | } else if (_.isString(expected)) { 55 | if (!eval(`'${actual}' ${expected}`)) { 56 | throw new CommandError([ 57 | errMessage.start, 58 | errMessage.received, 59 | `expected a value ${expected}`, 60 | errMessage.end, 61 | ]); 62 | } 63 | } else { 64 | throw new CommandError( 65 | 'Not sure how to validate ' + 66 | `the option "${option}" of the command "${this.command}".\n\n` + 67 | 'If you see this message in the wild, please create an issue ' + 68 | `so this error can be resolved.\n${repoUrl}` 69 | ); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/whitespace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {'simplify'|'keep-newline'|'keep'} mode 3 | * @return {function} 4 | */ 5 | export default function whitespace(mode) { 6 | const zeroWidthWhitespace = /[\u200B-\u200D\uFEFF]/g; 7 | 8 | if (mode === 'simplify') { 9 | return (input) => { 10 | return input.replace(zeroWidthWhitespace, '').replace(/\s+/g, ' ').trim(); 11 | }; 12 | } 13 | 14 | if (mode === 'keep-newline') { 15 | return (input) => { 16 | return input 17 | .replace(zeroWidthWhitespace, '') 18 | .replace(/[^\S\n]+/g, ' ') 19 | .replace(/^[^\S\n]/g, '') 20 | .replace(/[^\S\n]$/g, '') 21 | .replace(/[^\S\n]*\n[^\S\n]*/g, '\n'); 22 | }; 23 | } 24 | 25 | return (input) => input; 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "dom"], 4 | "allowJs": true, 5 | "types": ["./types", "cypress"], 6 | "outDir": "./dist" 7 | }, 8 | "include": ["cypress/**/*.js", "src/**/*.js"] 9 | } 10 | -------------------------------------------------------------------------------- /types/attribute.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | /** 6 | * Get the value of an attribute of a DOM element. 7 | * 8 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/attribute.md 9 | */ 10 | attribute( 11 | attribute: string, 12 | options?: Partial 13 | ): Chainable; 14 | 15 | /** 16 | * Get the value of an attribute of a DOM element. 17 | * 18 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/attribute.md 19 | */ 20 | attribute( 21 | options: Partial, 22 | attribute: string 23 | ): Chainable; 24 | } 25 | 26 | interface AttributeOptions extends Loggable, WhitespaceOptions { 27 | /** 28 | * If true, implicitly assert that all subjects have the requested attribute. 29 | * 30 | * @default true 31 | */ 32 | strict: boolean; 33 | 34 | /** 35 | * @default 'keep' 36 | */ 37 | whitespace: WhitespaceOptions['whitespace']; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /types/generic.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface WhitespaceOptions { 5 | /** 6 | * How to handle whitespace in the string. 7 | * 8 | * - 'simplify': Replace all whitespace with a single space. 9 | * - 'keep-newline': Replace all whitespace except for newline characters (`\n`) with a 10 | * single space. 11 | * - 'keep': Don't replace any whitespace. 12 | */ 13 | whitespace: 'simplify' | 'keep-newline' | 'keep'; 14 | } 15 | 16 | /** 17 | * Options that controls if the command can will be retried when it fails. 18 | * 19 | * A command should only be retryable when the command does not retry by default. 20 | */ 21 | interface Retryable { 22 | /** 23 | * Retry the command when upcoming assertions fail. 24 | * 25 | * @default false 26 | */ 27 | retry: boolean; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import './generic'; 2 | import './attribute'; 3 | import './then'; 4 | import './text'; 5 | import './to'; 6 | 7 | // TODO: Update `request()` docs 8 | // TODO: Update `then()` docs 9 | -------------------------------------------------------------------------------- /types/text.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | /** 6 | * Get the text contents of a DOM element. 7 | * 8 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/text.md 9 | */ 10 | text(options?: Partial): Chainable; 11 | } 12 | 13 | interface TextOptions extends Loggable, WhitespaceOptions { 14 | /** 15 | * Include the text contents of child elements upto `n` levels. 16 | * 17 | * @default 0 18 | */ 19 | depth: number; 20 | 21 | /** 22 | * @default 'simplify' 23 | */ 24 | whitespace: WhitespaceOptions['whitespace']; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /types/then.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | /** 6 | * Enables you to work with the subject yielded from the previous command. 7 | * 8 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/then.md 9 | */ 10 | then( 11 | options: Partial, 12 | fn: (this: ObjectLike, currentSubject: Subject) => Chainable 13 | ): Chainable; 14 | /** 15 | * Enables you to work with the subject yielded from the previous command / promise. 16 | * 17 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/then.md 18 | */ 19 | then( 20 | options: Partial, 21 | fn: (this: ObjectLike, currentSubject: Subject) => PromiseLike 22 | ): Chainable; 23 | /** 24 | * Enables you to work with the subject yielded from the previous command / promise. 25 | * 26 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/then.md 27 | */ 28 | then( 29 | options: Partial, 30 | fn: (this: ObjectLike, currentSubject: Subject) => S 31 | ): Chainable; 32 | /** 33 | * Enables you to work with the subject yielded from the previous command. 34 | * 35 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/then.md 36 | * @example 37 | * cy.get('.nav').then(($nav) => {}) // Yields .nav as first arg 38 | * cy.location().then((loc) => {}) // Yields location object as first arg 39 | */ 40 | then( 41 | options: Partial, 42 | fn: (this: ObjectLike, currentSubject: Subject) => void 43 | ): Chainable; 44 | 45 | /** 46 | * Enables you to work with the subject yielded from the previous command. 47 | * 48 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/then.md 49 | */ 50 | then( 51 | fn: (this: ObjectLike, currentSubject: Subject) => Chainable, 52 | options: Partial 53 | ): Chainable; 54 | /** 55 | * Enables you to work with the subject yielded from the previous command / promise. 56 | * 57 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/then.md 58 | */ 59 | then( 60 | fn: (this: ObjectLike, currentSubject: Subject) => PromiseLike, 61 | options: Partial 62 | ): Chainable; 63 | /** 64 | * Enables you to work with the subject yielded from the previous command / promise. 65 | * 66 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/then.md 67 | */ 68 | then( 69 | fn: (this: ObjectLike, currentSubject: Subject) => S, 70 | options: Partial 71 | ): Chainable; 72 | /** 73 | * Enables you to work with the subject yielded from the previous command. 74 | * 75 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/then.md 76 | * @example 77 | * cy.get('.nav').then(($nav) => {}) // Yields .nav as first arg 78 | * cy.location().then((loc) => {}) // Yields location object as first arg 79 | */ 80 | then( 81 | fn: (this: ObjectLike, currentSubject: Subject) => void, 82 | options: Partial 83 | ): Chainable; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /types/to.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | /** 6 | * Cast the subject to a given type 7 | * 8 | * - When the subject is an array it will cast all items in the array instead 9 | * - When the subject is already the given type it will do nothing 10 | * 11 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/to.md 12 | */ 13 | to(type: 'string' | 'number' | 'array', options?: Partial): Chainable; 14 | 15 | /** 16 | * Cast the subject to a given type 17 | * 18 | * - When the subject is an array it will cast all items in the array instead 19 | * - When the subject is already the given type it will do nothing 20 | * 21 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/to.md 22 | */ 23 | to( 24 | type: 'string', 25 | options?: Partial 26 | ): Chainable; 27 | 28 | /** 29 | * Cast the subject to a given type 30 | * 31 | * - When the subject is an array it will cast all items in the array instead 32 | * - When the subject is already the given type it will do nothing 33 | * 34 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/to.md 35 | */ 36 | to( 37 | type: 'number', 38 | options?: Partial 39 | ): Chainable; 40 | 41 | /** 42 | * Cast the subject to a given type 43 | * 44 | * - When the subject is an array it will cast all items in the array instead 45 | * - When the subject is already the given type it will do nothing 46 | * 47 | * @see https://github.com/Lakitna/cypress-commands/blob/master/docs/to.md 48 | */ 49 | to( 50 | type: 'array', 51 | options?: Partial 52 | ): Chainable; 53 | } 54 | } 55 | --------------------------------------------------------------------------------