├── .gitignore ├── images ├── app.png └── missing.png ├── .prettierrc.json ├── renovate.json ├── cypress.config.js ├── cypress └── e2e │ ├── spec-missing.cy.js │ └── spec.cy.js ├── .github └── workflows │ ├── ci.yml │ └── badges.yml ├── package.json ├── src └── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/screenshots/ 3 | -------------------------------------------------------------------------------- /images/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-highlight/HEAD/images/app.png -------------------------------------------------------------------------------- /images/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-highlight/HEAD/images/missing.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "prHourlyLimit": 2, 5 | "updateNotScheduled": false, 6 | "timezone": "America/New_York", 7 | "schedule": ["after 10pm and before 5am on every weekday", "every weekend"], 8 | "masterIssue": true, 9 | "labels": ["type: dependencies", "renovate"] 10 | } 11 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | viewportHeight: 300, 6 | viewportWidth: 500, 7 | defaultBrowser: 'electron', 8 | e2e: { 9 | setupNodeEvents(on, config) {}, 10 | supportFile: false, 11 | baseUrl: 'https://todomvc-vercel-gleb-bahmutov.vercel.app/', 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /cypress/e2e/spec-missing.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import { highlightMissingTestIds } from '../../src' 5 | 6 | beforeEach(() => { 7 | cy.visit('/') 8 | cy.get('.todo-list').should('be.visible') 9 | }) 10 | 11 | it('highlights input elements without data-cy attribute', () => { 12 | // cy.get('input[data-cy=new-todo]').invoke('removeAttr', 'data-cy') 13 | // confirm there are input elements without data-cy attribute 14 | // cy.get('input:not([data-cy])') 15 | 16 | highlightMissingTestIds() 17 | }) 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-24.04 6 | steps: 7 | - name: Checkout 🛎 8 | uses: actions/checkout@v6 9 | 10 | - name: Run tests 🧪 11 | # https://github.com/cypress-io/github-action 12 | uses: cypress-io/github-action@v6 13 | 14 | - name: Semantic Release 🚀 15 | uses: cycjimmy/semantic-release-action@v4 16 | with: 17 | branch: main 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-highlight", 3 | "version": "0.0.0-development", 4 | "description": "Highlights all elements on the page with good test selectors by injecting a CSS rule", 5 | "main": "src/index.js", 6 | "files": [ 7 | "src" 8 | ], 9 | "scripts": { 10 | "test": "cypress run", 11 | "semantic-release": "semantic-release", 12 | "cy:open": "cypress open" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/bahmutov/cypress-highlight.git" 17 | }, 18 | "keywords": [ 19 | "cypress-plugin" 20 | ], 21 | "author": "Gleb Bahmutov ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/bahmutov/cypress-highlight/issues" 25 | }, 26 | "homepage": "https://github.com/bahmutov/cypress-highlight#readme", 27 | "devDependencies": { 28 | "cypress": "^15.0.0", 29 | "prettier": "^3.4.2", 30 | "semantic-release": "^25.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/badges.yml: -------------------------------------------------------------------------------- 1 | name: badges 2 | on: 3 | push: 4 | # update README badge only if the README file changes 5 | # or if the package.json file changes, or this file changes 6 | branches: 7 | - main 8 | paths: 9 | - README.md 10 | - package.json 11 | - .github/workflows/badges.yml 12 | schedule: 13 | # update badges every night 14 | # because we have a few badges that are linked 15 | # to the external repositories 16 | - cron: '0 3 * * *' 17 | 18 | jobs: 19 | badges: 20 | name: Badges 21 | runs-on: ubuntu-24.04 22 | steps: 23 | - name: Checkout 🛎 24 | uses: actions/checkout@v6 25 | 26 | - name: Update version badges 🏷 27 | run: npx -p dependency-version-badge update-badge cypress 28 | 29 | - name: Commit any changed files 💾 30 | uses: stefanzweifel/git-auto-commit-action@v7 31 | with: 32 | commit_message: Updated badges 33 | branch: main 34 | file_pattern: README.md 35 | -------------------------------------------------------------------------------- /cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import { highlight } from '../..' 5 | 6 | beforeEach(() => { 7 | cy.visit('/') 8 | }) 9 | 10 | it('loads an app', () => { 11 | cy.get('[data-cy]').should('have.length.gt', 2) 12 | highlight() 13 | cy.screenshot('highlights', { capture: 'runner' }) 14 | }) 15 | 16 | it('highlights specific selectors', () => { 17 | cy.get('[data-cy]').should('have.length.gt', 2) 18 | // you can highlight different elements 19 | highlight('button[data-cy]') 20 | cy.screenshot('highlights-buttons', { capture: 'runner' }) 21 | }) 22 | 23 | it('allows passing options', () => { 24 | highlight({ 25 | selectors: 'button[data-cy]', 26 | }) 27 | }) 28 | 29 | it.skip('fails on found element', () => { 30 | highlight({ 31 | selectors: 'button[data-cy]', 32 | failIfFound: true, 33 | }) 34 | }) 35 | 36 | it.skip('fails on not found element', () => { 37 | highlight({ 38 | selectors: 'button[data-cy2]', 39 | failIfNotFound: true, 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Options to the highlight function 3 | * @typedef {Object} HighlightOptions 4 | * @property {string|string[]} selectors - list of selectors to highlight 5 | * @property [failIfFound] boolean - if true, the test fails if the selector is found 6 | * @property [failIfNotFound] boolean - if true, the test fails if the selector is not found 7 | */ 8 | 9 | /** 10 | * @param {string} stylesheetTitle 11 | * @param {string} css 12 | */ 13 | function addStyles(stylesheetTitle, css) { 14 | cy.wrap(null, { log: false }).then(() => { 15 | // @ts-ignore 16 | const doc = cy.state('document') 17 | const head = doc.head 18 | const hasStyle = Cypress._.find(head.styleSheets, { 19 | title: stylesheetTitle, 20 | }) 21 | if (hasStyle) { 22 | return 23 | } 24 | 25 | const style = doc.createElement('style') 26 | style.title = stylesheetTitle 27 | style.type = 'text/css' 28 | 29 | style.appendChild(document.createTextNode(css)) 30 | head.appendChild(style) 31 | }) 32 | } 33 | 34 | /** 35 | * Highlights all elements on the page with good test selectors 36 | * like "data-cy" by injecting a CSS rule. 37 | * @example 38 | * import { highlight } from 'cypress-highlight' 39 | * cy.visit('/') 40 | * highlight() 41 | * cy.screenshot('highlighted', { capture: 'runner'} ) 42 | * @example 43 | * highlight({ selectors: ['[data-cy]', '[data-testid]'] }) 44 | * @param {string[]|HighlightOptions[]} selectors (optional) List of selectors to highlight 45 | * @example highlight('[data-testid]', '[data-cy]') 46 | */ 47 | function highlight(...selectors) { 48 | let failIfFound = false 49 | let failIfNotFound = false 50 | 51 | if (selectors.length === 1 && typeof selectors[0] === 'object') { 52 | // using an options object 53 | const options = selectors[0] 54 | selectors = options.selectors || options.selector 55 | if (typeof selectors === 'string') { 56 | selectors = [selectors] 57 | } 58 | 59 | failIfFound = Boolean(options.failIfFound) 60 | failIfNotFound = Boolean(options.failIfNotFound) 61 | } 62 | 63 | if (failIfFound && failIfNotFound) { 64 | throw new Error('Cannot set both failIfFound and failIfNotFound') 65 | } 66 | 67 | if (Cypress._.isEmpty(selectors)) { 68 | selectors = ['[data-cy]'] 69 | } 70 | 71 | const andSelectors = selectors.join(', ') 72 | cy.log(`cypress-highlight: ${andSelectors}`) 73 | const stylesheetTitle = 74 | 'highlight-' + Cypress._.kebabCase(selectors.join('-')) 75 | const outline = 'outline: 1px solid red !important;\n' 76 | const css = selectors 77 | .map((selector) => `${selector} { ${outline} }`) 78 | .join('\n') 79 | addStyles(stylesheetTitle, css) 80 | 81 | if (failIfFound) { 82 | cy.get(andSelectors, { log: false }).should('not.exist') 83 | } else if (failIfNotFound) { 84 | cy.get(andSelectors, { log: false }).should('exist') 85 | } 86 | } 87 | 88 | /** 89 | * @param {boolean} failIfFound - if true, the test fails if the selector is found 90 | */ 91 | function highlightMissingTestIds(failIfFound = true) { 92 | const stylesheetTitle = 'highlight-missing-test-ids' 93 | const css = ` 94 | input:not([data-cy]),button:not([data-cy]),a:not([data-cy]) { outline: 2px solid pink !important; } 95 | ` 96 | addStyles(stylesheetTitle, css) 97 | if (failIfFound) { 98 | cy.document({ log: false }).then((doc) => { 99 | const input = doc.querySelector('input:not([data-cy])') 100 | if (input) { 101 | console.error('Found input elements without data-cy attribute') 102 | console.error(input) 103 | console.error(input.outerHTML) 104 | throw new Error( 105 | 'Found input elements without data-cy attribute, see DevTools console for details', 106 | ) 107 | } 108 | const button = doc.querySelector('button:not([data-cy])') 109 | if (button) { 110 | console.error('Found button elements without data-cy attribute') 111 | console.error(button) 112 | console.error(button.outerHTML) 113 | throw new Error( 114 | 'Found button elements without data-cy attribute, see DevTools console for details', 115 | ) 116 | } 117 | const a = doc.querySelector('a:not([data-cy])') 118 | if (a) { 119 | console.error('Found anchor elements without data-cy attribute') 120 | console.error(a) 121 | console.error(a.outerHTML) 122 | throw new Error( 123 | 'Found anchor elements without data-cy attribute, see DevTools console for details', 124 | ) 125 | } 126 | 127 | cy.log('**all important elements have data-cy attribute**') 128 | }) 129 | } 130 | } 131 | 132 | module.exports = { highlight, highlightMissingTestIds } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cypress-highlight 2 | 3 | [![ci status][ci image]][ci url] [![renovate-app badge][renovate-badge]][renovate-app] ![cypress version](https://img.shields.io/badge/cypress-15.0.0-brightgreen) 4 | 5 | > Highlights all elements on the page with good test selectors by injecting a CSS rule 6 | 7 | ![Image with highlighted elements that have data-cy attribute](./images/app.png) 8 | 9 | ## Videos 10 | 11 | - [Add A Red Border Around Elements That Have data-cy Attribute](https://youtu.be/pHzroBFY5V0) 12 | - [Cypress-highlight Plugin Introduction](https://youtu.be/EIbSzT6QSxc) 13 | 14 | ## Install 15 | 16 | ```shell 17 | $ npm install -D cypress-highlight 18 | # or using Yarn 19 | $ yarn add -D cypress-highlight 20 | ``` 21 | 22 | ## Use 23 | 24 | ```js 25 | import { highlight } from 'cypress-highlight' 26 | 27 | it('loads an app', () => { 28 | cy.visit('/') 29 | highlight() 30 | // you can capture a screenshot to see the elements 31 | // with good test selectors 32 | cy.screenshot('highlights', { capture: 'runner' }) 33 | }) 34 | ``` 35 | 36 | See [spec.js](./cypress/integration/spec.js) 37 | 38 | ### Multiple selectors 39 | 40 | By default, this module highlight all elements with `data-cy` attribute. You can pass your own list of selectors to highlight. For example, to highlight all elements with data attribute `test-id` and all elements with class name `testable`, use 41 | 42 | ```js 43 | highlight('[data-test-id]', '.testable') 44 | ``` 45 | 46 | ### Options object 47 | 48 | You can pass multiple options using an object 49 | 50 | ```js 51 | // highlight all elements that have "data-cy" attribute 52 | highlight({ 53 | selectors: '[data-cy]', 54 | }) 55 | // highlight all elements that have "data-cy" attribute 56 | // plus all elements with class "todo-li" 57 | highlight({ 58 | selectors: ['[data-cy]', '.todo-li'], 59 | }) 60 | ``` 61 | 62 | #### failIfFound 63 | 64 | You can highlight and then fail the test if any matching elements are found 65 | 66 | ```js 67 | highlight({ 68 | selectors: '[data-error]', 69 | failIfFound: true, 70 | }) 71 | ``` 72 | 73 | #### failIfNotFound 74 | 75 | You can fail the test if no matching elements are found 76 | 77 | ```js 78 | highlight({ 79 | selectors: 'li.todo', 80 | failIfNotFound: true, 81 | }) 82 | ``` 83 | 84 | ### highlightMissingTestIds 85 | 86 | Highlights any buttons / input elements / anchor links without `data-cy` attribute. Fails by default if any are found. 87 | 88 | ```js 89 | import { highlightMissingTestIds } from 'cypress-highlight' 90 | // somewhere during the test 91 | highlightMissingTestIds() 92 | // disable throwing an error on a found element 93 | highlightMissingTestIds(false) 94 | ``` 95 | 96 | ![Found input with missing data-cy attribute](./images/missing.png) 97 | 98 | ## Read 99 | 100 | [Cypress best practices for selecting elements](https://on.cypress.io/best-practices#Selecting-Elements) 101 | 102 | ## Small print 103 | 104 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2021 105 | 106 | - [@bahmutov](https://twitter.com/bahmutov) 107 | - [glebbahmutov.com](https://glebbahmutov.com) 108 | - [blog](https://glebbahmutov.com/blog) 109 | - [videos](https://www.youtube.com/glebbahmutov) 110 | - [presentations](https://slides.com/bahmutov) 111 | - [cypress.tips](https://cypress.tips) 112 | 113 | License: MIT - do anything with the code, but don't blame me if it does not work. 114 | 115 | Support: if you find any problems with this module, email / tweet / 116 | [open issue](https://github.com/bahmutov/cypress-highlight/issues) on Github 117 | 118 | ## MIT License 119 | 120 | Copyright (c) 2021 Gleb Bahmutov <gleb.bahmutov@gmail.com> 121 | 122 | Permission is hereby granted, free of charge, to any person 123 | obtaining a copy of this software and associated documentation 124 | files (the "Software"), to deal in the Software without 125 | restriction, including without limitation the rights to use, 126 | copy, modify, merge, publish, distribute, sublicense, and/or sell 127 | copies of the Software, and to permit persons to whom the 128 | Software is furnished to do so, subject to the following 129 | conditions: 130 | 131 | The above copyright notice and this permission notice shall be 132 | included in all copies or substantial portions of the Software. 133 | 134 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 135 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 136 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 137 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 138 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 139 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 140 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 141 | OTHER DEALINGS IN THE SOFTWARE. 142 | 143 | [ci image]: https://github.com/bahmutov/cypress-highlight/workflows/ci/badge.svg?branch=main 144 | [ci url]: https://github.com/bahmutov/cypress-highlight/actions 145 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 146 | [renovate-app]: https://renovateapp.com/ 147 | --------------------------------------------------------------------------------