├── .nvmrc ├── .node-version ├── .gitignore ├── .husky └── pre-commit ├── test-project ├── .gitignore ├── cypress │ ├── fixtures │ │ └── example.json │ ├── support │ │ ├── e2e.js │ │ └── commands.js │ └── e2e │ │ ├── 2-advanced-examples │ │ ├── window.cy.js │ │ ├── waiting.cy.js │ │ ├── location.cy.js │ │ ├── aliasing.cy.js │ │ ├── navigation.cy.js │ │ ├── viewport.cy.js │ │ ├── files.cy.js │ │ ├── misc.cy.js │ │ ├── connectors.cy.js │ │ ├── utilities.cy.js │ │ ├── querying.cy.js │ │ ├── traversal.cy.js │ │ ├── cookies.cy.js │ │ ├── storage.cy.js │ │ ├── cypress_api.cy.js │ │ ├── assertions.cy.js │ │ ├── spies_stubs_clocks.cy.js │ │ ├── network_requests.cy.js │ │ └── actions.cy.js │ │ └── 1-getting-started │ │ └── todo.cy.js ├── package.json ├── cypress.config.js ├── eslint-configs │ ├── eslint.globals.mjs │ ├── eslint.one-rule.mjs │ ├── eslint.recommended.mjs │ └── eslint.one-rule-deprecated.mjs ├── eslint.config.mjs └── README.md ├── vitest.config.mts ├── .prettierignore ├── .prettierrc.json ├── FLAT-CONFIG.md ├── .github └── workflows │ ├── triage_closed_issue_comment.yml │ └── add-issue-triage-board.yml ├── lib ├── flat.d.ts ├── config │ └── recommended.js ├── rules │ ├── no-xpath.js │ ├── no-async-tests.js │ ├── no-async-before.js │ ├── no-assigning-return-values.js │ ├── no-debug.js │ ├── no-pause.js │ ├── no-chained-get.js │ ├── require-data-selectors.js │ ├── no-force.js │ ├── no-unnecessary-waiting.js │ ├── assertion-before-screenshot.js │ └── unsafe-to-chain-command.js └── flat.js ├── docs └── rules │ ├── no-unnecessary-waiting.md │ ├── no-assigning-return-values.md │ ├── no-chained-get.md │ ├── no-debug.md │ ├── no-pause.md │ ├── no-xpath.md │ ├── unsafe-to-chain-command.md │ ├── assertion-before-screenshot.md │ ├── require-data-selectors.md │ ├── no-async-before.md │ ├── no-async-tests.md │ └── no-force.md ├── ESLINTRC-CONFIG.md ├── tests ├── lib │ └── rules │ │ ├── no-debug.js │ │ ├── no-pause.js │ │ ├── no-xpath.js │ │ ├── no-chained-get.js │ │ ├── no-async-tests.js │ │ ├── no-async-before.js │ │ ├── no-assigning-return-values.js │ │ ├── unsafe-to-chain-command.js │ │ ├── require-data-selectors.js │ │ ├── assertion-before-screenshot.js │ │ ├── no-force.js │ │ └── no-unnecessary-waiting.js └── config.js ├── LICENSE ├── package.json ├── eslint.config.mjs ├── circle.yml ├── CONTRIBUTING.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 24.11.0 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 24.11.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | .idea/* -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | npm run format:check 3 | -------------------------------------------------------------------------------- /test-project/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | 3 | cypress/downloads/ 4 | cypress/screenshots/ 5 | cypress/videos/ 6 | 7 | cypress/fixtures/profile.json 8 | cypress/fixtures/users.json 9 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['**/tests/**/*.[jt]s?(x)'], 6 | globals: true 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | **/package-lock.json 4 | 5 | **/*.js 6 | **/*.?js 7 | **/*.ts 8 | **/*.?ts 9 | **/*.md 10 | 11 | test-project/cypress/ 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "trailingComma": "all", 7 | "arrowParens": "always", 8 | "singleQuote": true 9 | } 10 | -------------------------------------------------------------------------------- /test-project/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /test-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-project", 3 | "version": "1.0.0", 4 | "description": "Test project for eslint-plugin-cypress", 5 | "scripts": { 6 | "lint": "eslint", 7 | "test": "cypress run" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test-project/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress"); 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) { 6 | // implement node event listeners here 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /FLAT-CONFIG.md: -------------------------------------------------------------------------------- 1 | # Cypress ESLint Plugin - Flat Config 2 | 3 | Please refer to the [README](./README.md) document which describes how to use `eslint-plugin-cypress` with an ESLint `v9` (default) [flat configuration](https://eslint.org/docs/latest/use/configure/configuration-files). 4 | -------------------------------------------------------------------------------- /.github/workflows/triage_closed_issue_comment.yml: -------------------------------------------------------------------------------- 1 | name: 'Handle Comment Workflow' 2 | on: 3 | issue_comment: 4 | types: 5 | - created 6 | jobs: 7 | closed-issue-comment: 8 | uses: cypress-io/cypress/.github/workflows/triage_handle_new_comments.yml@develop 9 | secrets: inherit 10 | -------------------------------------------------------------------------------- /lib/flat.d.ts: -------------------------------------------------------------------------------- 1 | import type { ESLint, Linter } from 'eslint' 2 | 3 | declare const plugin: { 4 | meta: { name: string, version: string } 5 | configs: { 6 | globals: Linter.Config 7 | recommended: Linter.Config 8 | } 9 | rules: NonNullable 10 | } 11 | 12 | export = plugin 13 | -------------------------------------------------------------------------------- /test-project/eslint-configs/eslint.globals.mjs: -------------------------------------------------------------------------------- 1 | // Only config.globals are activated 2 | import { defineConfig } from 'eslint/config' 3 | import pluginCypress from 'eslint-plugin-cypress' 4 | export default defineConfig([ 5 | { 6 | files: ['cypress/**/*.js'], 7 | extends: [ 8 | pluginCypress.configs.globals, 9 | ], 10 | }, 11 | ]) 12 | -------------------------------------------------------------------------------- /.github/workflows/add-issue-triage-board.yml: -------------------------------------------------------------------------------- 1 | name: 'Add issue/PR to Triage Board' 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | pull_request_target: 7 | types: 8 | - opened 9 | jobs: 10 | add-to-triage-project-board: 11 | uses: cypress-io/cypress/.github/workflows/triage_add_to_project.yml@develop 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /docs/rules/no-unnecessary-waiting.md: -------------------------------------------------------------------------------- 1 | # Disallow waiting for arbitrary time periods (`cypress/no-unnecessary-waiting`) 2 | 3 | 💼 This rule is enabled in the ✅ `recommended` config. 4 | 5 | 6 | 7 | ## Further Reading 8 | 9 | See [the Cypress Best Practices guide](https://on.cypress.io/best-practices#Unnecessary-Waiting). 10 | -------------------------------------------------------------------------------- /docs/rules/no-assigning-return-values.md: -------------------------------------------------------------------------------- 1 | # Disallow assigning return values of `cy` calls (`cypress/no-assigning-return-values`) 2 | 3 | 💼 This rule is enabled in the ✅ `recommended` config. 4 | 5 | 6 | 7 | ## Further Reading 8 | 9 | See [the Cypress Best Practices guide](https://on.cypress.io/best-practices#Assigning-Return-Values). 10 | -------------------------------------------------------------------------------- /lib/config/recommended.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | plugins: ['cypress'], 5 | env: { 6 | 'cypress/globals': true, 7 | }, 8 | rules: { 9 | 'cypress/no-assigning-return-values': 'error', 10 | 'cypress/no-unnecessary-waiting': 'error', 11 | 'cypress/no-async-tests': 'error', 12 | 'cypress/unsafe-to-chain-command': 'error', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /test-project/eslint-configs/eslint.one-rule.mjs: -------------------------------------------------------------------------------- 1 | // Plugin activated, only one rule applied 2 | import { defineConfig } from 'eslint/config' 3 | import pluginCypress from 'eslint-plugin-cypress' 4 | export default defineConfig([ 5 | { 6 | plugins: { 7 | cypress: pluginCypress, 8 | }, 9 | rules: { 10 | 'cypress/unsafe-to-chain-command': 'error', 11 | }, 12 | }, 13 | ]) 14 | -------------------------------------------------------------------------------- /test-project/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // All recommended rules are applied, except cypress/no-unnecessary-waiting 2 | import { defineConfig } from 'eslint/config' 3 | import pluginCypress from 'eslint-plugin-cypress' 4 | export default defineConfig([ 5 | { 6 | extends: [ 7 | pluginCypress.configs.recommended, 8 | ], 9 | rules: { 10 | 'cypress/no-unnecessary-waiting': 'off', 11 | }, 12 | }, 13 | ]) 14 | -------------------------------------------------------------------------------- /test-project/eslint-configs/eslint.recommended.mjs: -------------------------------------------------------------------------------- 1 | // All recommended rules are applied, except cypress/no-unnecessary-waiting 2 | import { defineConfig } from 'eslint/config' 3 | import pluginCypress from 'eslint-plugin-cypress' 4 | export default defineConfig([ 5 | { 6 | files: ['cypress/**/*.js'], 7 | extends: [ 8 | pluginCypress.configs.recommended, 9 | ], 10 | rules: { 11 | 'cypress/no-unnecessary-waiting': 'off', 12 | }, 13 | }, 14 | ]) 15 | -------------------------------------------------------------------------------- /test-project/eslint-configs/eslint.one-rule-deprecated.mjs: -------------------------------------------------------------------------------- 1 | // eslint-plugin-cypress/flat is deprecated and is identical to 2 | // eslint-plugin-cypress 3 | // Plugin activated, only one rule applied 4 | import { defineConfig } from 'eslint/config' 5 | import pluginCypress from 'eslint-plugin-cypress/flat' 6 | export default defineConfig([ 7 | { 8 | plugins: { 9 | cypress: pluginCypress, 10 | }, 11 | rules: { 12 | 'cypress/unsafe-to-chain-command': 'error', 13 | }, 14 | }, 15 | ]) 16 | -------------------------------------------------------------------------------- /docs/rules/no-chained-get.md: -------------------------------------------------------------------------------- 1 | # Disallow chain of `cy.get()` calls (`cypress/no-chained-get`) 2 | 3 | 4 | 5 | This rule disallows the usage of chained `.get()` calls as `cy.get()` always starts its search from the cy.root element. 6 | 7 | ## Rule Details 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```js 12 | cy.get('parent').get('child') 13 | ``` 14 | 15 | Examples of **correct** code for this rule: 16 | 17 | ```js 18 | cy.get('parent').find('child') 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/rules/no-debug.md: -------------------------------------------------------------------------------- 1 | # Disallow using `cy.debug()` calls (`cypress/no-debug`) 2 | 3 | 4 | 5 | It is recommended to remove any [cy.debug](https://on.cypress.io/debug) commands before committing specs to avoid other developers getting unexpected results. 6 | 7 | ## Rule Details 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```js 12 | cy.debug() 13 | cy.get('selector').debug() 14 | ``` 15 | 16 | Examples of **correct** code for this rule: 17 | 18 | ```js 19 | cy.get('selector') 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/rules/no-pause.md: -------------------------------------------------------------------------------- 1 | # Disallow using `cy.pause()` calls (`cypress/no-pause`) 2 | 3 | 4 | 5 | It is recommended to remove any [cy.pause](https://on.cypress.io/pause) commands before committing specs to avoid other developers getting unexpected results. 6 | 7 | ## Rule Details 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```js 12 | cy.pause() 13 | cy.get('selector').pause() 14 | ``` 15 | 16 | Examples of **correct** code for this rule: 17 | 18 | ```js 19 | cy.get('selector') 20 | ``` 21 | -------------------------------------------------------------------------------- /ESLINTRC-CONFIG.md: -------------------------------------------------------------------------------- 1 | # Cypress ESLint Plugin - Legacy Config 2 | 3 | This document previously described how to use the Cypress ESLint Plugin (`eslint-plugin-cypress`) with an [ESLint legacy config environment](https://eslint.org/docs/latest/use/configure/configuration-files-deprecated). 4 | 5 | This form of configuration was deprecated with the release of ESLint `v9` and its use with `eslint-plugin-cypress` is no longer supported. 6 | 7 | Users who were previously using a deprecated configuration environment should migrate to a [flat configuration](https://eslint.org/docs/latest/use/configure/configuration-files). 8 | -------------------------------------------------------------------------------- /test-project/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' -------------------------------------------------------------------------------- /tests/lib/rules/no-debug.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/no-debug') 4 | const RuleTester = require('eslint').RuleTester 5 | 6 | const ruleTester = new RuleTester() 7 | 8 | const errors = [{ messageId: 'unexpected' }] 9 | 10 | ruleTester.run('no-debug', rule, { 11 | 12 | valid: [ 13 | { code: 'debug()' }, 14 | { code: 'cy.get(\'button\').dblclick()' }, 15 | ], 16 | 17 | invalid: [ 18 | { code: 'cy.debug()', errors }, 19 | { code: 'cy.debug({ log: false })', errors }, 20 | { code: 'cy.get(\'button\').debug()', errors }, 21 | { code: 'cy.get(\'a\').should(\'have.attr\', \'href\').and(\'match\', /dashboard/).debug()', errors }, 22 | ], 23 | }) 24 | -------------------------------------------------------------------------------- /tests/lib/rules/no-pause.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/no-pause') 4 | const RuleTester = require('eslint').RuleTester 5 | 6 | const ruleTester = new RuleTester() 7 | 8 | const errors = [{ messageId: 'unexpected' }] 9 | 10 | ruleTester.run('no-pause', rule, { 11 | 12 | valid: [ 13 | { code: 'pause()' }, 14 | { code: 'cy.get(\'button\').dblclick()' }, 15 | ], 16 | 17 | invalid: [ 18 | { code: 'cy.pause()', errors }, 19 | { code: 'cy.pause({ log: false })', errors }, 20 | { code: 'cy.get(\'button\').pause()', errors }, 21 | { code: 'cy.get(\'a\').should(\'have.attr\', \'href\').and(\'match\', /dashboard/).pause()', errors }, 22 | ], 23 | }) 24 | -------------------------------------------------------------------------------- /docs/rules/no-xpath.md: -------------------------------------------------------------------------------- 1 | # Disallow using `cy.xpath()` calls (`cypress/no-xpath`) 2 | 3 | 4 | 5 | This rule disallows the usage of `cy.xpath()` for selecting elements. 6 | 7 | ## Rule Details 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```js 12 | cy.xpath('//div[@class=\"container\"]').click() 13 | ``` 14 | 15 | Examples of **correct** code for this rule: 16 | 17 | ```js 18 | cy.get('[data-cy="container"]').click() 19 | ``` 20 | 21 | ## Further Reading 22 | 23 | Both `@cypress/xpath` and `cypress-xpath` are deprecated. 24 | 25 | See [the Cypress Best Practices guide](https://docs.cypress.io/app/core-concepts/best-practices.html#Selecting-Elements). 26 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/window.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Window', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/window') 6 | }) 7 | 8 | it('cy.window() - get the global window object', () => { 9 | // https://on.cypress.io/window 10 | cy.window().should('have.property', 'top') 11 | }) 12 | 13 | it('cy.document() - get the document object', () => { 14 | // https://on.cypress.io/document 15 | cy.document().should('have.property', 'charset').and('eq', 'UTF-8') 16 | }) 17 | 18 | it('cy.title() - get the title', () => { 19 | // https://on.cypress.io/title 20 | cy.title().should('include', 'Kitchen Sink') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /docs/rules/unsafe-to-chain-command.md: -------------------------------------------------------------------------------- 1 | # Disallow actions within chains (`cypress/unsafe-to-chain-command`) 2 | 3 | 💼 This rule is enabled in the ✅ `recommended` config. 4 | 5 | 6 | 7 | ### Options 8 | 9 | 10 | 11 | | Name | Description | Type | Default | 12 | | :-------- | :---------------------------------------------------------- | :---- | :------ | 13 | | `methods` | An additional list of methods to check for unsafe chaining. | Array | `[]` | 14 | 15 | 16 | 17 | ## Further Reading 18 | 19 | See [retry-ability guide](https://docs.cypress.io/app/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle). 20 | -------------------------------------------------------------------------------- /tests/config.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect */ 2 | 'use strict' 3 | 4 | const globals = require('globals') 5 | const config = require('../lib/flat.js') 6 | 7 | describe('globals languageOptions', () => { 8 | const languageOptions = config.configs.globals.languageOptions 9 | 10 | it('should not mutate globals', () => { 11 | expect(globals.browser).not.toHaveProperty('cy') 12 | expect(globals.mocha).not.toHaveProperty('cy') 13 | }) 14 | 15 | it('should include other globals', () => { 16 | expect(languageOptions.globals).toEqual(expect.objectContaining(globals.browser)) 17 | expect(languageOptions.globals).toEqual(expect.objectContaining(globals.mocha)) 18 | }) 19 | 20 | it('should include cypress globals', () => { 21 | expect(languageOptions.globals).toEqual(expect.objectContaining({ 22 | cy: false, 23 | Cypress: false, 24 | })) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test-project/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -------------------------------------------------------------------------------- /docs/rules/assertion-before-screenshot.md: -------------------------------------------------------------------------------- 1 | # Require screenshots to be preceded by an assertion (`cypress/assertion-before-screenshot`) 2 | 3 | 4 | 5 | If you take screenshots without assertions then you may get different screenshots depending on timing. 6 | 7 | For example, if clicking a button makes some network calls and upon success, renders something, then the screenshot may sometimes have the new render and sometimes not. 8 | 9 | ## Rule Details 10 | 11 | This rule checks there is an assertion making sure your application state is correct before doing a screenshot. This makes sure the result of the screenshot will be consistent. 12 | 13 | Examples of **incorrect** code for this rule: 14 | 15 | ```js 16 | cy.visit('myUrl') 17 | cy.screenshot() 18 | ``` 19 | 20 | Examples of **correct** code for this rule: 21 | 22 | ```js 23 | cy.visit('myUrl') 24 | cy.get('[data-test-id="my-element"]').should('be.visible') 25 | cy.screenshot() 26 | ``` 27 | -------------------------------------------------------------------------------- /tests/lib/rules/no-xpath.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // ------------------------------------------------------------------------------ 4 | // Requirements 5 | // ------------------------------------------------------------------------------ 6 | 7 | const rule = require('../../../lib/rules/no-xpath'), 8 | RuleTester = require('eslint').RuleTester 9 | 10 | // ------------------------------------------------------------------------------ 11 | // Tests 12 | // ------------------------------------------------------------------------------ 13 | 14 | const ruleTester = new RuleTester() 15 | ruleTester.run('no-xpath', rule, { 16 | valid: [ 17 | { code: 'cy.get("button").click({force: true})' }, 18 | ], 19 | 20 | invalid: [ 21 | { 22 | code: 'cy.xpath(\'//div[@class="container"]/p[1]\').click()', 23 | errors: [{ messageId: 'unexpected' }], 24 | }, 25 | { 26 | code: 'cy.xpath(\'//p[1]\').should(\'exist\')', 27 | errors: [{ messageId: 'unexpected' }], 28 | }, 29 | ], 30 | }) 31 | -------------------------------------------------------------------------------- /tests/lib/rules/no-chained-get.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview disallow chain of `cy.get()` calls 3 | * @author benoit 4 | */ 5 | 'use strict' 6 | 7 | // ------------------------------------------------------------------------------ 8 | // Requirements 9 | // ------------------------------------------------------------------------------ 10 | 11 | const rule = require('../../../lib/rules/no-chained-get'), 12 | RuleTester = require('eslint').RuleTester 13 | 14 | // ------------------------------------------------------------------------------ 15 | // Tests 16 | // ------------------------------------------------------------------------------ 17 | 18 | const ruleTester = new RuleTester() 19 | ruleTester.run('no-chained-get', rule, { 20 | valid: [ 21 | { code: 'cy.get(\'div\')' }, 22 | { code: 'cy.get(\'.div\').find().get()' }, 23 | { code: 'cy.get(\'input\').should(\'be.disabled\')' }, 24 | ], 25 | invalid: [ 26 | { 27 | code: 'cy.get(\'div\').get(\'div\')', 28 | errors: [{ messageId: 'unexpected' }], 29 | }, 30 | ], 31 | }) 32 | -------------------------------------------------------------------------------- /tests/lib/rules/no-async-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/no-async-tests') 4 | const RuleTester = require('eslint').RuleTester 5 | 6 | const ruleTester = new RuleTester() 7 | 8 | const errors = [{ messageId: 'unexpected' }] 9 | 10 | ruleTester.run('no-async-tests', rule, { 11 | valid: [ 12 | { code: 'it(\'a test case\', () => { cy.get(\'.someClass\'); })' }, 13 | { code: 'it(\'a test case\', async () => { await somethingAsync(); })' }, 14 | { code: 'async function nonTestFn () { return await somethingAsync(); }' }, 15 | { code: 'const nonTestArrowFn = async () => { await somethingAsync(); }' }, 16 | ], 17 | invalid: [ 18 | { code: 'it(\'a test case\', async () => { cy.get(\'.someClass\'); })', errors }, 19 | { code: 'test(\'a test case\', async () => { cy.get(\'.someClass\'); })', errors }, 20 | { code: 'it(\'a test case\', async function () { cy.get(\'.someClass\'); })', errors }, 21 | { code: 'test(\'a test case\', async function () { cy.get(\'.someClass\'); })', errors }, 22 | ], 23 | }) 24 | -------------------------------------------------------------------------------- /lib/rules/no-xpath.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | meta: { 5 | type: 'suggestion', 6 | docs: { 7 | description: 'disallow using `cy.xpath()` calls', 8 | recommended: false, 9 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-xpath.md', 10 | }, 11 | fixable: null, // Or `code` or `whitespace` 12 | schema: [], // Add a schema if the rule has options 13 | messages: { 14 | unexpected: 'Avoid using cy.xpath command', 15 | }, 16 | }, 17 | 18 | create(context) { 19 | return { 20 | CallExpression(node) { 21 | if (isCallingCyXpath(node)) { 22 | context.report({ node, messageId: 'unexpected' }) 23 | } 24 | }, 25 | } 26 | }, 27 | } 28 | 29 | function isCallingCyXpath(node) { 30 | return node.callee.type === 'MemberExpression' 31 | && node.callee.object.type === 'Identifier' 32 | && node.callee.object.name === 'cy' 33 | && node.callee.property.type === 'Identifier' 34 | && node.callee.property.name === 'xpath' 35 | } 36 | -------------------------------------------------------------------------------- /tests/lib/rules/no-async-before.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/no-async-before') 4 | const RuleTester = require('eslint').RuleTester 5 | 6 | const ruleTester = new RuleTester() 7 | 8 | const errors = [{ messageId: 'unexpected' }] 9 | 10 | ruleTester.run('no-async-before', rule, { 11 | valid: [ 12 | { code: 'before(\'a before case\', () => { cy.get(\'.someClass\'); })' }, 13 | { code: 'before(\'a before case\', async () => { await somethingAsync(); })' }, 14 | { code: 'async function nonTestFn () { return await somethingAsync(); }' }, 15 | { code: 'const nonTestArrowFn = async () => { await somethingAsync(); }' }, 16 | ], 17 | invalid: [ 18 | { code: 'before(\'a test case\', async () => { cy.get(\'.someClass\'); })', errors }, 19 | { code: 'beforeEach(\'a test case\', async () => { cy.get(\'.someClass\'); })', errors }, 20 | { code: 'before(\'a test case\', async function () { cy.get(\'.someClass\'); })', errors }, 21 | { code: 'beforeEach(\'a test case\', async function () { cy.get(\'.someClass\'); })', errors }, 22 | ], 23 | }) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cypress.io 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 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/waiting.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | context('Waiting', () => { 3 | beforeEach(() => { 4 | cy.visit('https://example.cypress.io/commands/waiting') 5 | }) 6 | // BE CAREFUL of adding unnecessary wait times. 7 | // https://on.cypress.io/best-practices#Unnecessary-Waiting 8 | 9 | // https://on.cypress.io/wait 10 | it('cy.wait() - wait for a specific amount of time', () => { 11 | cy.get('.wait-input1').type('Wait 1000ms after typing') 12 | cy.wait(1000) 13 | cy.get('.wait-input2').type('Wait 1000ms after typing') 14 | cy.wait(1000) 15 | cy.get('.wait-input3').type('Wait 1000ms after typing') 16 | cy.wait(1000) 17 | }) 18 | 19 | it('cy.wait() - wait for a specific route', () => { 20 | // Listen to GET to comments/1 21 | cy.intercept('GET', '**/comments/*').as('getComment') 22 | 23 | // we have code that gets a comment when 24 | // the button is clicked in scripts.js 25 | cy.get('.network-btn').click() 26 | 27 | // wait for GET comments/1 28 | cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304]) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /docs/rules/require-data-selectors.md: -------------------------------------------------------------------------------- 1 | # Require `data-*` attribute selectors (`cypress/require-data-selectors`) 2 | 3 | 4 | 5 | Require `cy.get` to use only selectors that target `data-*` attributes. 6 | 7 | > Note: If you use this rule, consider only using the `warn` error level, since using `data-*` attribute selectors may not always be possible. 8 | 9 | ## Rule Details 10 | 11 | Examples of **incorrect** code for this rule: 12 | 13 | ```js 14 | cy.get('.a') 15 | cy.get('[daedta-cy=submit]').click() 16 | cy.get('[d-cy=submit]') 17 | cy.get('.btn-large').click() 18 | cy.get('.btn-.large').click() 19 | 20 | const CLASS_SELECTOR = ".my-class"; 21 | cy.get(CLASS_SELECTOR) 22 | ``` 23 | 24 | Examples of **correct** code for this rule: 25 | 26 | ```js 27 | cy.get('[data-cy=submit]').click() 28 | cy.get('[data-QA=submit]') 29 | cy.get(`[data-QA=submit]`) 30 | ``` 31 | 32 | ```js 33 | const ASSESSMENT_SUBMIT = "[data-cy=assessment-submit]" 34 | cy.get(ASSESSMENT_SUBMIT).click() 35 | ``` 36 | 37 | ## Further Reading 38 | 39 | See [the Cypress Best Practices guide](https://docs.cypress.io/app/core-concepts/best-practices.html#Selecting-Elements). 40 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/location.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Location', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/location') 6 | }) 7 | 8 | it('cy.hash() - get the current URL hash', () => { 9 | // https://on.cypress.io/hash 10 | cy.hash().should('be.empty') 11 | }) 12 | 13 | it('cy.location() - get window.location', () => { 14 | // https://on.cypress.io/location 15 | cy.location().should((location) => { 16 | expect(location.hash).to.be.empty 17 | expect(location.href).to.eq('https://example.cypress.io/commands/location') 18 | expect(location.host).to.eq('example.cypress.io') 19 | expect(location.hostname).to.eq('example.cypress.io') 20 | expect(location.origin).to.eq('https://example.cypress.io') 21 | expect(location.pathname).to.eq('/commands/location') 22 | expect(location.port).to.eq('') 23 | expect(location.protocol).to.eq('https:') 24 | expect(location.search).to.be.empty 25 | }) 26 | }) 27 | 28 | it('cy.url() - get the current URL', () => { 29 | // https://on.cypress.io/url 30 | cy.url().should('eq', 'https://example.cypress.io/commands/location') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test-project/README.md: -------------------------------------------------------------------------------- 1 | # Test-project 2 | 3 | This test project was generated via the Cypress App. 4 | 5 | This project can be updated to the latest Cypress default scaffolded E2E test specs by carrying out the following steps in the directory `/test-project`: 6 | 7 | ```shell 8 | rm -rf cypress cypress.config.js 9 | npm install cypress@latest --no-package-lock 10 | npx cypress open 11 | ``` 12 | 13 | - Select "Continue" for "What's New in Cypress" if displayed 14 | - Select "E2E Testing" 15 | - Select "Continue" in "Configuration files" 16 | - Select "Electron" browser 17 | - Select "Start E2E Testing in Electron" 18 | - Select "Scaffold example specs" 19 | - Close all Cypress windows 20 | 21 | Remove Cypress from `package.json`: 22 | 23 | ```shell 24 | npm uninstall cypress --no-package-lock 25 | ``` 26 | 27 | ## Tests 28 | 29 | Tests are run via [circle.yml](../circle.yml). 30 | 31 | To test the project locally: 32 | 33 | ```shell 34 | cd test-project 35 | npm install eslint@latest eslint-plugin-cypress@latest cypress@latest -D 36 | npx cypress run 37 | npx eslint 38 | ``` 39 | 40 | Do not commit the changes from installing the above dependencies. 41 | The CircleCI pipeline dynamically installs the latest `eslint*` dependencies. 42 | Cypress is not needed for the CircleCI tests. 43 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/aliasing.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Aliasing', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/aliasing') 6 | }) 7 | 8 | it('.as() - alias a DOM element for later use', () => { 9 | // https://on.cypress.io/as 10 | 11 | // Alias a DOM element for use later 12 | // We don't have to traverse to the element 13 | // later in our code, we reference it with @ 14 | 15 | cy.get('.as-table').find('tbody>tr') 16 | .first().find('td').first() 17 | .find('button').as('firstBtn') 18 | 19 | // when we reference the alias, we place an 20 | // @ in front of its name 21 | cy.get('@firstBtn').click() 22 | 23 | cy.get('@firstBtn') 24 | .should('have.class', 'btn-success') 25 | .and('contain', 'Changed') 26 | }) 27 | 28 | it('.as() - alias a route for later use', () => { 29 | // Alias the route to wait for its response 30 | cy.intercept('GET', '**/comments/*').as('getComment') 31 | 32 | // we have code that gets a comment when 33 | // the button is clicked in scripts.js 34 | cy.get('.network-btn').click() 35 | 36 | // https://on.cypress.io/wait 37 | cy.wait('@getComment').its('response.statusCode').should('eq', 200) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /tests/lib/rules/no-assigning-return-values.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/no-assigning-return-values') 4 | const RuleTester = require('eslint').RuleTester 5 | 6 | const ruleTester = new RuleTester() 7 | 8 | const errors = [{ messageId: 'unexpected' }] 9 | 10 | ruleTester.run('no-assigning-return-values', rule, { 11 | valid: [ 12 | { code: 'var foo = true;' }, 13 | { code: 'let foo = true;' }, 14 | { code: 'const foo = true;' }, 15 | { code: 'const foo = bar();' }, 16 | { code: 'const foo = bar().baz();' }, 17 | { code: 'const spy = cy.spy();' }, 18 | { code: 'const spy = cy.spy().as();' }, 19 | { code: 'const stub = cy.stub();' }, 20 | { code: 'const result = cy.now();' }, 21 | { code: 'const state = cy.state();' }, 22 | { code: 'cy.get("foo");' }, 23 | { code: 'cy.contains("foo").click();' }, 24 | ], 25 | 26 | invalid: [ 27 | { code: 'let a = cy.get("foo")', errors }, 28 | { code: 'const a = cy.get("foo")', errors }, 29 | { code: 'var a = cy.get("foo")', errors }, 30 | 31 | { code: 'let a = cy.contains("foo")', errors }, 32 | { code: 'let a = cy.window()', errors }, 33 | { code: 'let a = cy.wait("@something")', errors }, 34 | 35 | { code: 'let a = cy.contains("foo").click()', errors }, 36 | ], 37 | }) 38 | -------------------------------------------------------------------------------- /tests/lib/rules/unsafe-to-chain-command.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/unsafe-to-chain-command') 4 | const RuleTester = require('eslint').RuleTester 5 | 6 | const ruleTester = new RuleTester() 7 | 8 | const errors = [{ messageId: 'unexpected' }] 9 | 10 | ruleTester.run('action-ends-chain', rule, { 11 | valid: [ 12 | { 13 | code: 'cy.get("new-todo").type("todo A{enter}"); cy.get("new-todo").type("todo B{enter}"); cy.get("new-todo").should("have.class", "active");', 14 | }, 15 | { code: 'cy.focused().should("be.visible");' }, 16 | { code: 'cy.submitBtn().click();' }, 17 | ], 18 | 19 | invalid: [ 20 | { code: 'cy.get("new-todo").type("todo A{enter}").should("have.class", "active");', errors }, 21 | { code: 'cy.get("new-todo").type("todo A{enter}").type("todo B{enter}");', errors }, 22 | { code: 'cy.get("new-todo").focus().should("have.class", "active");', errors }, 23 | { 24 | code: 'cy.get("new-todo").customType("todo A{enter}").customClick();', 25 | options: [{ methods: ['customType', 'customClick'] }], 26 | errors, 27 | }, 28 | { 29 | code: 'cy.get("new-todo").customPress("Enter").customScroll();', 30 | options: [{ methods: ['customPress', 'customScroll'] }], 31 | errors, 32 | }, 33 | ], 34 | }) 35 | -------------------------------------------------------------------------------- /tests/lib/rules/require-data-selectors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/require-data-selectors') 4 | const RuleTester = require('eslint').RuleTester 5 | 6 | const ruleTester = new RuleTester() 7 | 8 | const errors = [{ messageId: 'unexpected' }] 9 | 10 | ruleTester.run('require-data-selectors', rule, { 11 | valid: [ 12 | { code: 'cy.get(\'[data-cy=submit]\').click()' }, 13 | { code: 'cy.get(\'[data-QA=submit]\')' }, 14 | { code: 'cy.clock(5000)' }, 15 | { code: 'cy.scrollTo(0, 10)' }, 16 | { code: 'cy.tick(500)' }, 17 | { code: 'cy.get(\`[data-cy=${1}]\`)' }, // eslint-disable-line no-useless-escape 18 | { code: 'cy.get("@my-alias")' }, 19 | { code: 'cy.get(`@my-alias`)' }, 20 | { code: 'const ASSESSMENT_SUBMIT = "[data-cy=assessment-submit]"; cy.get(ASSESSMENT_SUBMIT)' }, 21 | { code: 'const ALIAS_TEMPLATE = `@my-alias`; cy.get(ALIAS_TEMPLATE)' }, 22 | ], 23 | 24 | invalid: [ 25 | { code: 'cy.get(\'[daedta-cy=submit]\').click()', errors }, 26 | { code: 'cy.get(\'[d-cy=submit]\')', errors }, 27 | { code: 'cy.get(".btn-large").click()', errors }, 28 | { code: 'cy.get(".btn-.large").click()', errors }, 29 | { code: 'cy.get(".a")', errors }, 30 | { code: 'cy.get(\`[daedta-cy=${1}]\`)', errors }, // eslint-disable-line no-useless-escape 31 | { code: 'const BAD_SELECTOR = ".my-class"; cy.get(BAD_SELECTOR)', errors }, 32 | { code: 'const GOOD = "[data-cy=good]"; const BAD = ".bad"; cy.get(GOOD); cy.get(BAD)', errors }, 33 | ], 34 | }) 35 | -------------------------------------------------------------------------------- /docs/rules/no-async-before.md: -------------------------------------------------------------------------------- 1 | # Disallow using `async`/`await` in Cypress `before` methods (`cypress/no-async-before`) 2 | 3 | 4 | 5 | Cypress commands that return a promise may cause side effects in `before`/`beforeEach` hooks, possibly causing unexpected behavior. 6 | 7 | ## Rule Details 8 | 9 | This rule disallows using `async` `before` and `beforeEach` functions. 10 | 11 | Examples of **incorrect** code for this rule: 12 | 13 | ```js 14 | describe('my feature', () => { 15 | before('my test case', async () => { 16 | await cy.get('.myClass') 17 | // other operations 18 | }) 19 | }) 20 | ``` 21 | 22 | ```js 23 | describe('my feature', () => { 24 | before('my test case', async () => { 25 | cy.get('.myClass').click() 26 | 27 | await someAsyncFunction() 28 | }) 29 | }) 30 | ``` 31 | 32 | Examples of **correct** code for this rule: 33 | 34 | ```js 35 | describe('my feature', () => { 36 | before('my test case', () => { 37 | cy.get('.myClass') 38 | // other operations 39 | }) 40 | }) 41 | ``` 42 | 43 | ## When Not To Use It 44 | 45 | If there are genuine use-cases for using `async/await` in your `before` hooks then you may not want to include this rule (or at least demote it to a warning). 46 | 47 | ## Further Reading 48 | 49 | - [Mixing Async and Sync code](https://on.cypress.io/app/core-concepts/introduction-to-cypress#Mixing-Async-and-Sync-code) 50 | - [Commands Are Asynchronous](https://on.cypress.io/app/core-concepts/introduction-to-cypress.html#Commands-Are-Asynchronous) 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-cypress", 3 | "version": "0.0.0-development", 4 | "description": "An ESLint plugin for projects using Cypress", 5 | "main": "./lib/flat.js", 6 | "exports": { 7 | ".": "./lib/flat.js", 8 | "./flat": "./lib/flat.js" 9 | }, 10 | "files": [ 11 | "lib", 12 | "docs", 13 | "CONTRIBUTING.md" 14 | ], 15 | "author": "Cypress-io", 16 | "license": "MIT", 17 | "keywords": [ 18 | "eslint", 19 | "eslintplugin", 20 | "cypress" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/cypress-io/eslint-plugin-cypress.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/cypress-io/eslint-plugin-cypress/issues" 28 | }, 29 | "homepage": "https://github.com/cypress-io/eslint-plugin-cypress#readme", 30 | "peerDependencies": { 31 | "eslint": ">=9" 32 | }, 33 | "dependencies": { 34 | "globals": "^16.2.0" 35 | }, 36 | "devDependencies": { 37 | "@eslint/js": "^9.39.1", 38 | "@stylistic/eslint-plugin": "^5.4.0", 39 | "eslint": "^9.39.1", 40 | "eslint-plugin-eslint-plugin": "^7.0.0", 41 | "eslint-plugin-mocha": "^11.1.0", 42 | "husky": "^9.1.7", 43 | "prettier": "^3.6.2", 44 | "semantic-release": "24.2.7", 45 | "typescript": "^5.9.3", 46 | "typescript-eslint": "^8.45.0", 47 | "vitest": "^4.0.4" 48 | }, 49 | "scripts": { 50 | "lint": "eslint", 51 | "lint-fix": "eslint --fix", 52 | "format": "prettier --write .", 53 | "format:check": "prettier --check .", 54 | "semantic-release": "semantic-release", 55 | "test": "vitest run", 56 | "prepare": "husky" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/rules/no-async-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | meta: { 5 | type: 'problem', 6 | docs: { 7 | description: 'disallow using `async`/`await` in Cypress test cases', 8 | category: 'Possible Errors', 9 | recommended: true, 10 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-async-tests.md', 11 | }, 12 | schema: [], 13 | messages: { 14 | unexpected: 'Avoid using async functions with Cypress tests', 15 | }, 16 | }, 17 | 18 | create(context) { 19 | function isTestBlock(callExpressionNode) { 20 | const { type, name } = callExpressionNode.callee 21 | 22 | return type === 'Identifier' 23 | && (name === 'it' || name === 'test') 24 | } 25 | 26 | function isTestAsync(node) { 27 | return node.arguments 28 | && node.arguments.length >= 2 29 | && node.arguments[1].async === true 30 | } 31 | const sourceCode = context.sourceCode ?? context.getSourceCode() 32 | 33 | return { 34 | Identifier(node) { 35 | if (node.name === 'cy' || node.name === 'Cypress') { 36 | const ancestors = sourceCode.getAncestors 37 | ? sourceCode.getAncestors(node) 38 | : context.getAncestors() 39 | const asyncTestBlocks = ancestors 40 | .filter((n) => n.type === 'CallExpression') 41 | .filter(isTestBlock) 42 | .filter(isTestAsync) 43 | 44 | if (asyncTestBlocks.length >= 1) { 45 | asyncTestBlocks.forEach((node) => { 46 | context.report({ node, messageId: 'unexpected' }) 47 | }) 48 | } 49 | } 50 | }, 51 | } 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /lib/rules/no-async-before.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | meta: { 5 | type: 'problem', 6 | docs: { 7 | description: 'disallow using `async`/`await` in Cypress `before` methods', 8 | category: 'Possible Errors', 9 | recommended: true, 10 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-async-before.md', 11 | }, 12 | schema: [], 13 | messages: { 14 | unexpected: 'Avoid using async functions with Cypress before / beforeEach functions', 15 | }, 16 | }, 17 | 18 | create(context) { 19 | function isBeforeBlock(callExpressionNode) { 20 | const { type, name } = callExpressionNode.callee 21 | 22 | return type === 'Identifier' 23 | && (name === 'before' || name === 'beforeEach') 24 | } 25 | 26 | function isBeforeAsync(node) { 27 | return node.arguments 28 | && node.arguments.length >= 2 29 | && node.arguments[1].async === true 30 | } 31 | const sourceCode = context.sourceCode ?? context.getSourceCode() 32 | 33 | return { 34 | Identifier(node) { 35 | if (node.name === 'cy' || node.name === 'Cypress') { 36 | const ancestors = sourceCode.getAncestors 37 | ? sourceCode.getAncestors(node) 38 | : context.getAncestors() 39 | const asyncTestBlocks = ancestors 40 | .filter((n) => n.type === 'CallExpression') 41 | .filter(isBeforeBlock) 42 | .filter(isBeforeAsync) 43 | 44 | if (asyncTestBlocks.length >= 1) { 45 | asyncTestBlocks.forEach((node) => { 46 | context.report({ node, messageId: 'unexpected' }) 47 | }) 48 | } 49 | } 50 | }, 51 | } 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /docs/rules/no-async-tests.md: -------------------------------------------------------------------------------- 1 | # Disallow using `async`/`await` in Cypress test cases (`cypress/no-async-tests`) 2 | 3 | 💼 This rule is enabled in the ✅ `recommended` config. 4 | 5 | 6 | 7 | Cypress tests [that return a promise will error](https://docs.cypress.io/app/references/error-messages.html#Cypress-detected-that-you-returned-a-promise-from-a-command-while-also-invoking-one-or-more-cy-commands-in-that-promise) and cannot run successfully. 8 | An `async` function returns a promise under the hood, so a test using an `async` function will also error. 9 | 10 | ## Rule Details 11 | 12 | This rule disallows using `async` test functions. 13 | 14 | Examples of **incorrect** code for this rule: 15 | 16 | ```js 17 | describe('my feature', () => { 18 | it('my test case', async () => { 19 | await cy.get('.myClass') 20 | // other operations 21 | }) 22 | }) 23 | ``` 24 | 25 | ```js 26 | describe('my feature', () => { 27 | it('my test case', async () => { 28 | cy.get('.myClass').click() 29 | 30 | await someAsyncFunction() 31 | }) 32 | }) 33 | ``` 34 | 35 | Examples of **correct** code for this rule: 36 | 37 | ```js 38 | describe('my feature', () => { 39 | it('my test case', () => { 40 | cy.get('.myClass') 41 | // other operations 42 | }) 43 | }) 44 | ``` 45 | 46 | ## When Not To Use It 47 | 48 | If there are genuine use-cases for using `async/await` in your test cases then you may not want to include this rule (or at least demote it to a warning). 49 | 50 | ## Further Reading 51 | 52 | - [Mixing Async and Sync code](https://on.cypress.io/app/core-concepts/introduction-to-cypress#Mixing-Async-and-Sync-code) 53 | - [Commands Are Asynchronous](https://on.cypress.io/app/core-concepts/introduction-to-cypress.html#Commands-Are-Asynchronous) 54 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from 'eslint/config' 2 | import globals from 'globals' 3 | import js from '@eslint/js' 4 | import eslintPlugin from 'eslint-plugin-eslint-plugin' 5 | import mochaPlugin from 'eslint-plugin-mocha' 6 | import stylistic from '@stylistic/eslint-plugin' 7 | import tseslint from 'typescript-eslint' 8 | 9 | export default defineConfig([ 10 | 11 | globalIgnores(['test-project/**/*', '!test-project/**/eslint*']), 12 | 13 | { 14 | files: ['**/*.{,m}js'], 15 | extends: [ 16 | js.configs.recommended, 17 | eslintPlugin.configs.recommended, 18 | mochaPlugin.configs.recommended, 19 | stylistic.configs.recommended, 20 | ], 21 | rules: { 22 | 'no-redeclare': 'off', 23 | '@stylistic/arrow-parens': ['error', 'always'], 24 | '@stylistic/indent': ['error', 2], 25 | '@stylistic/comma-dangle': ['error', 'always-multiline'], 26 | '@stylistic/quotes': ['error', 'single'], 27 | '@stylistic/semi': ['error', 'never'], 28 | 'eslint-plugin/require-meta-docs-url': 29 | ['error', { pattern: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/{{name}}.md' }], 30 | 'eslint-plugin/require-meta-docs-description': 'error', 31 | 'eslint-plugin/meta-property-ordering': 'error', 32 | 'eslint-plugin/test-case-property-ordering': 'error', 33 | 'mocha/no-mocha-arrows': 'off', 34 | 'mocha/no-setup-in-describe': 'off', 35 | }, 36 | languageOptions: { 37 | globals: globals.node, 38 | }, 39 | }, 40 | { 41 | files: ['**/*.d.ts'], 42 | extends: [ 43 | js.configs.recommended, 44 | tseslint.configs.recommended, 45 | tseslint.configs.stylistic, 46 | stylistic.configs.recommended, 47 | ], 48 | }, 49 | ]) 50 | -------------------------------------------------------------------------------- /tests/lib/rules/assertion-before-screenshot.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/assertion-before-screenshot') 4 | const RuleTester = require('eslint').RuleTester 5 | 6 | const ruleTester = new RuleTester() 7 | 8 | const errors = [{ messageId: 'unexpected' }] 9 | 10 | ruleTester.run('assertion-before-screenshot', rule, { 11 | valid: [ 12 | { code: 'cy.get(".some-element"); cy.screenshot();' }, 13 | { code: 'cy.get(".some-element").should("exist").screenshot();' }, 14 | { code: 'cy.get(".some-element").should("exist").screenshot().click()' }, 15 | { code: 'cy.get(".some-element").should("exist"); if(true) cy.screenshot();' }, 16 | { code: 'if(true) { cy.get(".some-element").should("exist"); cy.screenshot(); }' }, 17 | { code: 'cy.get(".some-element").should("exist"); if(true) { cy.screenshot(); }' }, 18 | { code: 'const a = () => { cy.get(".some-element").should("exist"); cy.screenshot(); }' }, 19 | { code: 'cy.get(".some-element").should("exist").and("be.visible"); cy.screenshot();' }, 20 | { code: 'cy.get(".some-element").contains("Text"); cy.screenshot();' }, 21 | ], 22 | 23 | invalid: [ 24 | { code: 'cy.screenshot()', errors }, 25 | { code: 'cy.visit("somepage"); cy.screenshot();', errors }, 26 | { code: 'cy.custom(); cy.screenshot()', errors }, 27 | { code: 'cy.get(".some-element").click(); cy.screenshot()', errors }, 28 | { code: 'cy.get(".some-element").click().screenshot()', errors }, 29 | { code: 'if(true) { cy.get(".some-element").click(); cy.screenshot(); }', errors }, 30 | { code: 'cy.get(".some-element").click(); if(true) { cy.screenshot(); }', errors }, 31 | { code: 'cy.get(".some-element"); function a() { cy.screenshot(); }', errors }, 32 | { code: 'cy.get(".some-element"); const a = () => { cy.screenshot(); }', errors }, 33 | ], 34 | }) 35 | -------------------------------------------------------------------------------- /lib/rules/no-assigning-return-values.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // safely get nested object property 4 | function get(obj, propertyString = '') { 5 | const properties = propertyString.split('.') 6 | 7 | for (let i = 0; i < properties.length; i++) { 8 | const value = (obj || {})[properties[i]] 9 | 10 | if (value == null) return value 11 | 12 | obj = value 13 | } 14 | 15 | return obj 16 | } 17 | 18 | module.exports = { 19 | meta: { 20 | type: 'problem', 21 | docs: { 22 | description: 'disallow assigning return values of `cy` calls', 23 | category: 'Possible Errors', 24 | recommended: true, 25 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-assigning-return-values.md', 26 | }, 27 | schema: [], 28 | messages: { 29 | unexpected: 'Do not assign the return value of a Cypress command', 30 | }, 31 | }, 32 | create(context) { 33 | return { 34 | VariableDeclaration(node) { 35 | if (node.declarations.some(isCypressCommandDeclaration)) { 36 | context.report({ node, messageId: 'unexpected' }) 37 | } 38 | }, 39 | } 40 | }, 41 | } 42 | 43 | const allowedCommands = { 44 | now: true, 45 | spy: true, 46 | state: true, 47 | stub: true, 48 | } 49 | 50 | function isCypressCommandDeclaration(declarator) { 51 | let object = get(declarator, 'init.callee.object') 52 | 53 | if (!object) return 54 | 55 | while (object.callee) { 56 | object = object.callee.object 57 | 58 | if (!object) return 59 | } 60 | 61 | const commandName = get(declarator, 'init.callee.property.name') 62 | 63 | const parent = get(object, 'parent.property.name') || get(declarator, 'id.name') 64 | 65 | if (commandName && (allowedCommands[commandName] || allowedCommands[parent])) return 66 | 67 | return object.name === 'cy' 68 | } 69 | -------------------------------------------------------------------------------- /tests/lib/rules/no-force.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/no-force') 4 | const RuleTester = require('eslint').RuleTester 5 | 6 | const ruleTester = new RuleTester() 7 | 8 | const errors = [{ messageId: 'unexpected' }] 9 | 10 | ruleTester.run('no-force', rule, { 11 | 12 | valid: [ 13 | { code: 'cy.get(\'button\').click()' }, 14 | { code: 'cy.get(\'button\').click({multiple: true})' }, 15 | { code: 'cy.get(\'button\').dblclick()' }, 16 | { code: 'cy.get(\'input\').type(\'somth\')' }, 17 | { code: 'cy.get(\'input\').type(\'somth\', {anyoption: true})' }, 18 | { code: 'cy.get(\'input\').trigger(\'click\', {anyoption: true})' }, 19 | { code: 'cy.get(\'input\').rightclick({anyoption: true})' }, 20 | { code: 'cy.get(\'input\').check()' }, 21 | { code: 'cy.get(\'input\').select()' }, 22 | { code: 'cy.get(\'input\').focus()' }, 23 | { code: 'cy.document().trigger("keydown", { ...event })' }, 24 | ], 25 | 26 | invalid: [ 27 | { code: 'cy.get(\'button\').click({force: true})', errors }, 28 | { code: 'cy.get(\'button\').dblclick({force: true})', errors }, 29 | { code: 'cy.get(\'input\').type(\'somth\', {force: true})', errors }, 30 | { code: 'cy.get(\'div\').find(\'.foo\').type(\'somth\', {force: true})', errors }, 31 | { code: 'cy.get(\'div\').find(\'.foo\').find(\'.bar\').click({force: true})', errors }, 32 | { code: 'cy.get(\'div\').find(\'.foo\').find(\'.bar\').trigger(\'change\', {force: true})', errors }, 33 | { code: 'cy.get(\'input\').trigger(\'click\', {force: true})', errors }, 34 | { code: 'cy.get(\'input\').rightclick({force: true})', errors }, 35 | { code: 'cy.get(\'input\').check({force: true})', errors }, 36 | { code: 'cy.get(\'input\').select({force: true})', errors }, 37 | { code: 'cy.get(\'input\').focus({force: true})', errors }, 38 | ], 39 | }) 40 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/navigation.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Navigation', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io') 6 | cy.get('.navbar-nav').contains('Commands').click() 7 | cy.get('.dropdown-menu').contains('Navigation').click() 8 | }) 9 | 10 | it('cy.go() - go back or forward in the browser\'s history', () => { 11 | // https://on.cypress.io/go 12 | 13 | cy.location('pathname').should('include', 'navigation') 14 | 15 | cy.go('back') 16 | cy.location('pathname').should('not.include', 'navigation') 17 | 18 | cy.go('forward') 19 | cy.location('pathname').should('include', 'navigation') 20 | 21 | // clicking back 22 | cy.go(-1) 23 | cy.location('pathname').should('not.include', 'navigation') 24 | 25 | // clicking forward 26 | cy.go(1) 27 | cy.location('pathname').should('include', 'navigation') 28 | }) 29 | 30 | it('cy.reload() - reload the page', () => { 31 | // https://on.cypress.io/reload 32 | cy.reload() 33 | 34 | // reload the page without using the cache 35 | cy.reload(true) 36 | }) 37 | 38 | it('cy.visit() - visit a remote url', () => { 39 | // https://on.cypress.io/visit 40 | 41 | // Visit any sub-domain of your current domain 42 | // Pass options to the visit 43 | cy.visit('https://example.cypress.io/commands/navigation', { 44 | timeout: 50000, // increase total time for the visit to resolve 45 | onBeforeLoad (contentWindow) { 46 | // contentWindow is the remote page's window object 47 | expect(typeof contentWindow === 'object').to.be.true 48 | }, 49 | onLoad (contentWindow) { 50 | // contentWindow is the remote page's window object 51 | expect(typeof contentWindow === 'object').to.be.true 52 | }, 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /lib/flat.js: -------------------------------------------------------------------------------- 1 | const globals = require('globals') 2 | const { name, version } = require('../package.json') 3 | 4 | const plugin = { 5 | meta: { name, version }, 6 | configs: {}, 7 | rules: { 8 | 'assertion-before-screenshot': require('./rules/assertion-before-screenshot'), 9 | 'no-assigning-return-values': require('./rules/no-assigning-return-values'), 10 | 'no-async-before': require('./rules/no-async-before'), 11 | 'no-async-tests': require('./rules/no-async-tests'), 12 | 'no-chained-get': require('./rules/no-chained-get'), 13 | 'no-debug': require('./rules/no-debug'), 14 | 'no-force': require('./rules/no-force'), 15 | 'no-pause': require('./rules/no-pause'), 16 | 'no-unnecessary-waiting': require('./rules/no-unnecessary-waiting'), 17 | 'no-xpath': require('./rules/no-xpath'), 18 | 'require-data-selectors': require('./rules/require-data-selectors'), 19 | 'unsafe-to-chain-command': require('./rules/unsafe-to-chain-command'), 20 | }, 21 | } 22 | 23 | const commonGlobals 24 | = Object.assign({ 25 | cy: false, 26 | Cypress: false, 27 | expect: false, 28 | assert: false, 29 | chai: false, 30 | }, globals.browser, globals.mocha) 31 | 32 | Object.assign(plugin.configs, { 33 | globals: { 34 | name: 'cypress/globals', 35 | plugins: { 36 | cypress: plugin, 37 | }, 38 | languageOptions: { 39 | globals: 40 | commonGlobals, 41 | }, 42 | }, 43 | }) 44 | 45 | Object.assign(plugin.configs, { 46 | recommended: { 47 | name: 'cypress/recommended', 48 | plugins: { 49 | cypress: plugin, 50 | }, 51 | rules: { 52 | 'cypress/no-assigning-return-values': 'error', 53 | 'cypress/no-unnecessary-waiting': 'error', 54 | 'cypress/no-async-tests': 'error', 55 | 'cypress/unsafe-to-chain-command': 'error', 56 | }, 57 | languageOptions: { 58 | globals: 59 | commonGlobals, 60 | }, 61 | }, 62 | }) 63 | 64 | module.exports = plugin 65 | -------------------------------------------------------------------------------- /lib/rules/no-debug.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // ------------------------------------------------------------------------------ 4 | // Rule Definition 5 | // ------------------------------------------------------------------------------ 6 | 7 | module.exports = { 8 | meta: { 9 | type: 'suggestion', 10 | docs: { 11 | description: 'disallow using `cy.debug()` calls', 12 | category: 'Possible Errors', 13 | recommended: false, 14 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-debug.md', 15 | }, 16 | fixable: null, // or "code" or "whitespace" 17 | schema: [], 18 | messages: { 19 | unexpected: 'Do not use cy.debug command', 20 | }, 21 | }, 22 | 23 | create(context) { 24 | // variables should be defined here 25 | 26 | // ---------------------------------------------------------------------- 27 | // Helpers 28 | // ---------------------------------------------------------------------- 29 | function isCallingDebug(node) { 30 | return node.callee 31 | && node.callee.property 32 | && node.callee.property.type === 'Identifier' 33 | && node.callee.property.name === 'debug' 34 | } 35 | 36 | function isCypressCall(node) { 37 | if (!node.callee || node.callee.type !== 'MemberExpression') { 38 | return false 39 | } 40 | if (node.callee.object.type === 'Identifier' && node.callee.object.name === 'cy') { 41 | return true 42 | } 43 | return isCypressCall(node.callee.object) 44 | } 45 | 46 | // ---------------------------------------------------------------------- 47 | // Public 48 | // ---------------------------------------------------------------------- 49 | 50 | return { 51 | 52 | CallExpression(node) { 53 | if (isCypressCall(node) && isCallingDebug(node)) { 54 | context.report({ node, messageId: 'unexpected' }) 55 | } 56 | }, 57 | 58 | } 59 | }, 60 | } 61 | -------------------------------------------------------------------------------- /lib/rules/no-pause.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // ------------------------------------------------------------------------------ 4 | // Rule Definition 5 | // ------------------------------------------------------------------------------ 6 | 7 | module.exports = { 8 | meta: { 9 | type: 'suggestion', 10 | docs: { 11 | description: 'disallow using `cy.pause()` calls', 12 | category: 'Possible Errors', 13 | recommended: false, 14 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-pause.md', 15 | }, 16 | fixable: null, // or "code" or "whitespace" 17 | schema: [], 18 | messages: { 19 | unexpected: 'Do not use cy.pause command', 20 | }, 21 | }, 22 | 23 | create(context) { 24 | // variables should be defined here 25 | 26 | // ---------------------------------------------------------------------- 27 | // Helpers 28 | // ---------------------------------------------------------------------- 29 | function isCallingPause(node) { 30 | return node.callee 31 | && node.callee.property 32 | && node.callee.property.type === 'Identifier' 33 | && node.callee.property.name === 'pause' 34 | } 35 | 36 | function isCypressCall(node) { 37 | if (!node.callee || node.callee.type !== 'MemberExpression') { 38 | return false 39 | } 40 | if (node.callee.object.type === 'Identifier' && node.callee.object.name === 'cy') { 41 | return true 42 | } 43 | return isCypressCall(node.callee.object) 44 | } 45 | 46 | // ---------------------------------------------------------------------- 47 | // Public 48 | // ---------------------------------------------------------------------- 49 | 50 | return { 51 | 52 | CallExpression(node) { 53 | if (isCypressCall(node) && isCallingPause(node)) { 54 | context.report({ node, messageId: 'unexpected' }) 55 | } 56 | }, 57 | 58 | } 59 | }, 60 | } 61 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/viewport.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | context('Viewport', () => { 3 | beforeEach(() => { 4 | cy.visit('https://example.cypress.io/commands/viewport') 5 | }) 6 | 7 | it('cy.viewport() - set the viewport size and dimension', () => { 8 | // https://on.cypress.io/viewport 9 | 10 | cy.get('#navbar').should('be.visible') 11 | cy.viewport(320, 480) 12 | 13 | // the navbar should have collapse since our screen is smaller 14 | cy.get('#navbar').should('not.be.visible') 15 | cy.get('.navbar-toggle').should('be.visible').click() 16 | cy.get('.nav').find('a').should('be.visible') 17 | 18 | // lets see what our app looks like on a super large screen 19 | cy.viewport(2999, 2999) 20 | 21 | // cy.viewport() accepts a set of preset sizes 22 | // to easily set the screen to a device's width and height 23 | 24 | // We added a cy.wait() between each viewport change so you can see 25 | // the change otherwise it is a little too fast to see :) 26 | 27 | cy.viewport('macbook-15') 28 | cy.wait(200) 29 | cy.viewport('macbook-13') 30 | cy.wait(200) 31 | cy.viewport('macbook-11') 32 | cy.wait(200) 33 | cy.viewport('ipad-2') 34 | cy.wait(200) 35 | cy.viewport('ipad-mini') 36 | cy.wait(200) 37 | cy.viewport('iphone-6+') 38 | cy.wait(200) 39 | cy.viewport('iphone-6') 40 | cy.wait(200) 41 | cy.viewport('iphone-5') 42 | cy.wait(200) 43 | cy.viewport('iphone-4') 44 | cy.wait(200) 45 | cy.viewport('iphone-3') 46 | cy.wait(200) 47 | 48 | // cy.viewport() accepts an orientation for all presets 49 | // the default orientation is 'portrait' 50 | cy.viewport('ipad-2', 'portrait') 51 | cy.wait(200) 52 | cy.viewport('iphone-4', 'landscape') 53 | cy.wait(200) 54 | 55 | // The viewport will be reset back to the default dimensions 56 | // in between tests (the default can be set in cypress.config.{js|ts}) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /docs/rules/no-force.md: -------------------------------------------------------------------------------- 1 | # Disallow using `force: true` with action commands (`cypress/no-force`) 2 | 3 | 4 | 5 | Using `force: true` on inputs appears to be confusing rather than helpful. 6 | It usually silences the actual problem instead of providing a way to overcome it. 7 | See [Cypress Core Concepts](https://docs.cypress.io/app/core-concepts/interacting-with-elements.html#Forcing). 8 | 9 | If enabling this rule, it's recommended to set the severity to `warn`. 10 | 11 | ## Rule Details 12 | 13 | This rule disallows using the `force` option on:[`.click()`](https://on.cypress.io/click), 14 | [`.dblclick()`](https://on.cypress.io/dblclick), [`.type()`](https://on.cypress.io/type), 15 | [`.rightclick()`](https://on.cypress.io/rightclick), [`.select()`](https://on.cypress.io/select), 16 | [`.focus()`](https://on.cypress.io/focus), [`.check()`](https://on.cypress.io/check), 17 | and [`.trigger()`](https://on.cypress.io/trigger). 18 | 19 | Examples of **incorrect** code for this rule: 20 | 21 | ```js 22 | cy.get('button').click({ force: true }) 23 | cy.get('button').dblclick({ force: true }) 24 | cy.get('input').type('somth', { force: true }) 25 | cy.get('div').find('.foo').find('.bar').trigger('change', { force: true }) 26 | cy.get('input').trigger('click', { force: true }) 27 | cy.get('input').rightclick({ force: true }) 28 | cy.get('input').check({ force: true }) 29 | cy.get('input').select({ force: true }) 30 | cy.get('input').focus({ force: true }) 31 | ``` 32 | 33 | Examples of **correct** code for this rule: 34 | 35 | ```js 36 | cy.get('button').click() 37 | cy.get('button').click({ multiple: true }) 38 | cy.get('button').dblclick() 39 | cy.get('input').type('somth') 40 | cy.get('input').trigger('click', { anyoption: true }) 41 | cy.get('input').rightclick({ anyoption: true }) 42 | cy.get('input').check() 43 | cy.get('input').select() 44 | cy.get('input').focus() 45 | ``` 46 | 47 | ## When Not To Use It 48 | 49 | If you don't mind using `{ force: true }` with action commands, then turn this rule off. 50 | -------------------------------------------------------------------------------- /tests/lib/rules/no-unnecessary-waiting.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/no-unnecessary-waiting') 4 | const RuleTester = require('eslint').RuleTester 5 | 6 | const ruleTester = new RuleTester() 7 | 8 | const errors = [{ messageId: 'unexpected' }] 9 | 10 | ruleTester.run('no-unnecessary-waiting', rule, { 11 | valid: [ 12 | { code: 'foo.wait(10)' }, 13 | 14 | { code: 'cy.wait("@someRequest")' }, 15 | { code: 'cy.wait("@someRequest", { log: false })' }, 16 | { code: 'cy.wait("@someRequest").then((xhr) => xhr)' }, 17 | { code: 'cy.wait(["@someRequest", "@anotherRequest"])' }, 18 | 19 | { code: 'cy.clock(5000)' }, 20 | { code: 'cy.scrollTo(0, 10)' }, 21 | { code: 'cy.tick(500)' }, 22 | 23 | { code: 'const someRequest="@someRequest"; cy.wait(someRequest)' }, 24 | { code: 'function customWait (alias = "@someRequest") { cy.wait(alias) }' }, 25 | { code: 'const customWait = (alias = "@someRequest") => { cy.wait(alias) }' }, 26 | { code: 'function customWait (ms) { cy.wait(ms) }' }, 27 | { code: 'const customWait = (ms) => { cy.wait(ms) }' }, 28 | 29 | { code: 'import BAR_BAZ from "bar-baz"; cy.wait(BAR_BAZ)' }, 30 | { code: 'import { FOO_BAR } from "foo-bar"; cy.wait(FOO_BAR)' }, 31 | { code: 'import * as wildcard from "wildcard"; cy.wait(wildcard.value)' }, 32 | { code: 'import { NAME as OTHER_NAME } from "rename"; cy.wait(OTHER_NAME)' }, 33 | ], 34 | 35 | invalid: [ 36 | { code: 'cy.wait(0)', errors }, 37 | { code: 'cy.wait(100)', errors }, 38 | { code: 'cy.wait(5000)', errors }, 39 | { code: 'const someNumber=500; cy.wait(someNumber)', errors }, 40 | { code: 'function customWait (ms = 1) { cy.wait(ms) }', errors }, 41 | { code: 'const customWait = (ms = 1) => { cy.wait(ms) }', errors }, 42 | 43 | { code: 'cy.get(".some-element").wait(10)', errors }, 44 | { code: 'cy.get(".some-element").contains("foo").wait(10)', errors }, 45 | { code: 'const customWait = (ms = 1) => { cy.get(".some-element").wait(ms) }', errors }, 46 | ], 47 | }) 48 | -------------------------------------------------------------------------------- /lib/rules/no-chained-get.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // ------------------------------------------------------------------------------ 4 | // Rule Definition 5 | // ------------------------------------------------------------------------------ 6 | 7 | /** @type {import('eslint').Rule.RuleModule} */ 8 | module.exports = { 9 | meta: { 10 | type: 'problem', 11 | docs: { 12 | description: 'disallow chain of `cy.get()` calls', 13 | recommended: false, 14 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-chained-get.md', 15 | }, 16 | fixable: null, 17 | schema: [], 18 | messages: { 19 | unexpected: 'Avoid chaining multiple cy.get() calls', 20 | }, 21 | }, 22 | 23 | create(context) { 24 | const isRootCypress = (node) => { 25 | if ( 26 | node.type !== 'CallExpression' 27 | || node.callee.type !== 'MemberExpression' 28 | ) { 29 | return false 30 | } 31 | 32 | if ( 33 | node.callee.object.type === 'Identifier' 34 | && node.callee.object.name === 'cy' 35 | ) { 36 | return true 37 | } 38 | 39 | return isRootCypress(node.callee.object) 40 | } 41 | 42 | const hasChainedGet = (node) => { 43 | // Check if this node is a get() call 44 | const isGetCall 45 | = node.callee 46 | && node.callee.type === 'MemberExpression' 47 | && node.callee.property 48 | && node.callee.property.type === 'Identifier' 49 | && node.callee.property.name === 'get' 50 | 51 | if (!isGetCall) { 52 | return false 53 | } 54 | 55 | const obj = node.callee.object 56 | 57 | if (obj.type === 'CallExpression') { 58 | const objCallee = obj.callee 59 | 60 | if ( 61 | objCallee 62 | && objCallee.type === 'MemberExpression' 63 | && objCallee.property 64 | && objCallee.property.type === 'Identifier' 65 | && objCallee.property.name === 'get' 66 | ) { 67 | return true 68 | } 69 | 70 | return hasChainedGet(obj) 71 | } 72 | 73 | return false 74 | } 75 | 76 | return { 77 | CallExpression(node) { 78 | if (isRootCypress(node) && hasChainedGet(node)) { 79 | context.report({ 80 | node, 81 | messageId: 'unexpected', 82 | }) 83 | } 84 | }, 85 | } 86 | }, 87 | } 88 | -------------------------------------------------------------------------------- /lib/rules/require-data-selectors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | meta: { 4 | type: 'suggestion', 5 | docs: { 6 | description: 'require `data-*` attribute selectors', 7 | category: 'Possible Errors', 8 | recommended: false, 9 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/require-data-selectors.md', 10 | }, 11 | schema: [], 12 | messages: { 13 | unexpected: 'use data-* attribute selectors instead of classes or tag names', 14 | }, 15 | }, 16 | 17 | create(context) { 18 | const variablesSet = new Set() 19 | return { 20 | VariableDeclarator(node) { 21 | if (node.init && node.id && node.id.type === 'Identifier') { 22 | let selectorValue = null 23 | 24 | if (node.init.type === 'Literal' && typeof node.init.value === 'string') { 25 | selectorValue = node.init.value 26 | } 27 | else if (node.init.type === 'TemplateLiteral' 28 | && node.init.expressions.length === 0 29 | && node.init.quasis.length === 1) { 30 | selectorValue = node.init.quasis[0].value.cooked 31 | } 32 | 33 | if (selectorValue && isAliasOrDataSelector(selectorValue)) { 34 | variablesSet.add(node.id.name) 35 | } 36 | } 37 | }, 38 | 39 | CallExpression(node) { 40 | if (isCallingCyGet(node) && !isDataArgument(node, variablesSet)) { 41 | context.report({ node, messageId: 'unexpected' }) 42 | } 43 | }, 44 | } 45 | }, 46 | } 47 | 48 | function isCallingCyGet(node) { 49 | return node.callee.type === 'MemberExpression' 50 | && node.callee.object.type === 'Identifier' 51 | && node.callee.object.name === 'cy' 52 | && node.callee.property.type === 'Identifier' 53 | && node.callee.property.name === 'get' 54 | } 55 | 56 | function isDataArgument(node, dataVariables) { 57 | if (node.arguments.length === 0) return false 58 | 59 | const firstArg = node.arguments[0] 60 | 61 | if (firstArg.type === 'Literal') { 62 | return isAliasOrDataSelector(String(firstArg.value)) 63 | } 64 | 65 | if (firstArg.type === 'TemplateLiteral') { 66 | return isAliasOrDataSelector(String(firstArg.quasis[0].value.cooked)) 67 | } 68 | 69 | if (firstArg.type === 'Identifier') { 70 | return dataVariables.has(firstArg.name) 71 | } 72 | 73 | return false 74 | } 75 | 76 | function isAliasOrDataSelector(selector) { 77 | return ['[data-', '@'].some(function (validValue) { 78 | return selector.startsWith(validValue) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /lib/rules/no-force.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // ------------------------------------------------------------------------------ 4 | // Rule Definition 5 | // ------------------------------------------------------------------------------ 6 | 7 | module.exports = { 8 | meta: { 9 | type: 'suggestion', 10 | docs: { 11 | description: 'disallow using `force: true` with action commands', 12 | category: 'Possible Errors', 13 | recommended: false, 14 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-force.md', 15 | }, 16 | fixable: null, // or "code" or "whitespace" 17 | schema: [], 18 | messages: { 19 | unexpected: 'Do not use force on click and type calls', 20 | }, 21 | }, 22 | 23 | create(context) { 24 | // variables should be defined here 25 | 26 | // ---------------------------------------------------------------------- 27 | // Helpers 28 | // ---------------------------------------------------------------------- 29 | function isCallingClickOrType(node) { 30 | const allowedMethods = ['click', 'dblclick', 'type', 'trigger', 'check', 'rightclick', 'focus', 'select'] 31 | 32 | return node.property && node.property.type === 'Identifier' 33 | && allowedMethods.includes(node.property.name) 34 | } 35 | 36 | function isCypressCall(node) { 37 | return node.callee.type === 'MemberExpression' 38 | && node.callee.object.type === 'Identifier' 39 | && node.callee.object.name === 'cy' 40 | } 41 | 42 | function hasOptionForce(node) { 43 | return node.arguments && node.arguments.length 44 | && node.arguments.some((arg) => { 45 | return arg.type === 'ObjectExpression' && arg.properties.some((propNode) => propNode.key && propNode.key.name === 'force') 46 | }) 47 | } 48 | 49 | function deepCheck(node, checkFunc) { 50 | let currentNode = node 51 | 52 | while (currentNode.parent) { 53 | if (checkFunc(currentNode.parent)) { 54 | return true 55 | } 56 | 57 | currentNode = currentNode.parent 58 | } 59 | 60 | return false 61 | } 62 | 63 | // ---------------------------------------------------------------------- 64 | // Public 65 | // ---------------------------------------------------------------------- 66 | 67 | return { 68 | 69 | CallExpression(node) { 70 | if (isCypressCall(node) && deepCheck(node, isCallingClickOrType) && deepCheck(node, hasOptionForce)) { 71 | context.report({ node, messageId: 'unexpected' }) 72 | } 73 | }, 74 | 75 | } 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/files.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /// JSON fixture file can be loaded directly using 4 | // the built-in JavaScript bundler 5 | const requiredExample = require('../../fixtures/example') 6 | 7 | context('Files', () => { 8 | beforeEach(() => { 9 | cy.visit('https://example.cypress.io/commands/files') 10 | 11 | // load example.json fixture file and store 12 | // in the test context object 13 | cy.fixture('example.json').as('example') 14 | }) 15 | 16 | it('cy.fixture() - load a fixture', () => { 17 | // https://on.cypress.io/fixture 18 | 19 | // Instead of writing a response inline you can 20 | // use a fixture file's content. 21 | 22 | // when application makes an Ajax request matching "GET **/comments/*" 23 | // Cypress will intercept it and reply with the object in `example.json` fixture 24 | cy.intercept('GET', '**/comments/*', { fixture: 'example.json' }).as('getComment') 25 | 26 | // we have code that gets a comment when 27 | // the button is clicked in scripts.js 28 | cy.get('.fixture-btn').click() 29 | 30 | cy.wait('@getComment').its('response.body') 31 | .should('have.property', 'name') 32 | .and('include', 'Using fixtures to represent data') 33 | }) 34 | 35 | it('cy.fixture() or require - load a fixture', function () { 36 | // we are inside the "function () { ... }" 37 | // callback and can use test context object "this" 38 | // "this.example" was loaded in "beforeEach" function callback 39 | expect(this.example, 'fixture in the test context') 40 | .to.deep.equal(requiredExample) 41 | 42 | // or use "cy.wrap" and "should('deep.equal', ...)" assertion 43 | cy.wrap(this.example) 44 | .should('deep.equal', requiredExample) 45 | }) 46 | 47 | it('cy.readFile() - read file contents', () => { 48 | // https://on.cypress.io/readfile 49 | 50 | // You can read a file and yield its contents 51 | // The filePath is relative to your project's root. 52 | cy.readFile(Cypress.config('configFile')).then((config) => { 53 | expect(config).to.be.an('string') 54 | }) 55 | }) 56 | 57 | it('cy.writeFile() - write to a file', () => { 58 | // https://on.cypress.io/writefile 59 | 60 | // You can write to a file 61 | 62 | // Use a response from a request to automatically 63 | // generate a fixture file for use later 64 | cy.request('https://jsonplaceholder.cypress.io/users') 65 | .then((response) => { 66 | cy.writeFile('cypress/fixtures/users.json', response.body) 67 | }) 68 | 69 | cy.fixture('users').should((users) => { 70 | expect(users[0].name).to.exist 71 | }) 72 | 73 | // JavaScript arrays and objects are stringified 74 | // and formatted into text. 75 | cy.writeFile('cypress/fixtures/profile.json', { 76 | id: 8739, 77 | name: 'Jane', 78 | email: 'jane@example.com', 79 | }) 80 | 81 | cy.fixture('profile').should((profile) => { 82 | expect(profile.name).to.eq('Jane') 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /lib/rules/no-unnecessary-waiting.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | meta: { 5 | type: 'problem', 6 | docs: { 7 | description: 'disallow waiting for arbitrary time periods', 8 | category: 'Possible Errors', 9 | recommended: true, 10 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-unnecessary-waiting.md', 11 | }, 12 | schema: [], 13 | messages: { 14 | unexpected: 'Do not wait for arbitrary time periods', 15 | }, 16 | }, 17 | create(context) { 18 | const sourceCode = context.sourceCode ?? context.getSourceCode() 19 | 20 | return { 21 | CallExpression(node) { 22 | if (isCallingCyWait(node)) { 23 | const scope = sourceCode.getScope 24 | ? sourceCode.getScope(node) 25 | : context.getScope() 26 | 27 | if (isIdentifierNumberConstArgument(node, scope) || isNumberArgument(node)) { 28 | context.report({ node, messageId: 'unexpected' }) 29 | } 30 | } 31 | }, 32 | } 33 | }, 34 | } 35 | 36 | function nodeIsCalledByCy(node) { 37 | if (node.type === 'Identifier' && node.name === 'cy') return true 38 | 39 | if (typeof node.callee === 'undefined' || typeof node.callee.object === 'undefined') { 40 | return false 41 | } 42 | 43 | return nodeIsCalledByCy(node.callee.object) 44 | } 45 | 46 | function isCallingCyWait(node) { 47 | return node.callee.type === 'MemberExpression' 48 | && nodeIsCalledByCy(node) 49 | && node.callee.property.type === 'Identifier' 50 | && node.callee.property.name === 'wait' 51 | } 52 | 53 | function isNumberArgument(node) { 54 | return node.arguments.length > 0 55 | && node.arguments[0].type === 'Literal' 56 | && typeof (node.arguments[0].value) === 'number' 57 | } 58 | 59 | function isIdentifierNumberConstArgument(node, scope) { 60 | if (node.arguments.length === 0) return false 61 | 62 | if (node.arguments[0].type !== 'Identifier') return false 63 | 64 | const identifier = node.arguments[0] 65 | const resolvedIdentifier = scope.references.find((ref) => ref.identifier === identifier).resolved 66 | const definition = resolvedIdentifier.defs[0] 67 | const isVariable = definition.type === 'Variable' 68 | 69 | // const amount = 1000 or const amount = '@alias' 70 | // cy.wait(amount) 71 | if (isVariable) { 72 | if (!definition.node.init) return false 73 | 74 | return typeof definition.node.init.value === 'number' 75 | } 76 | 77 | // import { WAIT } from './constants' 78 | // cy.wait(WAIT) 79 | // we don't know if WAIT is a number or alias '@someRequest', so don't fail 80 | if (definition.type === 'ImportBinding') return false 81 | 82 | const param = definition.node.params[definition.index] 83 | 84 | // function wait (amount) { cy.wait(amount) } 85 | // we can't know the type of value, so don't fail 86 | if (!param || param.type !== 'AssignmentPattern') return false 87 | 88 | // function wait (amount = 1) { cy.wait(amount) } or 89 | // function wait (amount = '@alias') { cy.wait(amount) } 90 | return typeof param.right.value === 'number' 91 | } 92 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/misc.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Misc', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/misc') 6 | }) 7 | 8 | it('cy.exec() - execute a system command', () => { 9 | // execute a system command. 10 | // so you can take actions necessary for 11 | // your test outside the scope of Cypress. 12 | // https://on.cypress.io/exec 13 | 14 | // we can use Cypress.platform string to 15 | // select appropriate command 16 | // https://on.cypress/io/platform 17 | cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) 18 | 19 | // on CircleCI Windows build machines we have a failure to run bash shell 20 | // https://github.com/cypress-io/cypress/issues/5169 21 | // so skip some of the tests by passing flag "--env circle=true" 22 | const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle') 23 | 24 | if (isCircleOnWindows) { 25 | cy.log('Skipping test on CircleCI') 26 | 27 | return 28 | } 29 | 30 | // cy.exec problem on Shippable CI 31 | // https://github.com/cypress-io/cypress/issues/6718 32 | const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable') 33 | 34 | if (isShippable) { 35 | cy.log('Skipping test on ShippableCI') 36 | 37 | return 38 | } 39 | 40 | cy.exec('echo Jane Lane') 41 | .its('stdout').should('contain', 'Jane Lane') 42 | 43 | if (Cypress.platform === 'win32') { 44 | cy.exec(`print ${Cypress.config('configFile')}`) 45 | .its('stderr').should('be.empty') 46 | } 47 | else { 48 | cy.exec(`cat ${Cypress.config('configFile')}`) 49 | .its('stderr').should('be.empty') 50 | 51 | cy.log(`Cypress version ${Cypress.version}`) 52 | if (Cypress.version.split('.').map(Number)[0] < 15) { 53 | cy.exec('pwd') 54 | .its('code').should('eq', 0) 55 | } 56 | else { 57 | cy.exec('pwd') 58 | .its('exitCode').should('eq', 0) 59 | } 60 | } 61 | }) 62 | 63 | it('cy.focused() - get the DOM element that has focus', () => { 64 | // https://on.cypress.io/focused 65 | cy.get('.misc-form').find('#name').click() 66 | cy.focused().should('have.id', 'name') 67 | 68 | cy.get('.misc-form').find('#description').click() 69 | cy.focused().should('have.id', 'description') 70 | }) 71 | 72 | context('Cypress.Screenshot', function () { 73 | it('cy.screenshot() - take a screenshot', () => { 74 | // https://on.cypress.io/screenshot 75 | cy.screenshot('my-image') 76 | }) 77 | 78 | it('Cypress.Screenshot.defaults() - change default config of screenshots', function () { 79 | Cypress.Screenshot.defaults({ 80 | blackout: ['.foo'], 81 | capture: 'viewport', 82 | clip: { x: 0, y: 0, width: 200, height: 200 }, 83 | scale: false, 84 | disableTimersAndAnimations: true, 85 | screenshotOnRunFailure: true, 86 | onBeforeScreenshot () { }, 87 | onAfterScreenshot () { }, 88 | }) 89 | }) 90 | }) 91 | 92 | it('cy.wrap() - wrap an object', () => { 93 | // https://on.cypress.io/wrap 94 | cy.wrap({ foo: 'bar' }) 95 | .should('have.property', 'foo') 96 | .and('include', 'bar') 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/connectors.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Connectors', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/connectors') 6 | }) 7 | 8 | it('.each() - iterate over an array of elements', () => { 9 | // https://on.cypress.io/each 10 | cy.get('.connectors-each-ul>li') 11 | .each(($el, index, $list) => { 12 | console.log($el, index, $list) 13 | }) 14 | }) 15 | 16 | it('.its() - get properties on the current subject', () => { 17 | // https://on.cypress.io/its 18 | cy.get('.connectors-its-ul>li') 19 | // calls the 'length' property yielding that value 20 | .its('length') 21 | .should('be.gt', 2) 22 | }) 23 | 24 | it('.invoke() - invoke a function on the current subject', () => { 25 | // our div is hidden in our script.js 26 | // $('.connectors-div').hide() 27 | cy.get('.connectors-div').should('be.hidden') 28 | 29 | // https://on.cypress.io/invoke 30 | // call the jquery method 'show' on the 'div.container' 31 | cy.get('.connectors-div').invoke('show') 32 | 33 | cy.get('.connectors-div').should('be.visible') 34 | }) 35 | 36 | it('.spread() - spread an array as individual args to callback function', () => { 37 | // https://on.cypress.io/spread 38 | const arr = ['foo', 'bar', 'baz'] 39 | 40 | cy.wrap(arr).spread((foo, bar, baz) => { 41 | expect(foo).to.eq('foo') 42 | expect(bar).to.eq('bar') 43 | expect(baz).to.eq('baz') 44 | }) 45 | }) 46 | 47 | describe('.then()', () => { 48 | it('invokes a callback function with the current subject', () => { 49 | // https://on.cypress.io/then 50 | cy.get('.connectors-list > li') 51 | .then(($lis) => { 52 | expect($lis, '3 items').to.have.length(3) 53 | expect($lis.eq(0), 'first item').to.contain('Walk the dog') 54 | expect($lis.eq(1), 'second item').to.contain('Feed the cat') 55 | expect($lis.eq(2), 'third item').to.contain('Write JavaScript') 56 | }) 57 | }) 58 | 59 | it('yields the returned value to the next command', () => { 60 | cy.wrap(1) 61 | .then((num) => { 62 | expect(num).to.equal(1) 63 | 64 | return 2 65 | }) 66 | .then((num) => { 67 | expect(num).to.equal(2) 68 | }) 69 | }) 70 | 71 | it('yields the original subject without return', () => { 72 | cy.wrap(1) 73 | .then((num) => { 74 | expect(num).to.equal(1) 75 | // note that nothing is returned from this callback 76 | }) 77 | .then((num) => { 78 | // this callback receives the original unchanged value 1 79 | expect(num).to.equal(1) 80 | }) 81 | }) 82 | 83 | it('yields the value yielded by the last Cypress command inside', () => { 84 | cy.wrap(1) 85 | .then((num) => { 86 | expect(num).to.equal(1) 87 | // note how we run a Cypress command 88 | // the result yielded by this Cypress command 89 | // will be passed to the second ".then" 90 | cy.wrap(2) 91 | }) 92 | .then((num) => { 93 | // this callback receives the value yielded by "cy.wrap(2)" 94 | expect(num).to.equal(2) 95 | }) 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /lib/rules/assertion-before-screenshot.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assertionCommands = [ 4 | // assertions 5 | 'should', 6 | 'and', 7 | 'contains', 8 | 9 | // retries until it gets something 10 | 'get', 11 | 12 | // not an assertion, but unlikely to require waiting for render 13 | 'scrollIntoView', 14 | 'scrollTo', 15 | ] 16 | 17 | module.exports = { 18 | meta: { 19 | type: 'problem', 20 | docs: { 21 | description: 'require screenshots to be preceded by an assertion', 22 | category: 'Possible Errors', 23 | recommended: false, 24 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/assertion-before-screenshot.md', 25 | }, 26 | schema: [], 27 | messages: { 28 | unexpected: 'Make an assertion on the page state before taking a screenshot', 29 | }, 30 | }, 31 | create(context) { 32 | return { 33 | CallExpression(node) { 34 | if (isCallingCyScreenshot(node) && !isPreviousAnAssertion(node)) { 35 | context.report({ node, messageId: 'unexpected' }) 36 | } 37 | }, 38 | } 39 | }, 40 | } 41 | 42 | function isRootCypress(node) { 43 | while (node.type === 'CallExpression') { 44 | if (node.callee.type !== 'MemberExpression') return false 45 | 46 | if (node.callee.object.type === 'Identifier' 47 | && node.callee.object.name === 'cy') { 48 | return true 49 | } 50 | 51 | node = node.callee.object 52 | } 53 | 54 | return false 55 | } 56 | 57 | function getPreviousInChain(node) { 58 | return node.type === 'CallExpression' 59 | && node.callee.type === 'MemberExpression' 60 | && node.callee.object.type === 'CallExpression' 61 | && node.callee.object.callee.type === 'MemberExpression' 62 | && node.callee.object.callee.property.type === 'Identifier' 63 | && node.callee.object.callee.property.name 64 | } 65 | 66 | function getCallExpressionCypressCommand(node) { 67 | return isRootCypress(node) 68 | && node.callee.property.type === 'Identifier' 69 | && node.callee.property.name 70 | } 71 | 72 | function isCallingCyScreenshot(node) { 73 | return getCallExpressionCypressCommand(node) === 'screenshot' 74 | } 75 | 76 | function getPreviousCypressCommand(node) { 77 | const previousInChain = getPreviousInChain(node) 78 | 79 | if (previousInChain) { 80 | return previousInChain 81 | } 82 | 83 | while (node.parent && !node.parent.body) { 84 | node = node.parent 85 | } 86 | 87 | if (!node.parent || !node.parent.body) return null 88 | 89 | const body = node.parent.body.type === 'BlockStatement' ? node.parent.body.body : node.parent.body 90 | 91 | const index = body.indexOf(node) 92 | 93 | // in the case of a function declaration it won't be found 94 | if (index < 0) return null 95 | 96 | if (index === 0) return getPreviousCypressCommand(node.parent) 97 | 98 | const previousStatement = body[index - 1] 99 | 100 | if (previousStatement.type !== 'ExpressionStatement' 101 | || previousStatement.expression.type !== 'CallExpression') { 102 | return null 103 | } 104 | 105 | return getCallExpressionCypressCommand(previousStatement.expression) 106 | } 107 | 108 | function isPreviousAnAssertion(node) { 109 | const previousCypressCommand = getPreviousCypressCommand(node) 110 | 111 | return assertionCommands.indexOf(previousCypressCommand) >= 0 112 | } 113 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/utilities.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Utilities', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/utilities') 6 | }) 7 | 8 | it('Cypress._ - call a lodash method', () => { 9 | // https://on.cypress.io/_ 10 | cy.request('https://jsonplaceholder.cypress.io/users') 11 | .then((response) => { 12 | let ids = Cypress._.chain(response.body).map('id').take(3).value() 13 | 14 | expect(ids).to.deep.eq([1, 2, 3]) 15 | }) 16 | }) 17 | 18 | it('Cypress.$ - call a jQuery method', () => { 19 | // https://on.cypress.io/$ 20 | let $li = Cypress.$('.utility-jquery li:first') 21 | 22 | cy.wrap($li).should('not.have.class', 'active') 23 | cy.wrap($li).click() 24 | cy.wrap($li).should('have.class', 'active') 25 | }) 26 | 27 | it('Cypress.Blob - blob utilities and base64 string conversion', () => { 28 | // https://on.cypress.io/blob 29 | cy.get('.utility-blob').then(($div) => { 30 | // https://github.com/nolanlawson/blob-util#imgSrcToDataURL 31 | // get the dataUrl string for the javascript-logo 32 | return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous') 33 | .then((dataUrl) => { 34 | // create an element and set its src to the dataUrl 35 | let img = Cypress.$('', { src: dataUrl }) 36 | 37 | // need to explicitly return cy here since we are initially returning 38 | // the Cypress.Blob.imgSrcToDataURL promise to our test 39 | // append the image 40 | $div.append(img) 41 | 42 | cy.get('.utility-blob img').click() 43 | cy.get('.utility-blob img').should('have.attr', 'src', dataUrl) 44 | }) 45 | }) 46 | }) 47 | 48 | it('Cypress.minimatch - test out glob patterns against strings', () => { 49 | // https://on.cypress.io/minimatch 50 | let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', { 51 | matchBase: true, 52 | }) 53 | 54 | expect(matching, 'matching wildcard').to.be.true 55 | 56 | matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', { 57 | matchBase: true, 58 | }) 59 | 60 | expect(matching, 'comments').to.be.false 61 | 62 | // ** matches against all downstream path segments 63 | matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', { 64 | matchBase: true, 65 | }) 66 | 67 | expect(matching, 'comments').to.be.true 68 | 69 | // whereas * matches only the next path segment 70 | 71 | matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', { 72 | matchBase: false, 73 | }) 74 | 75 | expect(matching, 'comments').to.be.false 76 | }) 77 | 78 | it('Cypress.Promise - instantiate a bluebird promise', () => { 79 | // https://on.cypress.io/promise 80 | let waited = false 81 | 82 | /** 83 | * @return Bluebird 84 | */ 85 | function waitOneSecond () { 86 | // return a promise that resolves after 1 second 87 | return new Cypress.Promise((resolve, reject) => { 88 | setTimeout(() => { 89 | // set waited to true 90 | waited = true 91 | 92 | // resolve with 'foo' string 93 | resolve('foo') 94 | }, 1000) 95 | }) 96 | } 97 | 98 | cy.then(() => { 99 | // return a promise to cy.then() that 100 | // is awaited until it resolves 101 | return waitOneSecond().then((str) => { 102 | expect(str).to.eq('foo') 103 | expect(waited).to.be.true 104 | }) 105 | }) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /lib/rules/unsafe-to-chain-command.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { basename } = require('path') 4 | 5 | const NAME = basename(__dirname) 6 | const DESCRIPTION = 'disallow actions within chains' 7 | 8 | /** 9 | * Commands listed in the documentation with text: 'It is unsafe to chain further commands that rely on the subject after xxx.' 10 | * See {@link https://docs.cypress.io/app/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle Actions should be at the end of chains, not the middle} 11 | * for more information. 12 | * 13 | * @type {string[]} 14 | */ 15 | const unsafeToChainActions = [ 16 | 'blur', 17 | 'clear', 18 | 'click', 19 | 'check', 20 | 'dblclick', 21 | 'each', 22 | 'focus', 23 | 'rightclick', 24 | 'screenshot', 25 | 'scrollIntoView', 26 | 'scrollTo', 27 | 'select', 28 | 'selectFile', 29 | 'spread', 30 | 'submit', 31 | 'type', 32 | 'trigger', 33 | 'uncheck', 34 | 'within', 35 | ] 36 | 37 | /** 38 | * @type {import('eslint').Rule.RuleMetaData['schema']} 39 | */ 40 | const schema = { 41 | title: NAME, 42 | description: DESCRIPTION, 43 | type: 'object', 44 | properties: { 45 | methods: { 46 | type: 'array', 47 | description: 48 | 'An additional list of methods to check for unsafe chaining.', 49 | default: [], 50 | }, 51 | }, 52 | } 53 | 54 | /** 55 | * @param {import('eslint').Rule.RuleContext} context 56 | * @returns {Record} 57 | */ 58 | const getDefaultOptions = (context) => { 59 | return Object.entries(schema.properties).reduce((acc, [key, value]) => { 60 | if (!(value.default in value)) return acc 61 | 62 | return { 63 | ...acc, 64 | [key]: value.default, 65 | } 66 | }, context.options[0] || {}) 67 | } 68 | 69 | /** @type {import('eslint').Rule.RuleModule} */ 70 | module.exports = { 71 | meta: { 72 | type: 'problem', 73 | docs: { 74 | description: DESCRIPTION, 75 | category: 'Possible Errors', 76 | recommended: true, 77 | url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/unsafe-to-chain-command.md', 78 | }, 79 | schema: [schema], 80 | messages: { 81 | unexpected: 82 | 'It is unsafe to chain further commands that rely on the subject after this command. It is best to split the chain, chaining again from `cy.` in a next command line.', 83 | }, 84 | }, 85 | create(context) { 86 | const { methods } = getDefaultOptions(context) 87 | 88 | return { 89 | CallExpression(node) { 90 | if ( 91 | isRootCypress(node) 92 | && isActionUnsafeToChain(node, methods) 93 | && node.parent.type === 'MemberExpression' 94 | ) { 95 | context.report({ 96 | node, 97 | messageId: 'unexpected', 98 | }) 99 | } 100 | }, 101 | } 102 | }, 103 | } 104 | 105 | /** 106 | * @param {import('estree').Node} node 107 | * @returns {boolean} 108 | */ 109 | const isRootCypress = (node) => { 110 | if ( 111 | node.type !== 'CallExpression' 112 | || node.callee.type !== 'MemberExpression' 113 | ) { 114 | return false 115 | } 116 | 117 | if ( 118 | node.callee.object.type === 'Identifier' 119 | && node.callee.object.name === 'cy' 120 | ) { 121 | return true 122 | } 123 | 124 | return isRootCypress(node.callee.object) 125 | } 126 | 127 | /** 128 | * @param {import('estree').Node} node 129 | * @param {(string | RegExp)[]} additionalMethods 130 | */ 131 | const isActionUnsafeToChain = (node, additionalMethods = []) => { 132 | const unsafeActionsRegex = new RegExp([ 133 | ...unsafeToChainActions.map((action) => `^${action}$`), 134 | ...additionalMethods.map((method) => method instanceof RegExp ? method.source : method), 135 | ].join('|')) 136 | 137 | return ( 138 | node.callee 139 | && node.callee.property 140 | && node.callee.property.type === 'Identifier' 141 | && unsafeActionsRegex.test(node.callee.property.name) 142 | ) 143 | } 144 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/querying.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Querying', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/querying') 6 | }) 7 | 8 | // The most commonly used query is 'cy.get()', you can 9 | // think of this like the '$' in jQuery 10 | 11 | it('cy.get() - query DOM elements', () => { 12 | // https://on.cypress.io/get 13 | 14 | cy.get('#query-btn').should('contain', 'Button') 15 | 16 | cy.get('.query-btn').should('contain', 'Button') 17 | 18 | cy.get('#querying .well>button:first').should('contain', 'Button') 19 | // ↲ 20 | // Use CSS selectors just like jQuery 21 | 22 | cy.get('[data-test-id="test-example"]').should('have.class', 'example') 23 | 24 | // 'cy.get()' yields jQuery object, you can get its attribute 25 | // by invoking `.attr()` method 26 | cy.get('[data-test-id="test-example"]') 27 | .invoke('attr', 'data-test-id') 28 | .should('equal', 'test-example') 29 | 30 | // or you can get element's CSS property 31 | cy.get('[data-test-id="test-example"]') 32 | .invoke('css', 'position') 33 | .should('equal', 'static') 34 | 35 | // or use assertions directly during 'cy.get()' 36 | // https://on.cypress.io/assertions 37 | cy.get('[data-test-id="test-example"]') 38 | .should('have.attr', 'data-test-id', 'test-example') 39 | .and('have.css', 'position', 'static') 40 | }) 41 | 42 | it('cy.contains() - query DOM elements with matching content', () => { 43 | // https://on.cypress.io/contains 44 | cy.get('.query-list') 45 | .contains('bananas') 46 | .should('have.class', 'third') 47 | 48 | // we can pass a regexp to `.contains()` 49 | cy.get('.query-list') 50 | .contains(/^b\w+/) 51 | .should('have.class', 'third') 52 | 53 | cy.get('.query-list') 54 | .contains('apples') 55 | .should('have.class', 'first') 56 | 57 | // passing a selector to contains will 58 | // yield the selector containing the text 59 | cy.get('#querying') 60 | .contains('ul', 'oranges') 61 | .should('have.class', 'query-list') 62 | 63 | cy.get('.query-button') 64 | .contains('Save Form') 65 | .should('have.class', 'btn') 66 | }) 67 | 68 | it('.within() - query DOM elements within a specific element', () => { 69 | // https://on.cypress.io/within 70 | cy.get('.query-form').within(() => { 71 | cy.get('input:first').should('have.attr', 'placeholder', 'Email') 72 | cy.get('input:last').should('have.attr', 'placeholder', 'Password') 73 | }) 74 | }) 75 | 76 | it('cy.root() - query the root DOM element', () => { 77 | // https://on.cypress.io/root 78 | 79 | // By default, root is the document 80 | cy.root().should('match', 'html') 81 | 82 | cy.get('.query-ul').within(() => { 83 | // In this within, the root is now the ul DOM element 84 | cy.root().should('have.class', 'query-ul') 85 | }) 86 | }) 87 | 88 | it('best practices - selecting elements', () => { 89 | // https://on.cypress.io/best-practices#Selecting-Elements 90 | cy.get('[data-cy=best-practices-selecting-elements]').within(() => { 91 | // Worst - too generic, no context 92 | cy.get('button').click() 93 | 94 | // Bad. Coupled to styling. Highly subject to change. 95 | cy.get('.btn.btn-large').click() 96 | 97 | // Average. Coupled to the `name` attribute which has HTML semantics. 98 | cy.get('[name=submission]').click() 99 | 100 | // Better. But still coupled to styling or JS event listeners. 101 | cy.get('#main').click() 102 | 103 | // Slightly better. Uses an ID but also ensures the element 104 | // has an ARIA role attribute 105 | cy.get('#main[role=button]').click() 106 | 107 | // Much better. But still coupled to text content that may change. 108 | cy.contains('Submit').click() 109 | 110 | // Best. Insulated from all changes. 111 | cy.get('[data-cy=submit]').click() 112 | }) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | docker-executor: 5 | docker: 6 | - image: cimg/node:24.11.0 7 | resource_class: medium 8 | 9 | workflows: 10 | main: 11 | jobs: 12 | - lint 13 | - test-v9 14 | 15 | - build-test-project 16 | - test-test-project: 17 | matrix: 18 | parameters: 19 | eslint-version: ['9'] 20 | config-file: 21 | # configurations correspond to examples in README 22 | - 'globals' 23 | - 'one-rule-deprecated' # using deprecated /flat 24 | - 'one-rule' 25 | - 'recommended' 26 | requires: 27 | - build-test-project 28 | - release: 29 | context: org-npm-credentials 30 | requires: 31 | - lint 32 | - test-v9 33 | - test-test-project 34 | filters: 35 | branches: 36 | only: 37 | - master 38 | 39 | jobs: 40 | lint: 41 | executor: docker-executor 42 | steps: 43 | - checkout 44 | - run: 45 | name: Install dependencies 46 | command: npm ci 47 | - run: 48 | name: Show ESLint version 49 | command: npx eslint --version 50 | - run: 51 | name: Lint code with ESLint 52 | command: npm run lint 53 | - run: 54 | name: Lint code with Prettier 55 | command: npm run format:check 56 | 57 | test-v9: 58 | executor: docker-executor 59 | steps: 60 | - checkout 61 | - run: 62 | name: Install dependencies 63 | command: npm ci 64 | - run: 65 | name: Install ESLint 9 66 | command: npm install eslint@9 67 | - run: 68 | name: Show ESLint version 69 | command: npx eslint --version 70 | - run: 71 | name: Test ESLint 9 72 | command: npm test 73 | 74 | build-test-project: 75 | executor: docker-executor 76 | steps: 77 | - checkout 78 | - run: 79 | name: Install dependencies 80 | command: npm ci 81 | - run: 82 | name: Build tarball 83 | command: npm pack 84 | - run: 85 | name: Get version 86 | command: | 87 | echo "PLUGIN_VERSION=$(jq -r '.version' package.json)" >> $BASH_ENV 88 | cp $BASH_ENV bash.env 89 | - persist_to_workspace: 90 | root: . 91 | paths: 92 | - eslint-plugin-cypress-*.tgz 93 | - bash.env 94 | 95 | test-test-project: 96 | description: Run ESLint with different configurations 97 | parameters: 98 | eslint-version: 99 | description: Version of ESLint to use 100 | default: 'latest' 101 | type: string 102 | config-file: 103 | description: Configuration file 104 | default: 'default' 105 | type: string 106 | executor: docker-executor 107 | working_directory: ./test-project 108 | steps: 109 | - checkout: 110 | path: ../ 111 | - attach_workspace: 112 | at: . 113 | - run: 114 | name: Get plugin version 115 | command: | 116 | cat bash.env >> $BASH_ENV 117 | - run: 118 | name: Install dependencies 119 | command: | 120 | npm install eslint@<< parameters.eslint-version>> ./eslint-plugin-cypress-$PLUGIN_VERSION.tgz -D 121 | - run: 122 | name: Display ESLint version 123 | command: | 124 | npx eslint --version 125 | - run: echo Testing a << parameters.config-file >> configuration 126 | - run: 127 | name: Lint with example configuration 128 | command: | 129 | npx eslint --config ./eslint-configs/eslint.<< parameters.config-file >>.mjs . 130 | 131 | release: 132 | executor: docker-executor 133 | steps: 134 | - checkout 135 | - run: 136 | name: Install dependencies 137 | command: npm ci 138 | - run: 139 | name: Run semantic release 140 | command: npm run semantic-release 141 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/traversal.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Traversal', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/traversal') 6 | }) 7 | 8 | it('.children() - get child DOM elements', () => { 9 | // https://on.cypress.io/children 10 | cy.get('.traversal-breadcrumb') 11 | .children('.active') 12 | .should('contain', 'Data') 13 | }) 14 | 15 | it('.closest() - get closest ancestor DOM element', () => { 16 | // https://on.cypress.io/closest 17 | cy.get('.traversal-badge') 18 | .closest('ul') 19 | .should('have.class', 'list-group') 20 | }) 21 | 22 | it('.eq() - get a DOM element at a specific index', () => { 23 | // https://on.cypress.io/eq 24 | cy.get('.traversal-list>li') 25 | .eq(1).should('contain', 'siamese') 26 | }) 27 | 28 | it('.filter() - get DOM elements that match the selector', () => { 29 | // https://on.cypress.io/filter 30 | cy.get('.traversal-nav>li') 31 | .filter('.active').should('contain', 'About') 32 | }) 33 | 34 | it('.find() - get descendant DOM elements of the selector', () => { 35 | // https://on.cypress.io/find 36 | cy.get('.traversal-pagination') 37 | .find('li').find('a') 38 | .should('have.length', 7) 39 | }) 40 | 41 | it('.first() - get first DOM element', () => { 42 | // https://on.cypress.io/first 43 | cy.get('.traversal-table td') 44 | .first().should('contain', '1') 45 | }) 46 | 47 | it('.last() - get last DOM element', () => { 48 | // https://on.cypress.io/last 49 | cy.get('.traversal-buttons .btn') 50 | .last().should('contain', 'Submit') 51 | }) 52 | 53 | it('.next() - get next sibling DOM element', () => { 54 | // https://on.cypress.io/next 55 | cy.get('.traversal-ul') 56 | .contains('apples').next().should('contain', 'oranges') 57 | }) 58 | 59 | it('.nextAll() - get all next sibling DOM elements', () => { 60 | // https://on.cypress.io/nextall 61 | cy.get('.traversal-next-all') 62 | .contains('oranges') 63 | .nextAll().should('have.length', 3) 64 | }) 65 | 66 | it('.nextUntil() - get next sibling DOM elements until next el', () => { 67 | // https://on.cypress.io/nextuntil 68 | cy.get('#veggies') 69 | .nextUntil('#nuts').should('have.length', 3) 70 | }) 71 | 72 | it('.not() - remove DOM elements from set of DOM elements', () => { 73 | // https://on.cypress.io/not 74 | cy.get('.traversal-disabled .btn') 75 | .not('[disabled]').should('not.contain', 'Disabled') 76 | }) 77 | 78 | it('.parent() - get parent DOM element from DOM elements', () => { 79 | // https://on.cypress.io/parent 80 | cy.get('.traversal-mark') 81 | .parent().should('contain', 'Morbi leo risus') 82 | }) 83 | 84 | it('.parents() - get parent DOM elements from DOM elements', () => { 85 | // https://on.cypress.io/parents 86 | cy.get('.traversal-cite') 87 | .parents().should('match', 'blockquote') 88 | }) 89 | 90 | it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => { 91 | // https://on.cypress.io/parentsuntil 92 | cy.get('.clothes-nav') 93 | .find('.active') 94 | .parentsUntil('.clothes-nav') 95 | .should('have.length', 2) 96 | }) 97 | 98 | it('.prev() - get previous sibling DOM element', () => { 99 | // https://on.cypress.io/prev 100 | cy.get('.birds').find('.active') 101 | .prev().should('contain', 'Lorikeets') 102 | }) 103 | 104 | it('.prevAll() - get all previous sibling DOM elements', () => { 105 | // https://on.cypress.io/prevall 106 | cy.get('.fruits-list').find('.third') 107 | .prevAll().should('have.length', 2) 108 | }) 109 | 110 | it('.prevUntil() - get all previous sibling DOM elements until el', () => { 111 | // https://on.cypress.io/prevuntil 112 | cy.get('.foods-list').find('#nuts') 113 | .prevUntil('#veggies').should('have.length', 3) 114 | }) 115 | 116 | it('.siblings() - get all sibling DOM elements', () => { 117 | // https://on.cypress.io/siblings 118 | cy.get('.traversal-pills .active') 119 | .siblings().should('have.length', 2) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to cypress-io/eslint-plugin-cypress 2 | 3 | Thanks for taking the time to contribute! :smile: 4 | 5 | ## Preparation 6 | 7 | - Fork and clone this repository 8 | - Branch from the default `master` branch using a descriptive new branch name 9 | - Install dependencies with `npm ci` 10 | 11 | ## Rule references 12 | 13 | - Refer to the [ESLint documentation](https://eslint.org/docs/latest/) and the [Custom Rules](https://eslint.org/docs/latest/extend/custom-rules) page 14 | 15 | ## New rule 16 | 17 | To add a new rule: 18 | 19 | - Follow the instructions in the ESLint [generator-eslint](https://www.npmjs.com/package/generator-eslint) documentation to install [Yeoman](https://www.npmjs.com/package/yo) and the generator 20 | - Run the new rule generator `yo eslint:rule` and answer the questions 21 | - select "ESLint Plugin" 22 | - for "Type a short description of this rule" provide text which starts with one of "enforce", "require" or "disallow" (all lower case) 23 | - Yeoman creates three boilerplate files: 24 | - `docs/rules/.md` 25 | - `lib/rules/.js` 26 | - `test/rules/.js` 27 | - Run `npm run lint-fix` 28 | - Address the linting errors by editing `lib/rules/.js` 29 | - Add a `meta.messages` property (see [MessageIds](https://eslint.org/docs/latest/extend/custom-rules#messageids)) 30 | - Select the appropriate `meta.type` property using `problem`, `suggestion`, or `layout` 31 | - Complete the new rule by adding content to the three files previously created 32 | - Run `eslint-doc-generator` to generate automated documentation sections (see [Document generation](#document-generation) below) 33 | - Review documentation changes 34 | - Run `npm run lint` 35 | - Run `npm run format` 36 | - Run `npm test` to run [Vitest](https://vitest.dev/) 37 | - Make sure all tests are passing 38 | - Add the rule to [flat.js](https://github.com/cypress-io/eslint-plugin-cypress/blob/master/lib/flat.js) 39 | - Create a git commit with a commit message similar to: `feat: add rule ` (see [commit message conventions](https://github.com/semantic-release/semantic-release#commit-message-format)) 40 | - Create a PR from your branch 41 | 42 | ## Document generation 43 | 44 | This plugin uses the ESLint [eslint-doc-generator](https://www.npmjs.com/package/eslint-doc-generator) to generate consistent documentation. 45 | 46 | - Install with `npm install eslint-doc-generator -g` 47 | - Run `eslint-doc-generator` in the root directory of the plugin 48 | 49 | ## Merging pull requests 50 | 51 | This information is for Cypress.io Members or Collaborators who merge pull requests: 52 | 53 | 1. When merging a pull request, use the [Squash and merge](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github#squashing-your-merge-commits) option to squash all commits into one. 54 | 1. Make sure the commit subject and body follow [semantic commit convention](https://semantic-release.gitbook.io/semantic-release/#commit-message-format), for instance: 55 | 56 | ```text 57 | feat: added new parameter 58 | fix: fixed a bug 59 | ``` 60 | 61 | If you need to bump the major version, mark it as breaking change in the body of the commit's message like: 62 | 63 | ```text 64 | fix: upgrade dependency X 65 | 66 | BREAKING CHANGE: requires minimum Node.js 20 to run 67 | ``` 68 | 69 | 1. New versions of this module will be released automatically by the CI pipeline when any PR with a triggering commit message is merged to the `master` branch: see the `release` job of [circle.yml](circle.yml). 70 | This will create a new [GitHub release](https://github.com/cypress-io/eslint-plugin-cypress/releases) and publish it to [eslint-plugin-cypress](https://www.npmjs.com/package/eslint-plugin-cypress) on the [npm registry](https://docs.npmjs.com/about-the-public-npm-registry). 71 | 1. The module's CI is configured to use the [default Angular release rules](https://github.com/semantic-release/commit-analyzer/blob/master/lib/default-release-rules.js). 72 | This means that only `feat:`, `fix:` and `perf:` trigger a new release. 73 | Other Angular commit types listed in the [Angular commit message guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) can be used for documentation purposes, however they are ignored by the currently configured release process. 74 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/cookies.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Cookies', () => { 4 | beforeEach(() => { 5 | Cypress.Cookies.debug(true) 6 | 7 | cy.visit('https://example.cypress.io/commands/cookies') 8 | 9 | // clear cookies again after visiting to remove 10 | // any 3rd party cookies picked up such as cloudflare 11 | cy.clearCookies() 12 | }) 13 | 14 | it('cy.getCookie() - get a browser cookie', () => { 15 | // https://on.cypress.io/getcookie 16 | cy.get('#getCookie .set-a-cookie').click() 17 | 18 | // cy.getCookie() yields a cookie object 19 | cy.getCookie('token').should('have.property', 'value', '123ABC') 20 | }) 21 | 22 | it('cy.getCookies() - get browser cookies for the current domain', () => { 23 | // https://on.cypress.io/getcookies 24 | cy.getCookies().should('be.empty') 25 | 26 | cy.get('#getCookies .set-a-cookie').click() 27 | 28 | // cy.getCookies() yields an array of cookies 29 | cy.getCookies().should('have.length', 1).should((cookies) => { 30 | // each cookie has these properties 31 | expect(cookies[0]).to.have.property('name', 'token') 32 | expect(cookies[0]).to.have.property('value', '123ABC') 33 | expect(cookies[0]).to.have.property('httpOnly', false) 34 | expect(cookies[0]).to.have.property('secure', false) 35 | expect(cookies[0]).to.have.property('domain') 36 | expect(cookies[0]).to.have.property('path') 37 | }) 38 | }) 39 | 40 | it('cy.getAllCookies() - get all browser cookies', () => { 41 | // https://on.cypress.io/getallcookies 42 | cy.getAllCookies().should('be.empty') 43 | 44 | cy.setCookie('key', 'value') 45 | cy.setCookie('key', 'value', { domain: '.example.com' }) 46 | 47 | // cy.getAllCookies() yields an array of cookies 48 | cy.getAllCookies().should('have.length', 2).should((cookies) => { 49 | // each cookie has these properties 50 | expect(cookies[0]).to.have.property('name', 'key') 51 | expect(cookies[0]).to.have.property('value', 'value') 52 | expect(cookies[0]).to.have.property('httpOnly', false) 53 | expect(cookies[0]).to.have.property('secure', false) 54 | expect(cookies[0]).to.have.property('domain') 55 | expect(cookies[0]).to.have.property('path') 56 | 57 | expect(cookies[1]).to.have.property('name', 'key') 58 | expect(cookies[1]).to.have.property('value', 'value') 59 | expect(cookies[1]).to.have.property('httpOnly', false) 60 | expect(cookies[1]).to.have.property('secure', false) 61 | expect(cookies[1]).to.have.property('domain', '.example.com') 62 | expect(cookies[1]).to.have.property('path') 63 | }) 64 | }) 65 | 66 | it('cy.setCookie() - set a browser cookie', () => { 67 | // https://on.cypress.io/setcookie 68 | cy.getCookies().should('be.empty') 69 | 70 | cy.setCookie('foo', 'bar') 71 | 72 | // cy.getCookie() yields a cookie object 73 | cy.getCookie('foo').should('have.property', 'value', 'bar') 74 | }) 75 | 76 | it('cy.clearCookie() - clear a browser cookie', () => { 77 | // https://on.cypress.io/clearcookie 78 | cy.getCookie('token').should('be.null') 79 | 80 | cy.get('#clearCookie .set-a-cookie').click() 81 | 82 | cy.getCookie('token').should('have.property', 'value', '123ABC') 83 | 84 | // cy.clearCookies() yields null 85 | cy.clearCookie('token') 86 | 87 | cy.getCookie('token').should('be.null') 88 | }) 89 | 90 | it('cy.clearCookies() - clear browser cookies for the current domain', () => { 91 | // https://on.cypress.io/clearcookies 92 | cy.getCookies().should('be.empty') 93 | 94 | cy.get('#clearCookies .set-a-cookie').click() 95 | 96 | cy.getCookies().should('have.length', 1) 97 | 98 | // cy.clearCookies() yields null 99 | cy.clearCookies() 100 | 101 | cy.getCookies().should('be.empty') 102 | }) 103 | 104 | it('cy.clearAllCookies() - clear all browser cookies', () => { 105 | // https://on.cypress.io/clearallcookies 106 | cy.getAllCookies().should('be.empty') 107 | 108 | cy.setCookie('key', 'value') 109 | cy.setCookie('key', 'value', { domain: '.example.com' }) 110 | 111 | cy.getAllCookies().should('have.length', 2) 112 | 113 | // cy.clearAllCookies() yields null 114 | cy.clearAllCookies() 115 | 116 | cy.getAllCookies().should('be.empty') 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/storage.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Local Storage / Session Storage', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/storage') 6 | }) 7 | // Although localStorage is automatically cleared 8 | // in between tests to maintain a clean state 9 | // sometimes we need to clear localStorage manually 10 | 11 | it('cy.clearLocalStorage() - clear all data in localStorage for the current origin', () => { 12 | // https://on.cypress.io/clearlocalstorage 13 | cy.get('.ls-btn').click() 14 | cy.get('.ls-btn').should(() => { 15 | expect(localStorage.getItem('prop1')).to.eq('red') 16 | expect(localStorage.getItem('prop2')).to.eq('blue') 17 | expect(localStorage.getItem('prop3')).to.eq('magenta') 18 | }) 19 | 20 | cy.clearLocalStorage() 21 | cy.getAllLocalStorage().should(() => { 22 | expect(localStorage.getItem('prop1')).to.be.null 23 | expect(localStorage.getItem('prop2')).to.be.null 24 | expect(localStorage.getItem('prop3')).to.be.null 25 | }) 26 | 27 | cy.get('.ls-btn').click() 28 | cy.get('.ls-btn').should(() => { 29 | expect(localStorage.getItem('prop1')).to.eq('red') 30 | expect(localStorage.getItem('prop2')).to.eq('blue') 31 | expect(localStorage.getItem('prop3')).to.eq('magenta') 32 | }) 33 | 34 | // Clear key matching string in localStorage 35 | cy.clearLocalStorage('prop1') 36 | cy.getAllLocalStorage().should(() => { 37 | expect(localStorage.getItem('prop1')).to.be.null 38 | expect(localStorage.getItem('prop2')).to.eq('blue') 39 | expect(localStorage.getItem('prop3')).to.eq('magenta') 40 | }) 41 | 42 | cy.get('.ls-btn').click() 43 | cy.get('.ls-btn').should(() => { 44 | expect(localStorage.getItem('prop1')).to.eq('red') 45 | expect(localStorage.getItem('prop2')).to.eq('blue') 46 | expect(localStorage.getItem('prop3')).to.eq('magenta') 47 | }) 48 | 49 | // Clear keys matching regex in localStorage 50 | cy.clearLocalStorage(/prop1|2/) 51 | cy.getAllLocalStorage().should(() => { 52 | expect(localStorage.getItem('prop1')).to.be.null 53 | expect(localStorage.getItem('prop2')).to.be.null 54 | expect(localStorage.getItem('prop3')).to.eq('magenta') 55 | }) 56 | }) 57 | 58 | it('cy.getAllLocalStorage() - get all data in localStorage for all origins', () => { 59 | // https://on.cypress.io/getalllocalstorage 60 | cy.get('.ls-btn').click() 61 | 62 | // getAllLocalStorage() yields a map of origins to localStorage values 63 | cy.getAllLocalStorage().should((storageMap) => { 64 | expect(storageMap).to.deep.equal({ 65 | // other origins will also be present if localStorage is set on them 66 | 'https://example.cypress.io': { 67 | prop1: 'red', 68 | prop2: 'blue', 69 | prop3: 'magenta', 70 | }, 71 | }) 72 | }) 73 | }) 74 | 75 | it('cy.clearAllLocalStorage() - clear all data in localStorage for all origins', () => { 76 | // https://on.cypress.io/clearalllocalstorage 77 | cy.get('.ls-btn').click() 78 | 79 | // clearAllLocalStorage() yields null 80 | cy.clearAllLocalStorage() 81 | cy.getAllLocalStorage().should(() => { 82 | expect(localStorage.getItem('prop1')).to.be.null 83 | expect(localStorage.getItem('prop2')).to.be.null 84 | expect(localStorage.getItem('prop3')).to.be.null 85 | }) 86 | }) 87 | 88 | it('cy.getAllSessionStorage() - get all data in sessionStorage for all origins', () => { 89 | // https://on.cypress.io/getallsessionstorage 90 | cy.get('.ls-btn').click() 91 | 92 | // getAllSessionStorage() yields a map of origins to sessionStorage values 93 | cy.getAllSessionStorage().should((storageMap) => { 94 | expect(storageMap).to.deep.equal({ 95 | // other origins will also be present if sessionStorage is set on them 96 | 'https://example.cypress.io': { 97 | prop4: 'cyan', 98 | prop5: 'yellow', 99 | prop6: 'black', 100 | }, 101 | }) 102 | }) 103 | }) 104 | 105 | it('cy.clearAllSessionStorage() - clear all data in sessionStorage for all origins', () => { 106 | // https://on.cypress.io/clearallsessionstorage 107 | cy.get('.ls-btn').click() 108 | 109 | // clearAllSessionStorage() yields null 110 | cy.clearAllSessionStorage() 111 | cy.getAllSessionStorage().should(() => { 112 | expect(sessionStorage.getItem('prop4')).to.be.null 113 | expect(sessionStorage.getItem('prop5')).to.be.null 114 | expect(sessionStorage.getItem('prop6')).to.be.null 115 | }) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/cypress_api.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Cypress APIs', () => { 4 | context('Cypress.Commands', () => { 5 | beforeEach(() => { 6 | cy.visit('https://example.cypress.io/cypress-api') 7 | }) 8 | 9 | // https://on.cypress.io/custom-commands 10 | 11 | it('.add() - create a custom command', () => { 12 | Cypress.Commands.add('console', { 13 | prevSubject: true, 14 | }, (subject, method) => { 15 | // the previous subject is automatically received 16 | // and the commands arguments are shifted 17 | 18 | // allow us to change the console method used 19 | method = method || 'log' 20 | 21 | // log the subject to the console 22 | console[method]('The subject is', subject) 23 | 24 | // whatever we return becomes the new subject 25 | // we don't want to change the subject so 26 | // we return whatever was passed in 27 | return subject 28 | }) 29 | 30 | cy.get('button').console('info').then(($button) => { 31 | // subject is still $button 32 | }) 33 | }) 34 | }) 35 | 36 | context('Cypress.Cookies', () => { 37 | beforeEach(() => { 38 | cy.visit('https://example.cypress.io/cypress-api') 39 | }) 40 | 41 | // https://on.cypress.io/cookies 42 | it('.debug() - enable or disable debugging', () => { 43 | Cypress.Cookies.debug(true) 44 | 45 | // Cypress will now log in the console when 46 | // cookies are set or cleared 47 | cy.setCookie('fakeCookie', '123ABC') 48 | cy.clearCookie('fakeCookie') 49 | cy.setCookie('fakeCookie', '123ABC') 50 | cy.clearCookie('fakeCookie') 51 | cy.setCookie('fakeCookie', '123ABC') 52 | }) 53 | }) 54 | 55 | context('Cypress.arch', () => { 56 | beforeEach(() => { 57 | cy.visit('https://example.cypress.io/cypress-api') 58 | }) 59 | 60 | it('Get CPU architecture name of underlying OS', () => { 61 | // https://on.cypress.io/arch 62 | expect(Cypress.arch).to.exist 63 | }) 64 | }) 65 | 66 | context('Cypress.config()', () => { 67 | beforeEach(() => { 68 | cy.visit('https://example.cypress.io/cypress-api') 69 | }) 70 | 71 | it('Get and set configuration options', () => { 72 | // https://on.cypress.io/config 73 | let myConfig = Cypress.config() 74 | 75 | expect(myConfig).to.have.property('animationDistanceThreshold', 5) 76 | expect(myConfig).to.have.property('baseUrl', null) 77 | expect(myConfig).to.have.property('defaultCommandTimeout', 4000) 78 | expect(myConfig).to.have.property('requestTimeout', 5000) 79 | expect(myConfig).to.have.property('responseTimeout', 30000) 80 | expect(myConfig).to.have.property('viewportHeight', 660) 81 | expect(myConfig).to.have.property('viewportWidth', 1000) 82 | expect(myConfig).to.have.property('pageLoadTimeout', 60000) 83 | expect(myConfig).to.have.property('waitForAnimations', true) 84 | 85 | expect(Cypress.config('pageLoadTimeout')).to.eq(60000) 86 | 87 | // this will change the config for the rest of your tests! 88 | Cypress.config('pageLoadTimeout', 20000) 89 | 90 | expect(Cypress.config('pageLoadTimeout')).to.eq(20000) 91 | 92 | Cypress.config('pageLoadTimeout', 60000) 93 | }) 94 | }) 95 | 96 | context('Cypress.dom', () => { 97 | beforeEach(() => { 98 | cy.visit('https://example.cypress.io/cypress-api') 99 | }) 100 | 101 | // https://on.cypress.io/dom 102 | it('.isHidden() - determine if a DOM element is hidden', () => { 103 | let hiddenP = Cypress.$('.dom-p p.hidden').get(0) 104 | let visibleP = Cypress.$('.dom-p p.visible').get(0) 105 | 106 | // our first paragraph has css class 'hidden' 107 | expect(Cypress.dom.isHidden(hiddenP)).to.be.true 108 | expect(Cypress.dom.isHidden(visibleP)).to.be.false 109 | }) 110 | }) 111 | 112 | context('Cypress.env()', () => { 113 | beforeEach(() => { 114 | cy.visit('https://example.cypress.io/cypress-api') 115 | }) 116 | 117 | // We can set environment variables for highly dynamic values 118 | 119 | // https://on.cypress.io/environment-variables 120 | it('Get environment variables', () => { 121 | // https://on.cypress.io/env 122 | // set multiple environment variables 123 | Cypress.env({ 124 | host: 'veronica.dev.local', 125 | api_server: 'http://localhost:8888/v1/', 126 | }) 127 | 128 | // get environment variable 129 | expect(Cypress.env('host')).to.eq('veronica.dev.local') 130 | 131 | // set environment variable 132 | Cypress.env('api_server', 'http://localhost:8888/v2/') 133 | expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/') 134 | 135 | // get all environment variable 136 | expect(Cypress.env()).to.have.property('host', 'veronica.dev.local') 137 | expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/') 138 | }) 139 | }) 140 | 141 | context('Cypress.log', () => { 142 | beforeEach(() => { 143 | cy.visit('https://example.cypress.io/cypress-api') 144 | }) 145 | 146 | it('Control what is printed to the Command Log', () => { 147 | // https://on.cypress.io/cypress-log 148 | }) 149 | }) 150 | 151 | context('Cypress.platform', () => { 152 | beforeEach(() => { 153 | cy.visit('https://example.cypress.io/cypress-api') 154 | }) 155 | 156 | it('Get underlying OS name', () => { 157 | // https://on.cypress.io/platform 158 | expect(Cypress.platform).to.be.exist 159 | }) 160 | }) 161 | 162 | context('Cypress.version', () => { 163 | beforeEach(() => { 164 | cy.visit('https://example.cypress.io/cypress-api') 165 | }) 166 | 167 | it('Get current version of Cypress being run', () => { 168 | // https://on.cypress.io/version 169 | expect(Cypress.version).to.be.exist 170 | }) 171 | }) 172 | 173 | context('Cypress.spec', () => { 174 | beforeEach(() => { 175 | cy.visit('https://example.cypress.io/cypress-api') 176 | }) 177 | 178 | it('Get current spec information', () => { 179 | // https://on.cypress.io/spec 180 | // wrap the object so we can inspect it easily by clicking in the command log 181 | cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute']) 182 | }) 183 | }) 184 | }) 185 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/2-advanced-examples/assertions.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Assertions', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/assertions') 6 | }) 7 | 8 | describe('Implicit Assertions', () => { 9 | it('.should() - make an assertion about the current subject', () => { 10 | // https://on.cypress.io/should 11 | cy.get('.assertion-table') 12 | .find('tbody tr:last') 13 | .should('have.class', 'success') 14 | .find('td') 15 | .first() 16 | // checking the text of the element in various ways 17 | .should('have.text', 'Column content') 18 | .should('contain', 'Column content') 19 | .should('have.html', 'Column content') 20 | // chai-jquery uses "is()" to check if element matches selector 21 | .should('match', 'td') 22 | // to match text content against a regular expression 23 | // first need to invoke jQuery method text() 24 | // and then match using regular expression 25 | .invoke('text') 26 | .should('match', /column content/i) 27 | 28 | // a better way to check element's text content against a regular expression 29 | // is to use "cy.contains" 30 | // https://on.cypress.io/contains 31 | cy.get('.assertion-table') 32 | .find('tbody tr:last') 33 | // finds first element with text content matching regular expression 34 | .contains('td', /column content/i) 35 | .should('be.visible') 36 | 37 | // for more information about asserting element's text 38 | // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents 39 | }) 40 | 41 | it('.and() - chain multiple assertions together', () => { 42 | // https://on.cypress.io/and 43 | cy.get('.assertions-link') 44 | .should('have.class', 'active') 45 | .and('have.attr', 'href') 46 | .and('include', 'cypress.io') 47 | }) 48 | }) 49 | 50 | describe('Explicit Assertions', () => { 51 | // https://on.cypress.io/assertions 52 | it('expect - make an assertion about a specified subject', () => { 53 | // We can use Chai's BDD style assertions 54 | expect(true).to.be.true 55 | const o = { foo: 'bar' } 56 | 57 | expect(o).to.equal(o) 58 | expect(o).to.deep.equal({ foo: 'bar' }) 59 | // matching text using regular expression 60 | expect('FooBar').to.match(/bar$/i) 61 | }) 62 | 63 | it('pass your own callback function to should()', () => { 64 | // Pass a function to should that can have any number 65 | // of explicit assertions within it. 66 | // The ".should(cb)" function will be retried 67 | // automatically until it passes all your explicit assertions or times out. 68 | cy.get('.assertions-p') 69 | .find('p') 70 | .should(($p) => { 71 | // https://on.cypress.io/$ 72 | // return an array of texts from all of the p's 73 | const texts = $p.map((i, el) => Cypress.$(el).text()) 74 | 75 | // jquery map returns jquery object 76 | // and .get() convert this to simple array 77 | const paragraphs = texts.get() 78 | 79 | // array should have length of 3 80 | expect(paragraphs, 'has 3 paragraphs').to.have.length(3) 81 | 82 | // use second argument to expect(...) to provide clear 83 | // message with each assertion 84 | expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([ 85 | 'Some text from first p', 86 | 'More text from second p', 87 | 'And even more text from third p', 88 | ]) 89 | }) 90 | }) 91 | 92 | it('finds element by class name regex', () => { 93 | cy.get('.docs-header') 94 | .find('div') 95 | // .should(cb) callback function will be retried 96 | .should(($div) => { 97 | expect($div).to.have.length(1) 98 | 99 | const className = $div[0].className 100 | 101 | expect(className).to.match(/heading-/) 102 | }) 103 | // .then(cb) callback is not retried, 104 | // it either passes or fails 105 | .then(($div) => { 106 | expect($div, 'text content').to.have.text('Introduction') 107 | }) 108 | }) 109 | 110 | it('can throw any error', () => { 111 | cy.get('.docs-header') 112 | .find('div') 113 | .should(($div) => { 114 | if ($div.length !== 1) { 115 | // you can throw your own errors 116 | throw new Error('Did not find 1 element') 117 | } 118 | 119 | const className = $div[0].className 120 | 121 | if (!className.match(/heading-/)) { 122 | throw new Error(`Could not find class "heading-" in ${className}`) 123 | } 124 | }) 125 | }) 126 | 127 | it('matches unknown text between two elements', () => { 128 | /** 129 | * Text from the first element. 130 | * @type {string} 131 | */ 132 | let text 133 | 134 | /** 135 | * Normalizes passed text, 136 | * useful before comparing text with spaces and different capitalization. 137 | * @param {string} s Text to normalize 138 | */ 139 | const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase() 140 | 141 | cy.get('.two-elements') 142 | .find('.first') 143 | .then(($first) => { 144 | // save text from the first element 145 | text = normalizeText($first.text()) 146 | }) 147 | 148 | cy.get('.two-elements') 149 | .find('.second') 150 | .should(($div) => { 151 | // we can massage text before comparing 152 | const secondText = normalizeText($div.text()) 153 | 154 | expect(secondText, 'second text').to.equal(text) 155 | }) 156 | }) 157 | 158 | it('assert - assert shape of an object', () => { 159 | const person = { 160 | name: 'Joe', 161 | age: 20, 162 | } 163 | 164 | assert.isObject(person, 'value is object') 165 | }) 166 | 167 | it('retries the should callback until assertions pass', () => { 168 | cy.get('#random-number') 169 | .should(($div) => { 170 | const n = parseFloat($div.text()) 171 | 172 | expect(n).to.be.gte(1).and.be.lte(10) 173 | }) 174 | }) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /test-project/cypress/e2e/1-getting-started/todo.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // Welcome to Cypress! 4 | // 5 | // This spec file contains a variety of sample tests 6 | // for a todo list app that are designed to demonstrate 7 | // the power of writing tests in Cypress. 8 | // 9 | // To learn more about how Cypress works and 10 | // what makes it such an awesome testing tool, 11 | // please read our getting started guide: 12 | // https://on.cypress.io/introduction-to-cypress 13 | 14 | describe('example to-do app', () => { 15 | beforeEach(() => { 16 | // Cypress starts out with a blank slate for each test 17 | // so we must tell it to visit our website with the `cy.visit()` command. 18 | // Since we want to visit the same URL at the start of all our tests, 19 | // we include it in our beforeEach function so that it runs before each test 20 | cy.visit('https://example.cypress.io/todo') 21 | }) 22 | 23 | it('displays two todo items by default', () => { 24 | // We use the `cy.get()` command to get all elements that match the selector. 25 | // Then, we use `should` to assert that there are two matched items, 26 | // which are the two default items. 27 | cy.get('.todo-list li').should('have.length', 2) 28 | 29 | // We can go even further and check that the default todos each contain 30 | // the correct text. We use the `first` and `last` functions 31 | // to get just the first and last matched elements individually, 32 | // and then perform an assertion with `should`. 33 | cy.get('.todo-list li').first().should('have.text', 'Pay electric bill') 34 | cy.get('.todo-list li').last().should('have.text', 'Walk the dog') 35 | }) 36 | 37 | it('can add new todo items', () => { 38 | // We'll store our item text in a variable so we can reuse it 39 | const newItem = 'Feed the cat' 40 | 41 | // Let's get the input element and use the `type` command to 42 | // input our new list item. After typing the content of our item, 43 | // we need to type the enter key as well in order to submit the input. 44 | // This input has a data-test attribute so we'll use that to select the 45 | // element in accordance with best practices: 46 | // https://on.cypress.io/selecting-elements 47 | cy.get('[data-test=new-todo]').type(`${newItem}{enter}`) 48 | 49 | // Now that we've typed our new item, let's check that it actually was added to the list. 50 | // Since it's the newest item, it should exist as the last element in the list. 51 | // In addition, with the two default items, we should have a total of 3 elements in the list. 52 | // Since assertions yield the element that was asserted on, 53 | // we can chain both of these assertions together into a single statement. 54 | cy.get('.todo-list li') 55 | .should('have.length', 3) 56 | .last() 57 | .should('have.text', newItem) 58 | }) 59 | 60 | it('can check off an item as completed', () => { 61 | // In addition to using the `get` command to get an element by selector, 62 | // we can also use the `contains` command to get an element by its contents. 63 | // However, this will yield the