├── .gitignore ├── .npmrc ├── README.md ├── circle.yml ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── img └── get-it.png ├── index.html ├── index.js ├── package-lock.json ├── package.json └── renovate.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cypress-get-it [![CircleCI](https://circleci.com/gh/bahmutov/cypress-get-it.svg?style=svg)](https://circleci.com/gh/bahmutov/cypress-get-it) [![renovate-app badge][renovate-badge]][renovate-app] 2 | 3 | > Get elements by data attribute by creating a Cy command on the fly 4 | 5 | [![NPM][npm-icon] ][npm-url] 6 | 7 | ## Why 8 | 9 | According to the Cypress best practices for [Selecting Elements](https://on.cypress.io/best-practices#Selecting-Elements) using dedicated test id data attributes is the most stable way of finding elements during end-to-end tests. Yet typing the full selector is ... annoying. 10 | 11 | ```html 12 |
foo
13 | ``` 14 | ```js 15 | cy.get('[data-test-id="foo"]').should('have.text', 'foo') 16 | ``` 17 | 18 | Who has time for this! 19 | 20 | So you could make a little utility function 21 | 22 | ```js 23 | const tid = s => `[data-test-id="${s}"]` 24 | cy.get(tid('foo')).should('have.text', 'foo') 25 | ``` 26 | 27 | Hmm, sure, but what if your HTML markup has different data attributes? Like this 28 | 29 | ```html 30 |
foo
31 |
bar
32 |
baz
33 | ``` 34 | 35 | Do you need to write 3 different helper functions? No. With `cypress-get-it` you can create data attribute selector _methods_ with descriptive names. In fact, the actual selector will be created _from the method name_ on the fly. You want to get element with `data-test-id=foo`? Just use camel cased words and call `cy.getDataTestId('foo')`. The above elements become 36 | 37 | ```js 38 | cy.getDataTestId('foo').should('have.text', 'foo') 39 | cy.getDataTest('bar').should('have.text', 'bar') 40 | cy.getTestId('baz').should('have.text', 'baz') 41 | ``` 42 | 43 | ![cypress-get-it](img/get-it.png) 44 | 45 | So any element attribute like `foo-bar-baz="something"` could be fetched with descriptive `cy.getFooBarBaz("something")`. 46 | 47 | ## How it works 48 | 49 | Load this plugin from your support file and it will replace global `cy` instance with an ES6 Proxy which will intercept any methods calls to `get...` and will call [`cy.get`](https://on.cypress.io/get) with the right selector automatically. 50 | 51 | ## Install and use 52 | 53 | ```shell 54 | npm i -D cypress-get-it 55 | ``` 56 | 57 | Require this module from your `cypress/support/index.js` 58 | 59 | ```js 60 | require('cypress-get-it') 61 | ``` 62 | 63 | ### Small print 64 | 65 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2019 66 | 67 | * [@bahmutov](https://twitter.com/bahmutov) 68 | * [glebbahmutov.com](https://glebbahmutov.com) 69 | * [blog](https://glebbahmutov.com/blog) 70 | 71 | License: MIT - do anything with the code, but don't blame me if it does not work. 72 | 73 | Support: if you find any problems with this module, email / tweet / 74 | [open issue](https://github.com/bahmutov/cypress-get-it/issues) on Github 75 | 76 | ## MIT License 77 | 78 | Copyright (c) 2019 Gleb Bahmutov <gleb.bahmutov@gmail.com> 79 | 80 | Permission is hereby granted, free of charge, to any person 81 | obtaining a copy of this software and associated documentation 82 | files (the "Software"), to deal in the Software without 83 | restriction, including without limitation the rights to use, 84 | copy, modify, merge, publish, distribute, sublicense, and/or sell 85 | copies of the Software, and to permit persons to whom the 86 | Software is furnished to do so, subject to the following 87 | conditions: 88 | 89 | The above copyright notice and this permission notice shall be 90 | included in all copies or substantial portions of the Software. 91 | 92 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 93 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 94 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 95 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 96 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 97 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 98 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 99 | OTHER DEALINGS IN THE SOFTWARE. 100 | 101 | [npm-icon]: https://nodei.co/npm/cypress-skip-and-only-ui.svg?downloads=true 102 | [npm-url]: https://npmjs.org/package/cypress-skip-and-only-ui 103 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 104 | [renovate-app]: https://renovateapp.com/ 105 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | cypress: cypress-io/cypress@1.19.1 4 | 5 | jobs: 6 | release: 7 | executor: cypress/base-12-14-0 8 | steps: 9 | - attach_workspace: 10 | at: ~/ 11 | - run: npm run semantic-release 🚀 12 | 13 | workflows: 14 | build: 15 | jobs: 16 | - cypress/run: 17 | name: Cypress run 🛎📦🧪 18 | - release: 19 | filters: 20 | branches: 21 | only: 22 | - master 23 | requires: 24 | - Cypress run 🛎📦🧪 25 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /cypress/integration/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | it('finds element by test id data attribute', () => { 4 | cy.visit('index.html') 5 | cy.get('[data-test-id="foo"]').should('have.text', 'foo') 6 | cy.getDataTestId('foo').should('have.text', 'foo') 7 | cy.getDataTest('bar').should('have.text', 'bar') 8 | cy.getTestId('baz').should('have.text', 'baz') 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /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 is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import '../..' 18 | import './commands' 19 | 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | -------------------------------------------------------------------------------- /img/get-it.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-get-it/971db60d7c694fbf42e576425ce0051b072bf3f4/img/get-it.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |
foo
3 |
bar
4 |
baz
5 | 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | const getSomethingToWords = s => Cypress._.split(Cypress._.snakeCase(s), '_') 4 | 5 | const getAttribute = words => { 6 | // ['get', 'data', test', 'id'] 7 | words.shift() 8 | return words.join('-') 9 | } 10 | 11 | global.cy = new Proxy(global.cy, { 12 | get (target, prop) { 13 | console.log('getting prop', prop) 14 | if (/^get\w+/.test(prop)) { 15 | console.log('get', prop) 16 | const words = getSomethingToWords(prop) 17 | console.log(words) 18 | const attribute = getAttribute(words) 19 | console.log(attribute) 20 | 21 | return selector => { 22 | cy.log(`${prop} "${selector}"`) 23 | return target.get(`[${attribute}="${selector}"]`, { log: false }) 24 | } 25 | } else { 26 | return target[prop] 27 | } 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-get-it", 3 | "version": "0.0.0-development", 4 | "description": "Get elements by data attribute by creating a Cy command on the fly", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "cypress run", 8 | "semantic-release": "semantic-release" 9 | }, 10 | "keywords": [ 11 | "cypress", 12 | "cypress-plugin" 13 | ], 14 | "author": "Gleb Bahmutov (https://glebbahmutov.com/)", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "cypress": "4.2.0", 18 | "semantic-release": "17.0.4" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/bahmutov/cypress-get-it.git" 23 | }, 24 | "files": [ 25 | "*.js" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "commitMessage": "{{semanticPrefix}}Update {{depName}} to {{newVersion}} 🌟", 7 | "prTitle": "{{semanticPrefix}}{{#if isPin}}Pin{{else}}Update{{/if}} dependency {{depName}} to version {{newVersion}} 🌟", 8 | "prConcurrentLimit": 3, 9 | "prHourlyLimit": 2, 10 | "rangeStrategy": "pin", 11 | "schedule": [ 12 | "after 10pm and before 5am on every weekday", 13 | "every weekend" 14 | ], 15 | "updateNotScheduled": false, 16 | "timezone": "America/New_York", 17 | "lockFileMaintenance": { 18 | "enabled": true 19 | }, 20 | "separatePatchReleases": true, 21 | "separateMultipleMajor": true, 22 | "masterIssue": true, 23 | "labels": [ 24 | "type: dependencies", 25 | "renovate" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------