├── .gitignore ├── .vscode └── settings.json ├── cypress.json ├── .npmrc ├── cypress ├── integration │ ├── spec-2.js │ └── spec.js ├── plugins │ └── index.js ├── fixtures │ └── example.json ├── support │ └── index.js └── README.md ├── test ├── plugin-does-grep.js ├── plugin-selects-no-tests.js ├── plugin-browserify-with-grep.js ├── plugin-selects-does.js └── spec.js ├── renovate.json ├── circle.yml ├── src ├── index.js ├── itify.js ├── grep-pick-tests.js └── spec-parser.js ├── grep.js ├── package.json ├── README.md └── __snapshots__ └── spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://todomvc.com/examples/vue/" 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | save-exact=true 3 | progress=false 4 | package-lock=true 5 | -------------------------------------------------------------------------------- /cypress/integration/spec-2.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('Second spec', () => { 4 | it('works', () => {}) 5 | }) 6 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const selectTestsWithGrep = require('../../grep') 2 | module.exports = (on, config) => { 3 | on('file:preprocessor', selectTestsWithGrep(config)) 4 | } 5 | -------------------------------------------------------------------------------- /test/plugin-does-grep.js: -------------------------------------------------------------------------------- 1 | const selectTestsWithGrep = require('../grep') 2 | module.exports = (on, config) => { 3 | on('file:preprocessor', selectTestsWithGrep(config)) 4 | } 5 | -------------------------------------------------------------------------------- /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/plugin-selects-no-tests.js: -------------------------------------------------------------------------------- 1 | const selectTests = require('..') 2 | 3 | const pickTests = (filename, foundTests, cypressConfig) => { 4 | // no tests to run! 5 | } 6 | 7 | module.exports = (on, config) => { 8 | on('file:preprocessor', selectTests(config, pickTests)) 9 | } 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "major": { 7 | "automerge": false 8 | }, 9 | "timezone": "America/New_York", 10 | "schedule": [ 11 | "after 10pm and before 5am on every weekday", 12 | "every weekend" 13 | ], 14 | "lockFileMaintenance": { 15 | "enabled": true 16 | }, 17 | "masterIssue": true, 18 | "prHourlyLimit": 2, 19 | "updateNotScheduled": false 20 | } 21 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | cypress: cypress-io/cypress@1 4 | jobs: 5 | release: 6 | executor: cypress/base-10 7 | steps: 8 | - attach_workspace: 9 | at: ~/ 10 | - run: npm run semantic-release 11 | workflows: 12 | build: 13 | jobs: 14 | - cypress/run: 15 | command: npm test 16 | - release: 17 | requires: 18 | - cypress/run 19 | filters: 20 | branches: 21 | only: 22 | - master 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const browserify = require('@cypress/browserify-preprocessor') 2 | const itify = require('./itify') 3 | 4 | // can we make "pickTests" async? 5 | const onFilePreprocessor = (config, pickTests) => { 6 | const options = { 7 | browserifyOptions: { 8 | transform: [ 9 | ...browserify.defaultOptions.browserifyOptions.transform, 10 | itify(config, pickTests) 11 | ] 12 | } 13 | } 14 | 15 | return browserify(options) 16 | } 17 | 18 | module.exports = onFilePreprocessor 19 | -------------------------------------------------------------------------------- /cypress/integration/spec.js: -------------------------------------------------------------------------------- 1 | // enables intelligent code completion for Cypress commands 2 | // https://on.cypress.io/intelligent-code-completion 3 | /// 4 | 5 | describe('Example tests', () => { 6 | it('works', () => {}) 7 | 8 | context('nested', () => { 9 | // should we pick tests based on grep or some kind of tags? 10 | 11 | // @tagA 12 | it('does A', () => {}) 13 | 14 | // @tagB 15 | it('does B', () => {}) 16 | 17 | // @tags foo,bar 18 | specify('does C', () => {}) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /grep.js: -------------------------------------------------------------------------------- 1 | const selectTests = require('./src') 2 | const { grepPickTests } = require('./src/grep-pick-tests') 3 | 4 | /** 5 | * Selects spec files to run using partial string match (fgrep). 6 | * Selects tests to run using partial string match in the suite or test name (grep). 7 | * 8 | * @example 9 | ```shell 10 | ## run tests with "works" in their full titles 11 | $ npx cypress open --env grep=works 12 | ## runs only specs with "foo" in their filename 13 | $ npx cypress run --env fgrep=foo 14 | ## runs only tests with "works" from specs with "foo" 15 | $ npx cypress run --env fgrep=foo,grep=works 16 | ## runs tests with "feature A" in the title 17 | $ npx cypress run --env grep='feature A' 18 | ``` 19 | */ 20 | const selectTestsWithGrep = config => selectTests(config, grepPickTests) 21 | 22 | module.exports = selectTestsWithGrep 23 | -------------------------------------------------------------------------------- /cypress/README.md: -------------------------------------------------------------------------------- 1 | # Cypress.io end-to-end tests 2 | 3 | [Cypress.io](https://www.cypress.io) is an open source, MIT licensed end-to-end test runner 4 | 5 | ## Folder structure 6 | 7 | These folders hold end-to-end tests and supporting files for the Cypress Test Runner. 8 | 9 | - [fixtures](fixtures) holds optional JSON data for mocking, [read more](https://on.cypress.io/fixture) 10 | - [integration](integration) holds the actual test files, [read more](https://on.cypress.io/writing-and-organizing-tests) 11 | - [plugins](plugins) allow you to customize how tests are loaded, [read more](https://on.cypress.io/plugins) 12 | - [support](support) file runs before all tests and is a great place to write or load additional custom commands, [read more](https://on.cypress.io/writing-and-organizing-tests#Support-file) 13 | 14 | ## `cypress.json` file 15 | 16 | You can configure project options in the [../cypress.json](../cypress.json) file, see [Cypress configuration doc](https://on.cypress.io/configuration). 17 | 18 | ## More information 19 | 20 | - [https://github.com/cypress.io/cypress](https://github.com/cypress.io/cypress) 21 | - [https://docs.cypress.io/](https://docs.cypress.io/) 22 | - [Writing your first Cypress test](http://on.cypress.io/intro) 23 | -------------------------------------------------------------------------------- /test/plugin-browserify-with-grep.js: -------------------------------------------------------------------------------- 1 | // for https://github.com/bahmutov/cypress-select-tests/issues/33 2 | // where we need custom browserify + grep selection 3 | const browserify = require('@cypress/browserify-preprocessor') 4 | // utility function to process source in browserify 5 | const itify = require('../src/itify') 6 | // actual picking tests based on environment variables in the config file 7 | const { grepPickTests } = require('../src/grep-pick-tests') 8 | 9 | module.exports = (on, config) => { 10 | let customBrowserify 11 | 12 | // we need custom browserify transformation 13 | on('before:browser:launch', (browser = {}) => { 14 | const options = browserify.defaultOptions 15 | const envPreset = options.browserifyOptions.transform[1][1].presets[0] 16 | options.browserifyOptions.transform[1][1].presets[0] = [ 17 | envPreset, 18 | { 19 | ignoreBrowserslistConfig: true, 20 | targets: { [browser.name]: browser.majorVersion } 21 | } 22 | ] 23 | 24 | // notice how we add OUR select tests transform to the list of browserify options 25 | options.browserifyOptions.transform.push(itify(config, grepPickTests)) 26 | customBrowserify = browserify(options) 27 | }) 28 | 29 | on('file:preprocessor', file => customBrowserify(file)) 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-select-tests", 3 | "version": "0.0.0-development", 4 | "description": "User space solution for picking Cypress tests to run", 5 | "private": false, 6 | "main": "src/index.js", 7 | "scripts": { 8 | "test": "mocha --timeout 20000 --reporter spec 'test/*spec.js'", 9 | "semantic-release": "semantic-release" 10 | }, 11 | "keywords": [ 12 | "cypress", 13 | "cypress-io", 14 | "cypress-plugin", 15 | "cypress-preprocessor" 16 | ], 17 | "files": [ 18 | "src", 19 | "grep.js" 20 | ], 21 | "author": "Gleb Bahmutov ", 22 | "license": "MIT", 23 | "peerDependencies": { 24 | "cypress": "3 || 5 || 6 || 7" 25 | }, 26 | "dependencies": { 27 | "@cypress/browserify-preprocessor": "3.0.1", 28 | "debug": "4.3.1", 29 | "falafel": "2.2.4", 30 | "pluralize": "8.0.0", 31 | "through": "2.3.8" 32 | }, 33 | "devDependencies": { 34 | "cypress": "7.4.0", 35 | "lazy-ass": "1.6.0", 36 | "mocha": "8.4.0", 37 | "mocha-banner": "1.1.2", 38 | "ramda": "0.27.1", 39 | "semantic-release": "17.4.3", 40 | "snap-shot-it": "7.9.6" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/bahmutov/cypress-select-tests.git" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/plugin-selects-does.js: -------------------------------------------------------------------------------- 1 | const selectTests = require('..') 2 | 3 | /** 4 | * Return only the names of the tests we want to run. 5 | * The logic is up to us: maybe grep based on CLI arguments and use spec filename? 6 | * Or grep the test names? 7 | * 8 | * TODO: make this function optionally async 9 | */ 10 | const pickTests = (filename, foundTests, cypressConfig) => { 11 | // only leave some of the tests, picking by name 12 | // each test name is a list of strings 13 | // [suite name, suite name, ..., test name] 14 | 15 | console.log('picking tests to run in file %s', filename) 16 | 17 | // we could use Cypress env variables to use same options as Mocha 18 | // see https://mochajs.org/ 19 | // --fgrep, -f Only run tests containing this string 20 | // --grep, -g Only run tests matching this string or regexp 21 | 22 | // for example, only leave tests where the test name is "works" 23 | // return foundTests.filter(testName => R.last(testName) === 'works') 24 | 25 | const fgrep = cypressConfig.env.fgrep 26 | const grep = cypressConfig.env.grep // assume string for now, not regexp 27 | if (!fgrep && !grep) { 28 | // run all tests 29 | return foundTests 30 | } 31 | 32 | if (fgrep) { 33 | if (!filename.includes(fgrep)) { 34 | console.log('spec filename %s not matching fgrep "%s"', filename, fgrep) 35 | return 36 | } 37 | } 38 | if (grep) { 39 | return foundTests.filter(testName => 40 | testName.some(part => part.includes(grep)) 41 | ) 42 | } 43 | 44 | return foundTests 45 | } 46 | 47 | module.exports = (on, config) => { 48 | on('file:preprocessor', selectTests(config, pickTests)) 49 | } 50 | -------------------------------------------------------------------------------- /src/itify.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('cypress-select-tests') 4 | const through = require('through') 5 | const pluralize = require('pluralize') 6 | const specParser = require('./spec-parser') 7 | 8 | const formatTestName = parts => ' - ' + parts.join(' / ') 9 | 10 | const formatTestNames = foundTests => 11 | foundTests.map(formatTestName).join('\n') + '\n' 12 | 13 | function process (config, pickTests, filename, source) { 14 | // console.log('---source') 15 | // console.log(source) 16 | debug('Cypress config %O', config) 17 | 18 | const foundTests = specParser.findTests(source) 19 | if (!foundTests.length) { 20 | return source 21 | } 22 | 23 | debug('Found %s', pluralize('test', foundTests.length, true)) 24 | debug(formatTestNames(foundTests)) 25 | 26 | // if pickTests returns undefined, 27 | // assume the user does not want any tests from this file 28 | const testNamesToRun = pickTests(filename, foundTests, config) || [] 29 | debug('Will only run %s', pluralize('test', testNamesToRun.length, true)) 30 | debug(formatTestNames(testNamesToRun)) 31 | 32 | const processed = specParser.skipTests(source, testNamesToRun) 33 | // console.log('---processed') 34 | // console.log(processed) 35 | 36 | return processed 37 | } 38 | 39 | // good example of a simple Browserify transform is 40 | // https://github.com/thlorenz/varify 41 | module.exports = function itify (config, pickTests) { 42 | return function itifyTransform (filename) { 43 | debug('file %s', filename) 44 | 45 | let data = '' 46 | 47 | function ondata (buf) { 48 | data += buf 49 | } 50 | 51 | function onend () { 52 | this.queue(process(config, pickTests, filename, data)) 53 | this.emit('end') 54 | } 55 | 56 | return through(ondata, onend) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/grep-pick-tests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns only the names of the tests we want to run using 3 | * Cypress env variables "fgrep" and "grep". 4 | */ 5 | const grepPickTests = (filename, foundTests, cypressConfig) => { 6 | // only leave some of the tests, picking by name 7 | // each test name is a list of strings 8 | // [suite name, suite name, ..., test name] 9 | 10 | // we could use Cypress env variables to use same options as Mocha 11 | // see https://mochajs.org/ 12 | // --fgrep, -f Only run tests containing this string [string] 13 | // --grep, -g Only run tests matching this string or regexp [string] 14 | // --invert, -i Inverts --grep and --fgrep matches [boolean] 15 | // for example, only leave tests where the test name is "works" 16 | // return foundTests.filter(testName => R.last(testName) === 'works') 17 | 18 | const fgrep = cypressConfig.env.fgrep 19 | const grep = cypressConfig.env.grep // assume string for now, not regexp 20 | const invert = cypressConfig.env.invert 21 | 22 | if (fgrep) { 23 | if (invert) { 24 | console.log('\tJust tests with a name that does not contain: %s', fgrep) 25 | if (filename.includes(fgrep)) { 26 | console.warn( 27 | '\tTest filename %s matched fgrep "%s"', 28 | filename, 29 | fgrep 30 | ) 31 | return 32 | } 33 | } else { 34 | console.log('\tJust tests with a name that contains: %s', fgrep) 35 | if (!filename.includes(fgrep)) { 36 | console.warn( 37 | '\tTest filename %s did not match fgrep "%s"', 38 | filename, 39 | fgrep 40 | ) 41 | return 42 | } 43 | } 44 | } 45 | if (grep) { 46 | if (invert) { 47 | console.log('\tJust tests not tagged with: %s', grep) 48 | return foundTests.filter(testName => 49 | !testName.some(part => part && part.includes(grep)) 50 | ) 51 | } else { 52 | console.log('\tJust tests tagged with: %s', grep) 53 | return foundTests.filter(testName => 54 | testName.some(part => part && part.includes(grep)) 55 | ) 56 | } 57 | } 58 | 59 | return foundTests 60 | } 61 | 62 | module.exports = { grepPickTests } 63 | -------------------------------------------------------------------------------- /src/spec-parser.js: -------------------------------------------------------------------------------- 1 | const falafel = require('falafel') 2 | const debug = require('debug')('cypress-select-tests') 3 | 4 | const isTestBlock = name => node => { 5 | return ( 6 | node.type === 'CallExpression' && 7 | node.callee && 8 | node.callee.type === 'Identifier' && 9 | node.callee.name === name 10 | ) 11 | } 12 | 13 | const isDescribe = isTestBlock('describe') 14 | 15 | const isContext = isTestBlock('context') 16 | 17 | const isIt = isTestBlock('it') 18 | 19 | const isSpecify = isTestBlock('specify') 20 | 21 | const isItOnly = node => { 22 | return ( 23 | node.type === 'CallExpression' && 24 | node.callee && 25 | node.callee.type === 'MemberExpression' && 26 | node.callee.object && 27 | node.callee.property && 28 | node.callee.object.type === 'Identifier' && 29 | node.callee.object.name === 'it' && 30 | node.callee.object.type === 'Identifier' && 31 | node.callee.property.name === 'only' 32 | ) 33 | } 34 | 35 | const isItSkip = node => { 36 | return ( 37 | node.type === 'CallExpression' && 38 | node.callee && 39 | node.callee.type === 'MemberExpression' && 40 | node.callee.object && 41 | node.callee.property && 42 | node.callee.object.type === 'Identifier' && 43 | node.callee.object.name === 'it' && 44 | node.callee.object.type === 'Identifier' && 45 | node.callee.property.name === 'skip' 46 | ) 47 | } 48 | 49 | const getItsName = node => node.arguments[0].value 50 | 51 | /** 52 | * Given an AST test node, walks up its parent chain 53 | * to find all "describe" or "context" names 54 | */ 55 | const findSuites = (node, names = []) => { 56 | if (!node) { 57 | return 58 | } 59 | 60 | if (isDescribe(node) || isContext(node)) { 61 | names.push(getItsName(node)) 62 | } 63 | 64 | return findSuites(node.parent, names) 65 | } 66 | 67 | const findTests = source => { 68 | const foundTestNames = [] 69 | 70 | const onNode = node => { 71 | // console.log(node) 72 | 73 | if (isIt(node) || isSpecify(node)) { 74 | const names = [getItsName(node)] 75 | findSuites(node, names) 76 | 77 | // we were searching from inside out, thus need to revert the names 78 | const testName = names.reverse() 79 | // console.log('found test', testName) 80 | foundTestNames.push(testName) 81 | } 82 | // TODO: handle it.only and it.skip 83 | // or should it.only disable filtering? 84 | 85 | // else if (isItOnly(node)) { 86 | // const testName = [getItsName(node)] 87 | // console.log('found it.only', testName) 88 | // // nothing to do 89 | // } else if (isItSkip(node)) { 90 | // const testName = [getItsName(node)] 91 | // console.log('found it.skip', testName) 92 | // node.update('it.only' + node.source().substr(7)) 93 | // } 94 | } 95 | 96 | // ignore source output for now 97 | falafel(source, onNode) 98 | 99 | return foundTestNames 100 | } 101 | 102 | const equals = x => y => String(x) === String(y) 103 | 104 | const skipTests = (source, leaveTests) => { 105 | const onNode = node => { 106 | // console.log(node) 107 | 108 | if (isIt(node) || isSpecify(node)) { 109 | const names = [getItsName(node)] 110 | findSuites(node, names) 111 | // we were searching from inside out, thus need to revert the names 112 | const testName = names.reverse() 113 | // console.log('found test', testName) 114 | // foundTestNames.push(testName) 115 | const shouldLeaveTest = leaveTests.some(equals(testName)) 116 | if (shouldLeaveTest) { 117 | debug('leaving test', testName) 118 | } else { 119 | debug('disabling test', testName) 120 | if (isSpecify(node)) { 121 | return node.update('specify.skip' + node.source().substr(7)) 122 | } 123 | node.update('it.skip' + node.source().substr(2)) 124 | } 125 | } 126 | // TODO: handle it.only and it.skip 127 | 128 | // else if (isItOnly(node)) { 129 | // const testName = [getItsName(node)] 130 | // console.log('found it.only', testName) 131 | // // nothing to do 132 | // } else if (isItSkip(node)) { 133 | // const testName = [getItsName(node)] 134 | // console.log('found it.skip', testName) 135 | // node.update('it.only' + node.source().substr(7)) 136 | // } 137 | } 138 | 139 | const output = falafel(source, onNode) 140 | return output.toString() 141 | } 142 | 143 | module.exports = { 144 | findTests, 145 | skipTests 146 | } 147 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | require('mocha-banner').register() 3 | const R = require('ramda') 4 | const la = require('lazy-ass') 5 | const snapshot = require('snap-shot-it') 6 | const path = require('path') 7 | // @ts-ignore 8 | const cypress = require('cypress') 9 | 10 | const pickMainStatsFromRun = R.compose( 11 | R.pick(['suites', 'tests', 'passes', 'pending', 'skipped', 'failures']), 12 | R.prop('stats') 13 | ) 14 | 15 | const pickTestInfo = R.compose( 16 | R.project(['title', 'state']), 17 | R.prop('tests') 18 | ) 19 | 20 | const pickRunInfo = run => ({ 21 | stats: pickMainStatsFromRun(run), 22 | spec: R.pick(['name', 'relative'], run.spec), 23 | tests: pickTestInfo(run) 24 | }) 25 | 26 | it('runs only tests with "does" in their name from spec.js', () => { 27 | return cypress 28 | .run({ 29 | env: { 30 | grep: 'does' 31 | }, 32 | config: { 33 | video: false, 34 | videoUploadOnPasses: false, 35 | pluginsFile: path.join(__dirname, 'plugin-selects-does.js') 36 | }, 37 | spec: 'cypress/integration/spec.js' 38 | }) 39 | .then(R.prop('runs')) 40 | .then(runs => { 41 | la(runs.length === 1, 'expected single run', runs) 42 | return runs[0] 43 | }) 44 | .then(run => { 45 | snapshot({ 46 | 'main stats': pickMainStatsFromRun(run) 47 | }) 48 | 49 | snapshot({ 50 | 'test state': pickTestInfo(run) 51 | }) 52 | }) 53 | }) 54 | 55 | it('runs tests without "does" in their name from spec.js with grep invert', () => { 56 | return cypress 57 | .run({ 58 | env: { 59 | grep: 'does', 60 | invert: 'true' 61 | }, 62 | config: { 63 | video: false, 64 | videoUploadOnPasses: false, 65 | pluginsFile: path.join(__dirname, 'plugin-does-grep.js') 66 | }, 67 | spec: 'cypress/integration/spec.js' 68 | }) 69 | .then(R.prop('runs')) 70 | .then(runs => { 71 | la(runs.length === 1, 'expected single run', runs) 72 | return runs[0] 73 | }) 74 | .then(run => { 75 | // 1 pass without "does", 3 pending with "does" 76 | snapshot({ 77 | 'main stats': pickMainStatsFromRun(run) 78 | }) 79 | 80 | snapshot({ 81 | 'test state': pickTestInfo(run) 82 | }) 83 | }) 84 | }) 85 | 86 | it('runs no tests', () => { 87 | return cypress 88 | .run({ 89 | config: { 90 | video: false, 91 | videoUploadOnPasses: false, 92 | pluginsFile: path.join(__dirname, 'plugin-selects-no-tests.js') 93 | }, 94 | spec: 'cypress/integration/spec.js' 95 | }) 96 | .then(R.prop('runs')) 97 | .then(runs => { 98 | la(runs.length === 1, 'expected single run', runs) 99 | return runs[0] 100 | }) 101 | .then(run => { 102 | snapshot({ 103 | 'main stats': pickMainStatsFromRun(run) 104 | }) 105 | 106 | snapshot({ 107 | 'test state': pickTestInfo(run) 108 | }) 109 | }) 110 | }) 111 | 112 | it('only runs tests in spec-2', () => { 113 | return cypress 114 | .run({ 115 | env: { 116 | fgrep: 'spec-2' 117 | }, 118 | config: { 119 | video: false, 120 | videoUploadOnPasses: false, 121 | pluginsFile: path.join(__dirname, 'plugin-does-grep.js') 122 | }, 123 | spec: 'cypress/integration/*' 124 | }) 125 | .then(R.prop('runs')) 126 | .then(runs => { 127 | la(runs.length === 2, 'expected two specs', runs) 128 | 129 | const info = R.map(pickRunInfo, runs) 130 | snapshot(info) 131 | }) 132 | }) 133 | 134 | it('runs tests except selected files with fgrep invert', () => { 135 | return cypress 136 | .run({ 137 | env: { 138 | fgrep: '-2', 139 | invert: 'true' 140 | }, 141 | config: { 142 | video: false, 143 | videoUploadOnPasses: false, 144 | pluginsFile: path.join(__dirname, 'plugin-does-grep.js') 145 | }, 146 | spec: 'cypress/integration/*' 147 | }) 148 | .then(R.prop('runs')) 149 | .then(runs => { 150 | la(runs.length === 2, 'expected two specs', runs) 151 | 152 | const info = R.map(pickRunInfo, runs) 153 | // only tests from cypress/integration/spec.js should run 154 | snapshot(info) 155 | }) 156 | }) 157 | 158 | it('combines custom browserify with grep picker', () => { 159 | return cypress 160 | .run({ 161 | env: { 162 | // should run only tests that have "does B" in their title 163 | grep: 'does B' 164 | }, 165 | config: { 166 | video: false, 167 | videoUploadOnPasses: false, 168 | pluginsFile: path.join(__dirname, 'plugin-browserify-with-grep.js') 169 | }, 170 | spec: 'cypress/integration/spec.js' 171 | }) 172 | .then(R.prop('runs')) 173 | .then(runs => { 174 | la(runs.length === 1, 'expected single run', runs) 175 | return runs[0] 176 | }) 177 | .then(run => { 178 | const mainStats = pickMainStatsFromRun(run) 179 | const testState = pickTestInfo(run) 180 | 181 | // should be 1 test passing, the rest pending 182 | snapshot({ 183 | 'main stats': mainStats 184 | }) 185 | 186 | // only the test "does B" should pass 187 | snapshot({ 188 | 'test state': testState 189 | }) 190 | }) 191 | }) 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cypress-select-tests 2 | 3 | ## DEPRECATED 4 | 5 | You probably want to use [cypress-grep](https://github.com/bahmutov/cypress-grep) instead: no TypeScript or JavaScript parsing problems. 6 | 7 | ## OLD README 8 | 9 | > User space solution for picking Cypress tests to run 10 | 11 | [![NPM][npm-icon]][npm-url] 12 | 13 | [![Build status][ci-image]][ci-url] 14 | [![semantic-release][semantic-image]][semantic-url] 15 | [![standard][standard-image]][standard-url] 16 | [![renovate-app badge][renovate-badge]][renovate-app] 17 | 18 | ## Install 19 | 20 | Assuming [Cypress](https://www.cypress.io) has been installed: 21 | 22 | ```shell 23 | npm install --save-dev cypress-select-tests 24 | ``` 25 | 26 | ### Warning ⚠️ 27 | 28 | - this package assumes JavaScript specs 29 | - this package might conflict and/or overwrite other Cypress Browserify preprocessor settings 30 | 31 | ## Mocha-like selection 32 | 33 | [Mocha](https://mochajs.org/) has `--fgrep`, `--grep` and `--invert` CLI arguments to select spec files and tests to run. This package provides imitation using strings. In your `cypress/plugins/index.js` use: 34 | 35 | ```js 36 | const selectTestsWithGrep = require('cypress-select-tests/grep') 37 | module.exports = (on, config) => { 38 | on('file:preprocessor', selectTestsWithGrep(config)) 39 | } 40 | ``` 41 | 42 | Then open or run Cypress and use environment variables to pass strings to find. There are various ways to [pass environment variables](https://on.cypress.io/environment-variables), here is via CLI arguments: 43 | 44 | ```shell 45 | ## run tests with "works" in their full titles 46 | $ npx cypress open --env grep=works 47 | ## runs only specs with "foo" in their filename 48 | $ npx cypress run --env fgrep=foo 49 | ## runs only tests with "works" from specs with "foo" 50 | $ npx cypress run --env fgrep=foo,grep=works 51 | ## runs tests with "feature A" in the title 52 | $ npx cypress run --env grep='feature A' 53 | ## runs only specs NOT with "foo" in their filename 54 | $ npx cypress run --env fgrep=foo,invert=true 55 | ## runs tests NOT with "feature A" in the title 56 | $ npx cypress run --env grep='feature A',invert=true 57 | ``` 58 | 59 | The test picking function is available by itself in [src/grep-pick-tests.js](src/grep-pick-tests.js) file. 60 | 61 | ## Write your own selection logic 62 | 63 | In your `cypress/plugins/index.js` use this module as a file preprocessor and write your own `pickTests` function. 64 | 65 | ```js 66 | const selectTests = require('cypress-select-tests') 67 | 68 | // return test names you want to run 69 | const pickTests = (filename, foundTests, cypressConfig) => { 70 | // found tests will be names of the tests found in "filename" spec 71 | // it is a list of names, each name an Array of strings 72 | // ['suite 1', 'suite 2', ..., 'test name'] 73 | 74 | // return [] to skip ALL tests 75 | // OR 76 | // let's only run tests with "does" in the title 77 | return foundTests.filter(fullTestName => fullTestName.join(' ').includes('does')) 78 | } 79 | 80 | module.exports = (on, config) => { 81 | on('file:preprocessor', selectTests(config, pickTests)) 82 | } 83 | ``` 84 | 85 | Using `pickTests` allows you to implement your own test selection logic. All tests filtered out will be shown / counted as pending. 86 | 87 | ## Combine custom browserify with grep picker 88 | 89 | If you are adjusting Browserify options, and would like to use the above Mocha-like grep test picker, see [test/plugin-browserify-with-grep.js](test/plugin-browserify-with-grep.js) file. In essence, you want add the grep transform to the list of Browserify plugins. Something like 90 | 91 | ```js 92 | const browserify = require('@cypress/browserify-preprocessor') 93 | // utility function to process source in browserify 94 | const itify = require('cypress-select-tests/src/itify') 95 | // actual picking tests based on environment variables in the config file 96 | const { grepPickTests } = require('cypress-select-tests/src/grep-pick-tests') 97 | 98 | module.exports = (on, config) => { 99 | let customBrowserify 100 | 101 | // get the browserify options, then push another transform 102 | options.browserifyOptions.transform.push(itify(config, grepPickTests)) 103 | customBrowserify = browserify(options) 104 | 105 | on('file:preprocessor', file => customBrowserify(file)) 106 | } 107 | ``` 108 | 109 | ## Examples 110 | 111 | - [cypress-select-tests-example](https://github.com/bahmutov/cypress-select-tests-example) 112 | - [cypress-example-recipes grep](https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/preprocessors__grep) 113 | 114 | ## Debugging 115 | 116 | To see additional debugging output run 117 | 118 | ``` 119 | DEBUG=cypress-select-tests npx cypress open 120 | ``` 121 | 122 | ### Small print 123 | 124 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2019 125 | 126 | - [@bahmutov](https://twitter.com/bahmutov) 127 | - [glebbahmutov.com](https://glebbahmutov.com) 128 | - [blog](https://glebbahmutov.com/blog) 129 | 130 | License: MIT - do anything with the code, but don't blame me if it does not work. 131 | 132 | Support: if you find any problems with this module, email / tweet / 133 | [open issue](https://github.com/bahmutov/cypress-select-tests/issues) on Github 134 | 135 | ## MIT License 136 | 137 | Copyright (c) 2019 Gleb Bahmutov <gleb.bahmutov@gmail.com> 138 | 139 | Permission is hereby granted, free of charge, to any person 140 | obtaining a copy of this software and associated documentation 141 | files (the "Software"), to deal in the Software without 142 | restriction, including without limitation the rights to use, 143 | copy, modify, merge, publish, distribute, sublicense, and/or sell 144 | copies of the Software, and to permit persons to whom the 145 | Software is furnished to do so, subject to the following 146 | conditions: 147 | 148 | The above copyright notice and this permission notice shall be 149 | included in all copies or substantial portions of the Software. 150 | 151 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 152 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 153 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 154 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 155 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 156 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 157 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 158 | OTHER DEALINGS IN THE SOFTWARE. 159 | 160 | [npm-icon]: https://nodei.co/npm/cypress-select-tests.svg?downloads=true 161 | [npm-url]: https://npmjs.org/package/cypress-select-tests 162 | [ci-image]: https://circleci.com/gh/bahmutov/cypress-select-tests.svg?style=svg 163 | [ci-url]: https://circleci.com/gh/bahmutov/cypress-select-tests 164 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 165 | [semantic-url]: https://github.com/semantic-release/semantic-release 166 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg 167 | [standard-url]: http://standardjs.com/ 168 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 169 | [renovate-app]: https://renovateapp.com/ 170 | -------------------------------------------------------------------------------- /__snapshots__/spec.js: -------------------------------------------------------------------------------- 1 | exports['only runs tests in spec-2 1'] = [ 2 | { 3 | "stats": { 4 | "suites": 1, 5 | "tests": 1, 6 | "passes": 1, 7 | "pending": 0, 8 | "skipped": 0, 9 | "failures": 0 10 | }, 11 | "spec": { 12 | "name": "spec-2.js", 13 | "relative": "cypress/integration/spec-2.js" 14 | }, 15 | "tests": [ 16 | { 17 | "title": [ 18 | "Second spec", 19 | "works" 20 | ], 21 | "state": "passed" 22 | } 23 | ] 24 | }, 25 | { 26 | "stats": { 27 | "suites": 2, 28 | "tests": 4, 29 | "passes": 0, 30 | "pending": 4, 31 | "skipped": 0, 32 | "failures": 0 33 | }, 34 | "spec": { 35 | "name": "spec.js", 36 | "relative": "cypress/integration/spec.js" 37 | }, 38 | "tests": [ 39 | { 40 | "title": [ 41 | "Example tests", 42 | "works" 43 | ], 44 | "state": "pending" 45 | }, 46 | { 47 | "title": [ 48 | "Example tests", 49 | "nested", 50 | "does A" 51 | ], 52 | "state": "pending" 53 | }, 54 | { 55 | "title": [ 56 | "Example tests", 57 | "nested", 58 | "does B" 59 | ], 60 | "state": "pending" 61 | }, 62 | { 63 | "title": [ 64 | "Example tests", 65 | "nested", 66 | "does C" 67 | ], 68 | "state": "pending" 69 | } 70 | ] 71 | } 72 | ] 73 | 74 | exports['runs no tests 1'] = { 75 | "main stats": { 76 | "suites": 2, 77 | "tests": 4, 78 | "passes": 0, 79 | "pending": 4, 80 | "skipped": 0, 81 | "failures": 0 82 | } 83 | } 84 | 85 | exports['runs no tests 2'] = { 86 | "test state": [ 87 | { 88 | "title": [ 89 | "Example tests", 90 | "works" 91 | ], 92 | "state": "pending" 93 | }, 94 | { 95 | "title": [ 96 | "Example tests", 97 | "nested", 98 | "does A" 99 | ], 100 | "state": "pending" 101 | }, 102 | { 103 | "title": [ 104 | "Example tests", 105 | "nested", 106 | "does B" 107 | ], 108 | "state": "pending" 109 | }, 110 | { 111 | "title": [ 112 | "Example tests", 113 | "nested", 114 | "does C" 115 | ], 116 | "state": "pending" 117 | } 118 | ] 119 | } 120 | 121 | exports['runs only tests with "does" in their name from spec.js 1'] = { 122 | "main stats": { 123 | "suites": 2, 124 | "tests": 4, 125 | "passes": 3, 126 | "pending": 1, 127 | "skipped": 0, 128 | "failures": 0 129 | } 130 | } 131 | 132 | exports['runs only tests with "does" in their name from spec.js 2'] = { 133 | "test state": [ 134 | { 135 | "title": [ 136 | "Example tests", 137 | "works" 138 | ], 139 | "state": "pending" 140 | }, 141 | { 142 | "title": [ 143 | "Example tests", 144 | "nested", 145 | "does A" 146 | ], 147 | "state": "passed" 148 | }, 149 | { 150 | "title": [ 151 | "Example tests", 152 | "nested", 153 | "does B" 154 | ], 155 | "state": "passed" 156 | }, 157 | { 158 | "title": [ 159 | "Example tests", 160 | "nested", 161 | "does C" 162 | ], 163 | "state": "passed" 164 | } 165 | ] 166 | } 167 | 168 | exports['combines custom browserify with grep picker 1'] = { 169 | "main stats": { 170 | "suites": 2, 171 | "tests": 4, 172 | "passes": 1, 173 | "pending": 3, 174 | "skipped": 0, 175 | "failures": 0 176 | } 177 | } 178 | 179 | exports['combines custom browserify with grep picker 2'] = { 180 | "test state": [ 181 | { 182 | "title": [ 183 | "Example tests", 184 | "works" 185 | ], 186 | "state": "pending" 187 | }, 188 | { 189 | "title": [ 190 | "Example tests", 191 | "nested", 192 | "does A" 193 | ], 194 | "state": "pending" 195 | }, 196 | { 197 | "title": [ 198 | "Example tests", 199 | "nested", 200 | "does B" 201 | ], 202 | "state": "passed" 203 | }, 204 | { 205 | "title": [ 206 | "Example tests", 207 | "nested", 208 | "does C" 209 | ], 210 | "state": "pending" 211 | } 212 | ] 213 | } 214 | 215 | exports['runs tests except selected files with fgrep invert 1'] = [ 216 | { 217 | "stats": { 218 | "suites": 1, 219 | "tests": 1, 220 | "passes": 0, 221 | "pending": 1, 222 | "skipped": 0, 223 | "failures": 0 224 | }, 225 | "spec": { 226 | "name": "spec-2.js", 227 | "relative": "cypress/integration/spec-2.js" 228 | }, 229 | "tests": [ 230 | { 231 | "title": [ 232 | "Second spec", 233 | "works" 234 | ], 235 | "state": "pending" 236 | } 237 | ] 238 | }, 239 | { 240 | "stats": { 241 | "suites": 2, 242 | "tests": 4, 243 | "passes": 4, 244 | "pending": 0, 245 | "skipped": 0, 246 | "failures": 0 247 | }, 248 | "spec": { 249 | "name": "spec.js", 250 | "relative": "cypress/integration/spec.js" 251 | }, 252 | "tests": [ 253 | { 254 | "title": [ 255 | "Example tests", 256 | "works" 257 | ], 258 | "state": "passed" 259 | }, 260 | { 261 | "title": [ 262 | "Example tests", 263 | "nested", 264 | "does A" 265 | ], 266 | "state": "passed" 267 | }, 268 | { 269 | "title": [ 270 | "Example tests", 271 | "nested", 272 | "does B" 273 | ], 274 | "state": "passed" 275 | }, 276 | { 277 | "title": [ 278 | "Example tests", 279 | "nested", 280 | "does C" 281 | ], 282 | "state": "passed" 283 | } 284 | ] 285 | } 286 | ] 287 | 288 | exports['runs tests without "does" in their name from spec.js with grep invert 1'] = { 289 | "main stats": { 290 | "suites": 2, 291 | "tests": 4, 292 | "passes": 1, 293 | "pending": 3, 294 | "skipped": 0, 295 | "failures": 0 296 | } 297 | } 298 | 299 | exports['runs tests without "does" in their name from spec.js with grep invert 2'] = { 300 | "test state": [ 301 | { 302 | "title": [ 303 | "Example tests", 304 | "works" 305 | ], 306 | "state": "passed" 307 | }, 308 | { 309 | "title": [ 310 | "Example tests", 311 | "nested", 312 | "does A" 313 | ], 314 | "state": "pending" 315 | }, 316 | { 317 | "title": [ 318 | "Example tests", 319 | "nested", 320 | "does B" 321 | ], 322 | "state": "pending" 323 | }, 324 | { 325 | "title": [ 326 | "Example tests", 327 | "nested", 328 | "does C" 329 | ], 330 | "state": "pending" 331 | } 332 | ] 333 | } 334 | --------------------------------------------------------------------------------