├── .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] 
4 |
5 | > Highlights all elements on the page with good test selectors by injecting a CSS rule
6 |
7 | 
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 | 
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 |
--------------------------------------------------------------------------------