├── .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 |
--------------------------------------------------------------------------------