├── .github └── workflows │ ├── badges.yml │ ├── ci.yml │ ├── merge.yml │ └── pr.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── README.md ├── bin └── find.js ├── cypress.config.js ├── cypress ├── e2e │ ├── featureA │ │ ├── user.cy.ts │ │ └── utils.js │ ├── spec-b.cy.js │ ├── spec.cy.js │ └── utils.js ├── html-e2e │ ├── test-html.cy.js │ └── test-tags.cy.js └── tsconfig.json ├── images ├── debug.png └── report.png ├── mocks ├── branch-1.js ├── branch-tagged-1-no-alpha.js ├── branch-tagged-1.js ├── changed-imported-file.js ├── my-app │ └── e2e │ │ ├── cypress.config.js │ │ └── e2e-tests │ │ └── spec-a.cy.js ├── parent-1.js └── parent-2.js ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── badge.js ├── count.js ├── files.js ├── grep.js ├── index.js ├── output-html.js ├── print.js ├── tagged.js └── tests-counts.js ├── test-components ├── comp1.cy.js └── comp2.cy.ts ├── test-effective-tags ├── .gitignore ├── cypress.json ├── cypress │ └── integration │ │ ├── spec1.js │ │ └── spec2.js ├── package-lock.json └── package.json ├── test-exclusive ├── .gitignore ├── cypress.json ├── cypress │ └── integration │ │ └── spec1.js ├── package-lock.json └── package.json ├── test-json ├── .gitignore ├── cypress.json ├── cypress │ └── integration │ │ ├── spec1.js │ │ └── spec2.js ├── package-lock.json └── package.json ├── test-modules ├── .gitignore ├── cypress.config.js ├── cypress │ └── e2e │ │ └── spec1.cy.js ├── package-lock.json └── package.json ├── test-parsing-error ├── .gitignore ├── cypress.json ├── cypress │ └── integration │ │ ├── spec1.js │ │ └── spec2.js ├── package-lock.json └── package.json ├── test-required-tags ├── .gitignore ├── cypress.json ├── cypress │ └── integration │ │ ├── spec1.js │ │ ├── spec2.js │ │ └── spec3.js ├── package-lock.json └── package.json ├── test-skip-node-modules ├── .gitignore ├── cypress.config.js ├── cypress │ └── e2e │ │ ├── node_modules │ │ └── spec2.cy.js │ │ └── spec1.cy.js ├── package-lock.json └── package.json ├── test-ts-aliases ├── .gitignore ├── cypress.config.custom.ts ├── cypress.config.ts ├── cypress │ ├── e2e │ │ ├── spec1.cy.ts │ │ └── spec2.cy.ts │ └── support │ │ └── utils.ts ├── package-lock.json ├── package.json └── tsconfig.json ├── test-ts-import ├── .gitignore ├── cypress.config.ts ├── cypress │ └── e2e │ │ ├── spec1.cy.ts │ │ └── spec2.cy.ts ├── package-lock.json ├── package.json └── tsconfig.json ├── test-ts ├── .gitignore ├── cypress.config.custom.ts ├── cypress.config.ts ├── cypress │ └── e2e │ │ ├── spec1.cy.ts │ │ └── spec2.cy.ts ├── package-lock.json ├── package.json └── tsconfig.json └── test ├── basic.js ├── branch-tagged.js ├── branch.js ├── cli.js ├── count.js ├── grep.js ├── json.js ├── npm ├── get-specs.js ├── get-tests.js └── snapshots │ ├── get-tests.js.md │ └── get-tests.js.snap ├── print.js ├── snapshots ├── branch-tagged.js.md ├── branch-tagged.js.snap ├── branch.js.md ├── branch.js.snap ├── cli.js.md ├── cli.js.snap ├── json.js.md ├── json.js.snap ├── print.js.md └── print.js.snap ├── tagged.js └── tagged.json /.github/workflows/badges.yml: -------------------------------------------------------------------------------- 1 | name: badges 2 | on: 3 | schedule: 4 | # update badges every night 5 | # because we have a few badges that are linked 6 | # to the external repositories 7 | - cron: '0 3 * * *' 8 | 9 | jobs: 10 | badges: 11 | name: Badges 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - name: Checkout 🛎 15 | uses: actions/checkout@v3 16 | 17 | - name: Install dependencies 📦 18 | # https://github.com/cypress-io/github-action 19 | uses: cypress-io/github-action@v5 20 | with: 21 | runTests: false 22 | 23 | - name: Update version badges 🏷 24 | run: npm run version-badge 25 | 26 | - name: Update test counts badge 📊 27 | run: npm run demo-test-counts-badge 28 | 29 | - name: Commit any changed files 💾 30 | uses: stefanzweifel/git-auto-commit-action@v4 31 | with: 32 | commit_message: Updated badges 33 | branch: main 34 | file_pattern: README.md 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: push 3 | 4 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 5 | permissions: 6 | contents: write 7 | pages: write 8 | id-token: write 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-22.04 13 | # Deploy to the github-pages environment 14 | environment: 15 | name: github-pages 16 | url: ${{ steps.deployment.outputs.page_url }} 17 | steps: 18 | - name: Checkout 🛎 19 | uses: actions/checkout@v4 20 | 21 | - name: Run tests 🧪 22 | # https://github.com/cypress-io/github-action 23 | uses: cypress-io/github-action@v6 24 | env: 25 | DEBUG: 'cypress:server:specs' 26 | 27 | - name: Check production dependencies 💎 28 | # remove OR once dependencies don't trigger an alert 29 | run: npm run prod-deps || true 30 | 31 | # prints all specs 32 | - name: Demo found specs 💻 33 | run: npm run demo 34 | 35 | - name: Print specs sorted by last modified date 36 | run: npm run demo-sorted-by-modified 37 | 38 | - name: Print dependencies between tests and utils 💻 39 | run: npm run deps 40 | 41 | # module and import support 42 | - name: Test module support 43 | run: npm run demo-tests 44 | working-directory: test-modules 45 | 46 | # prints all specs and tests inside each spec 47 | - name: Demo test names 💻 48 | run: npm run demo-names 49 | 50 | - name: Demo test names in Markdown format 💻 51 | run: npm run demo-names-markdown 52 | 53 | - name: Demo test names in Markdown format and put into job summary 💻 54 | run: npm run demo-names-markdown >> "$GITHUB_STEP_SUMMARY" 55 | 56 | - name: Demo component test names 🧩 57 | run: npm run demo-component 58 | 59 | - name: Demo tags 💻 60 | run: npm run demo-tags 61 | 62 | - name: Demo test names and tags 💻 63 | run: npm run demo-names-and-tags 64 | 65 | - name: Demo pending tests only 💻 66 | run: npm run demo-skipped-tests 67 | 68 | - name: Demo count skipped tests 💻 69 | run: npm run demo-count-skipped-tests 70 | 71 | - name: Demo names in json output 💻 72 | run: npm run demo-names-json 73 | 74 | - name: Demo tags in json output 💻 75 | run: npm run demo-tags-json 76 | 77 | - name: Demo with custom cypress config 💻 78 | run: npm run demo-custom-cypress-config 79 | 80 | - name: Print specs changed against the main branch 🌳 81 | run: npm run print-changed-specs 82 | 83 | - name: Count the specs changed against the main branch 🌳 84 | run: npm run count-changed-specs 85 | 86 | - name: Load cypress.config.ts 🤘 87 | run: npm run demo-tests --prefix test-ts 88 | - name: Test cypress.config.ts project 89 | run: npm test --prefix test-ts 90 | 91 | - name: Load cypress.config.ts with import keyword 🤘 92 | run: npm run demo-tests --prefix test-ts-import 93 | - name: Test cypress.config.ts project with import keyword 94 | run: npm test --prefix test-ts-import 95 | 96 | - name: Trace TS path aliases demo 97 | run: npm run deps --prefix test-ts-aliases 98 | 99 | - name: Test cypress.json project (Cypress v9) 100 | run: npm run demo-tests --prefix test-json 101 | 102 | - name: Show finding specs by part of the test title 103 | run: node ./bin/find.js --grep "something" --set-gha-outputs 104 | 105 | - name: Test parsing broken JS file 🐞 106 | run: npm run test-names --prefix test-parsing-error 107 | 108 | - name: Test exclusive tests ↗️ 109 | run: npm run demo-exclusive 110 | 111 | - name: Count all tags, including required 112 | run: npm run count-all-tags 113 | 114 | - name: Unit tests 🧪 115 | run: npm test 116 | 117 | - name: Find tests in subfolder 118 | run: npm run demo-subfolder 119 | 120 | - name: Confirm we can run specs in subfolder 121 | uses: cypress-io/github-action@v6 122 | with: 123 | # we have already installed all dependencies above 124 | install: false 125 | config-file: mocks/my-app/e2e/cypress.config.js 126 | 127 | - name: Count all tests 📊 128 | run: npm run demo-test-counts 129 | 130 | - name: Show specs with tags 131 | id: find-tagged 132 | run: node ./bin/find --tagged @user --set-gha-outputs 133 | - name: Print the found specs with tags 134 | run: | 135 | echo '${{ steps.find-tagged.outputs.taggedSpecsN }} Specs with tests tagged user' >> "$GITHUB_STEP_SUMMARY" 136 | echo '${{ steps.find-tagged.outputs.taggedSpecs }}' >> "$GITHUB_STEP_SUMMARY" 137 | 138 | - name: Create HTML test report 139 | run: npm run test-report 140 | - name: Save HTML test report 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: report 144 | path: report/index.html 145 | 146 | - name: Deploy HTML test report to GH Pages 147 | if: github.ref == 'refs/heads/main' 148 | uses: actions/upload-pages-artifact@v3 149 | with: 150 | path: report 151 | 152 | - name: Publish the static report as static page 153 | if: github.ref == 'refs/heads/main' 154 | id: deployment 155 | uses: actions/deploy-pages@v4 156 | 157 | - name: Semantic Release 🚀 158 | uses: cycjimmy/semantic-release-action@v4 159 | if: github.ref == 'refs/heads/main' 160 | with: 161 | branch: main 162 | env: 163 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 164 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 165 | -------------------------------------------------------------------------------- /.github/workflows/merge.yml: -------------------------------------------------------------------------------- 1 | name: merge 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-22.04 6 | steps: 7 | - name: Checkout 🛎 8 | uses: actions/checkout@v3 9 | 10 | - name: Install dependencies 11 | # https://github.com/cypress-io/github-action 12 | uses: cypress-io/github-action@v5 13 | with: 14 | runTests: false 15 | 16 | - name: Show specs with tag alpha 17 | id: find-alpha 18 | run: node ./bin/find --tagged @alpha --set-gha-outputs --gha-summary 19 | 20 | - name: Show specs with tag user 21 | id: find-user 22 | run: node ./bin/find --tagged @user --set-gha-outputs 23 | 24 | # https://docs.github.com/en/actions/learn-github-actions/expressions 25 | - name: Merge two lists of specs 26 | id: merge 27 | # the tricky part is adding two numbers and setting the sum as GHA output 28 | # note: the specs might have duplicate entries 29 | run: | 30 | echo 'specs=${{ steps.find-alpha.outputs.taggedSpecs }},${{ steps.find-user.outputs.taggedSpecs }}' >> "$GITHUB_OUTPUT" 31 | echo 'specsN='`expr ${{ steps.find-alpha.outputs.taggedSpecsN }} + ${{ steps.find-user.outputs.taggedSpecsN }}` >> "$GITHUB_OUTPUT" 32 | 33 | - name: Print all specs 34 | run: | 35 | echo '${{ steps.merge.outputs.specsN }} Specs with tests tagged user' >> "$GITHUB_STEP_SUMMARY" 36 | echo '${{ steps.merge.outputs.specs }}' >> "$GITHUB_STEP_SUMMARY" 37 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | on: pull_request 3 | jobs: 4 | changed-files: 5 | runs-on: ubuntu-22.04 6 | steps: 7 | - name: Checkout 🛎 8 | # https://github.com/actions/checkout 9 | uses: actions/checkout@v4 10 | with: 11 | fetch-depth: 20 12 | 13 | - name: Install dependencies 📦 14 | # https://github.com/cypress-io/github-action 15 | uses: cypress-io/github-action@v6 16 | with: 17 | runTests: false 18 | 19 | - name: Print dependencies between tests and utils 💻 20 | run: npm run deps 21 | 22 | - name: Print files changed against the parent of this branch 🌳 23 | run: | 24 | git --version 25 | git status 26 | git fetch 27 | git log --pretty=format:"%h %s" --graph --since=1.month 28 | git merge-base origin/main HEAD 29 | git diff --name-only --diff-filter=AMR $(git merge-base origin/main HEAD).. 30 | 31 | - name: Print specs changed against the parent of this branch 🌳 32 | # and set GitHub Actions output 33 | id: step1 34 | run: node ./bin/find --branch main --parent --set-gha-outputs --gha-summary --specs-per-machine 4 --max-machines 2 35 | 36 | - name: Print set outputs 37 | run: echo ${{ steps.step1.outputs.changedSpecsN }} specs ${{ steps.step1.outputs.changedSpecs }} machines ${{ steps.step1.outputs.machinesNeeded }} 38 | 39 | - name: Print changed specs if you trace imports and requires 🔭 40 | # in the folder "cypress" 41 | id: step2 42 | run: node ./bin/find --branch main --parent --trace-imports cypress --set-gha-outputs 43 | 44 | - name: Print set outputs 45 | run: echo ${{ steps.step2.outputs.changedSpecsN }} ${{ steps.step2.outputs.changedSpecs }} 46 | 47 | - name: Saving traced dependencies 48 | run: node ./bin/find --branch main --parent --trace-imports cypress --cache-trace --time-trace 49 | 50 | - name: Loading cached traced dependencies 51 | run: node ./bin/find --branch main --parent --trace-imports cypress --cache-trace 52 | 53 | - name: Print specs sorted by last modified date 54 | run: node ./bin/find --branch main --parent --sort-by-modified --set-gha-outputs 55 | 56 | # module and import support 57 | - name: Test module support 58 | run: npm run demo-tests 59 | working-directory: test-modules 60 | 61 | - name: Trace TS path aliases demo 62 | run: npm run deps 63 | working-directory: test-ts-aliases 64 | 65 | - name: Test cypress.json project (Cypress v9) 66 | run: npm run demo-tests --prefix test-json 67 | 68 | - name: Find tests in subfolder 69 | run: npm run demo-subfolder 70 | 71 | - name: Demo test names in Markdown format and put into job summary 💻 72 | run: npm run demo-names-markdown >> "$GITHUB_STEP_SUMMARY" 73 | 74 | - name: Show specs with tags 75 | id: find-tagged 76 | run: node ./bin/find --tagged @user --set-gha-outputs 77 | - name: Print the found specs with tags 78 | run: | 79 | echo '${{ steps.find-tagged.outputs.taggedSpecsN }} Specs with tests tagged user' >> "$GITHUB_STEP_SUMMARY" 80 | echo '${{ steps.find-tagged.outputs.taggedSpecs }}' >> "$GITHUB_STEP_SUMMARY" 81 | 82 | - name: Confirm we can run specs in subfolder 83 | uses: cypress-io/github-action@v6 84 | with: 85 | # we have already installed all dependencies above 86 | install: false 87 | config-file: mocks/my-app/e2e/cypress.config.js 88 | 89 | - name: Produce HTML report 90 | run: node ./bin/find --names --write-html-filename report/index.html 91 | - name: Save HTML report 92 | uses: actions/upload-artifact@v4 93 | with: 94 | name: report 95 | path: report/index.html 96 | 97 | - name: Test HTML output 98 | uses: cypress-io/github-action@v6 99 | with: 100 | install: false 101 | config: specPattern=cypress/html-e2e/**/*.cy.js 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | deps.json 5 | report/ 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.11.0 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # find-cypress-specs [![renovate-app badge][renovate-badge]][renovate-app] ![cypress version](https://img.shields.io/badge/cypress-14.2.0-brightgreen) [![ci](https://github.com/bahmutov/find-cypress-specs/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bahmutov/find-cypress-specs/actions/workflows/ci.yml) 2 | 3 | > Find Cypress spec files using the config settings 4 | 5 | ![Cypress tests](https://img.shields.io/badge/cy%20tests-E2E%206%20%7C%20component%202-blue) 6 | 7 | ```bash 8 | $ npx find-cypress-specs 9 | # prints all spec files separated by a comma 10 | cypress/e2e/spec.js,cypress/e2e/featureA/user.js 11 | ``` 12 | 13 | Supports JS and TS specs 14 | 15 | ## Count 16 | 17 | You can count the number of specs found 18 | 19 | ```bash 20 | $ npx find-cypress-specs --count 21 | 2 22 | ``` 23 | 24 | ## Component specs 25 | 26 | By default, it finds the E2E specs and tests. You can find component specs using `--component` CLI option 27 | 28 | ``` 29 | $ npx find-cypress-specs --component 30 | ``` 31 | 32 | ## against branch 33 | 34 | By default, this module simply prints all spec filenames. You can add `--branch` parameter to only print the specs changed against that `origin/branch`. 35 | 36 | ```bash 37 | $ npx find-cypress-specs --branch main 38 | # prints only some specs, the ones that have changed against the "origin/main" 39 | ``` 40 | 41 | ## find specs by part of the test title 42 | 43 | You can grep all test titles (only the test titles, not the suite names) and find the list of specs 44 | 45 | ```bash 46 | $ npx find-cypress-specs --grep "works, needs" 47 | ``` 48 | 49 | This will find all specs with tests that have "works" in the title or "needs" in the title and output the list of specs. If you use `--set-gha-outputs` and GitHub Actions, it sets the outputs `grepSpecs` and `grepSpecsN`. 50 | 51 | ## find number of machines 52 | 53 | If we find all the changed specs to run, we might need to decide how many machines we need. We can do a rough job by specifying the number of specs per machine plus the max number. 54 | 55 | ``` 56 | $ npx find-cypress-specs --branch main --specs-per-machine 4 --max-machines 5 57 | ``` 58 | 59 | For now, it is only useful when setting the GHA outputs. 60 | 61 | ## set GitHub Actions outputs 62 | 63 | If you add `--set-gha-outputs` command line switch, then the number of changed specs and the comma-separated file list will be set as GH Actions outputs `changedSpecsN` and `changedSpecs`. See [pr.yml](./.github/workflows/pr.yml) for example 64 | 65 | ```yml 66 | - name: Print specs changed against the parent of this branch 🌳 67 | # and set GitHub Actions output 68 | id: step1 69 | run: node ./bin/find --branch main --parent --set-gha-outputs 70 | 71 | - name: Print set outputs 72 | run: echo ${{ steps.step1.outputs.changedSpecsN }} ${{ steps.step1.outputs.changedSpecs }} 73 | ``` 74 | 75 | If you set the number of machines, it will set the output `machinesNeeded` 76 | 77 | ## Write GitHub Actions job summary 78 | 79 | You can output changes specs by using the parameter `--gha-summary` 80 | 81 | ## against the parent commit 82 | 83 | When dealing with a long-term branch, you do not want to see the changed files in the main branch. Instead, you want to only consider the specs changed in the _current_ branch all the way to its parent commit. You can pass the flag `--parent` to only pick the modified and added specs. 84 | 85 | ```bash 86 | $ npx find-cypress-specs --branch main --parent 87 | # same as 88 | # git diff --name-only --diff-filter=AMR $(git merge-base origin/main HEAD).. 89 | ``` 90 | 91 | Note: to get the changed files, we need to fetch the repo, see [pr.yml](./.github/workflows/pr.yml) 92 | 93 | ``` 94 | $ checkout 95 | $ git fetch 96 | $ npx find-cypress-specs --branch main --parent 97 | ``` 98 | 99 | ## with traced import and require statements 100 | 101 | Imagine you open a pull request and only change something in an `utils.js` file used by other specs. Which specs should you run? By default `--branch main --parent` would not find any changed specs, so no specs would execute, and you will have to run _all_ specs just to be safe. This program has a mode `--trace-imports ` which uses [spec-change](https://github.com/bahmutov/spec-change) to inspect JS/TS files and find dependencies between them. Thus it can discover that when `utils.js` changes, the specs that `import './utils'` or `require('./utils')` should also be considered modified. 102 | 103 | ``` 104 | $ npx find-cypress-specs --branch main --parent --trace-imports cypress 105 | ``` 106 | 107 | **Note:** the argument is the subfolder name to limit the number of files to inspect when tracing the imports. 108 | 109 | You can time how long tracing takes by adding option `--time-trace` to the command line arguments. You can also saved traced dependencies using `--cache-trace` argument. Next time the dependencies will be loaded from the file without recomputing. This is convenient on CI to avoid recomputing them. For example, if you need the number of affected files and their filenames 110 | 111 | ``` 112 | # get the number of affected specs 113 | $ npx find-cypress-specs --branch main --parent --trace-imports cypress --cache-trace --count 114 | # quickly get the affected specs without recomputing the dependencies 115 | $ npx find-cypress-specs --branch main --parent --trace-imports cypress --cache-trace 116 | ``` 117 | 118 | The cached trace will be saved in file `deps.json`, you probably want to Git ignore it. 119 | 120 | You can limit the number of added traced files using the `--max-added-traced-specs ` parameter. This avoids ALL specs added when you change some common utility that many specs import. 121 | 122 | ### number of changed files 123 | 124 | You can print just the number of changed specs 125 | 126 | ```bash 127 | $ npx find-cypress-specs --branch main --count 128 | # prints the number of spec files changed against the branch "origin/main" 129 | 5 130 | ``` 131 | 132 | ### filter by a tag 133 | 134 | You can filter all changed specs and only report (and count) the specs that have changed AND include the given tag(s) 135 | 136 | ```bash 137 | $ npx find-cypress-specs --branch main --tagged @user,@preview 138 | # prints only some specs, the ones that have changed against the "origin/main" 139 | # and that have any tests or suites inside tagged "@user" or "@preview" 140 | ``` 141 | 142 | You can set the list and number of specs with tags into GHA outputs 143 | 144 | ```bash 145 | $ npx find-cypress-specs --branch main --tagged @user,@preview --set-gha-outputs 146 | ``` 147 | 148 | The number of found specs will be set as `taggedSpecsN` and the list will be set as `taggedSpecs`. 149 | 150 | You can find tests / suites tagged with several tags together using AND syntax `tag1+tag2`. For example, to find all `@user` tests also tagged `@smoke` 151 | 152 | ```bash 153 | $ npx find-cypress-specs --names --tagged @user+@smoke 154 | ``` 155 | 156 | ### count tagged changed specs 157 | 158 | Let's say we changed 2 specs that have tests tagged `@user`. We can output the count by adding `--count` option 159 | 160 | ```bash 161 | $ npx find-cypress-specs --branch main --tagged @user --count 162 | 2 163 | ``` 164 | 165 | ## Specs sorted by the Git modified timestamp 166 | 167 | You can find all specs and output their list sorted by the last modified Git timestamp. The latest modified specs will be listed first. This feature can help you run the latest specs first. 168 | 169 | ```bash 170 | $ npx find-cypress-specs --sort-by-modified 171 | ``` 172 | 173 | You can set the spec filenames as GHA outputs 174 | 175 | ```bash 176 | # sets GitHub Actions outputs "changedSpecsN" and "changedSpecs" 177 | $ npx find-cypress-specs --sort-by-modified --set-gha-outputs 178 | ``` 179 | 180 | **Important:** to get the right commit dates, Git needs the full repository history. For example, when using the GHA to check out the repo, fetch the entire history: 181 | 182 | ```yml 183 | - name: Checkout 🛎 184 | uses: actions/checkout@v4 185 | with: 186 | # fetch all history to get the full commit history 187 | # this is needed to get the correct order of specs 188 | # by modified Git timestamp 189 | fetch-depth: 0 190 | ``` 191 | 192 | 📝 Read the blog post [Run Changed Cypress Specs On CI First](https://glebbahmutov.com/blog/run-changed-specs-first/). 193 | 194 | ## Test names 195 | 196 | You can print each spec file with the suite and test names inside of it (found using [find-test-names](https://github.com/bahmutov/find-test-names)) 197 | 198 | ```bash 199 | $ npx find-cypress-specs --names 200 | # prints something like 201 | 202 | cypress/e2e/spec.js (2 tests) 203 | └─ parent suite [@main] 204 | ├─ works well enough 205 | └─ inner suite 206 | └─ shows something [@user] 207 | 208 | cypress/e2e/featureA/user.js (2 tests, 1 pending) 209 | ├─ works 210 | └⊙ needs to be written 211 | 212 | found 2 specs (4 tests, 1 pending) 213 | ``` 214 | 215 | Where the tags are listed inside `[ ... ]` (see [@bahmutov/cy-grep](https://github.com/bahmutov/cy-grep)) and the [pending tests](https://glebbahmutov.com/blog/cypress-test-statuses/) are marked with `⊙` character. 216 | 217 | Required tags are marked with `[[ ... ]]`. 218 | 219 | You can print the results in JSON format using `--json` or `-j` option. 220 | 221 | You can print the tests in Markdown format using `--markdown` or `--md` option 222 | 223 | | Spec | 224 | | -------------------------------- | 225 | | **`foo/a/spec.cy.js`** (3 tests) | 226 | | `suite / test 1` | 227 | | `suite / test 2` | 228 | | `suite / test 3` | 229 | | **`foo/b/spec.cy.js`** (1 test) | 230 | | `another / test / test 1` | 231 | 232 | ## Test tags 233 | 234 | You can count tags attached to the individual tests using `--tags` arguments 235 | 236 | ``` 237 | $ npx find-cypress-specs --tags 238 | # prints the tags table sorted by tag 239 | 240 | Tag Tests 241 | ----- ---------- 242 | @sign 1 243 | @user 2 244 | ``` 245 | 246 | Each tag count includes the tests that use the tag directly, and the _effective_ tags applied from the parent suites, both `tags` and `requiredTags`. 247 | 248 | You can print the results in JSON format using `--json` or `-j` option. 249 | 250 | ## Test names filtered by a tag 251 | 252 | ```bash 253 | $ npx find-cypress-specs --names --tagged 254 | # finds all specs and tests, then filters the output by a single tag 255 | ``` 256 | 257 | ## Test names filtered by multiple tags 258 | 259 | ```bash 260 | $ npx find-cypress-specs --names --tagged ,,,... 261 | # finds all specs and tests, then filters the output showing all tests 262 | # tagged with tag1 or tag2 or tag3 or ... 263 | ``` 264 | 265 | ## File names filtered by a tag 266 | 267 | ``` 268 | $ npx find-cypress-specs --tagged 269 | # finds all specs and tests, then filters the output showing only the file names associated with the tests 270 | 271 | 272 | # cypress/e2e/spec.cy.js,cypress/e2e/featureA/user.cy.ts 273 | ``` 274 | 275 | If you pass an empty string argument like `--tagged ''`, an empty list is returned. 276 | 277 | You can print the number of found tagged specs by adding `--count` argument 278 | 279 | ``` 280 | $ npx find-cypress-specs --tagged --count 281 | 3 282 | ``` 283 | 284 | ## File names filtered by multiple tags 285 | 286 | ``` 287 | $ npx find-cypress-specs --tagged ,,,... 288 | # finds all specs and tests, then filters the output showing only the file names associated with the tests 289 | # tagged with tag1 or tag2 or tag3 or ... 290 | ``` 291 | 292 | ## Show only the pending tests 293 | 294 | You can show only the tests marked with "it.skip" which are called "pending" according to Mocha / Cypress [terminology](https://glebbahmutov.com/blog/cypress-test-statuses/). 295 | 296 | ```bash 297 | $ npx find-cypress-specs --names --pending 298 | # --skipped is an alias to --pending 299 | $ npx find-cypress-specs --names --skipped 300 | # prints and counts only the pending tests 301 | cypress/e2e/featureA/user.cy.ts (1 test, 1 pending) 302 | └⊙ needs to be written [@alpha] 303 | 304 | found 1 spec (1 test, 1 pending) 305 | ``` 306 | 307 | ## Print skipped tests 308 | 309 | ``` 310 | $ npx find-cypress-specs --names --skipped 311 | ``` 312 | 313 | Prints each spec that has skipped tests. 314 | 315 | ## Count tests 316 | 317 | You can see the total number of E2E and component tests 318 | 319 | ``` 320 | $ npx find-cypress-specs --test-counts 321 | 4 e2e tests, 2 component tests 322 | ``` 323 | 324 | ### Update README badge 325 | 326 | You can set or update a badge in README with test counts by adding the `--update-badge` argument 327 | 328 | ``` 329 | $ npx find-cypress-specs --test-counts --update-badge 330 | 4 e2e tests, 2 component tests 331 | ⚠️ Could not find test count badge 332 | Insert new badge on the first line 333 | saving updated readme with new test counts 334 | ``` 335 | 336 | See the [badges.yml workflow](./.github/workflows/badges.yml) 337 | 338 | ## Count skipped tests 339 | 340 | Prints the single number with the count of skipped tests 341 | 342 | ``` 343 | $ npx find-cypress-specs --names --skipped --count 344 | 345 | 5 346 | ``` 347 | 348 | ## cypress.config.ts 349 | 350 | If the project uses TypeScript and `cypress.config.ts` then this module uses [tsx](https://github.com/privatenumber/tsx) to load the config and fetch the spec pattern. 351 | 352 | If you are using `import` keyword in your `cypress.config.ts` you might get an error like this: 353 | 354 | ``` 355 | import { defineConfig } from 'cypress'; 356 | ^^^^^^ 357 | 358 | SyntaxError: Cannot use import statement outside a module 359 | ``` 360 | 361 | In that case, add to your `tsconfig.json` file the `ts-node` block: 362 | 363 | ```json 364 | { 365 | "ts-node": { 366 | "compilerOptions": { 367 | "module": "commonjs" 368 | } 369 | } 370 | } 371 | ``` 372 | 373 | See example in [bahmutov/test-todomvc-using-app-actions](https://github.com/bahmutov/test-todomvc-using-app-actions). 374 | 375 | **Tip:** read my blog post [Convert Cypress Specs from JavaScript to TypeScript](https://glebbahmutov.com/blog/cypress-js-to-ts/). 376 | 377 | ## Custom config filename 378 | 379 | If you want to use a custom Cypress config, pass it via the environment variable `CYPRESS_CONFIG_FILE` 380 | 381 | ``` 382 | $ CYPRESS_CONFIG_FILE=path/to/cypress.config.js npx find-cypres-specs ... 383 | ``` 384 | 385 | ## Absolute spec filenames 386 | 387 | You can return absolute filenames to the found specs 388 | 389 | ```js 390 | getSpecs(config, 'e2e|component', true) 391 | ``` 392 | 393 | ## HTML Test Report 394 | 395 | You can generate a static self-contained HTML report with all specs, tests, and test tags 396 | 397 | ```bash 398 | $ npx find-cypress-specs --write-html-filename report/index.html 399 | ``` 400 | 401 | Open the HTML report in your browser. The report should show the tests organized by spec / suite plus all test tags and allow filtering tests by the checked tags. 402 | 403 | ![The report](./images/report.png) 404 | 405 | See an example report at [https://glebbahmutov.com/find-cypress-specs/](https://glebbahmutov.com/find-cypress-specs/) 406 | 407 | ## Details 408 | 409 | Cypress uses the resolved [configuration values](https://on.cypress.io/configuration) to find the spec files to run. It searches the `integrationFolder` for all patterns listed in `testFiles` and removes any files matching the `ignoreTestFiles` patterns. 410 | 411 | You can see how Cypress finds the specs using `DEBUG=cypress:cli,cypress:server:specs` environment variable to see verbose logs. The logic should be in the file `packages/server/lib/util/specs.ts` in the repo [cypress-io/cypress](https://github.com/cypress-io/cypress) 412 | 413 | ## Debugging 414 | 415 | Run the utility with environment variable `DEBUG=find-cypress-specs` to see the verbose logs 416 | 417 | ![Debug output](./images/debug.png) 418 | 419 | Finding tests in the individual specs uses [find-test-names](https://github.com/bahmutov/find-test-names) so you might want to enable debugging both modules at once: 420 | 421 | ``` 422 | $ DEBUG=find-cypress-specs,find-test-names npx find-cypress-specs --names 423 | ``` 424 | 425 | To debug finding changed specs against a branch, use `find-cypress-specs:git` 426 | 427 | ``` 428 | $ DEBUG=find-cypress-specs:git npx find-cypress-specs --branch main 429 | ``` 430 | 431 | ## Videos 432 | 433 | - [Use Ava Snapshots And Execa-wrap To Write End-to-End Tests For CLI Utilities](https://youtu.be/rsw17RqP0G0) 434 | 435 | ## Examples 436 | 437 | - 📝 blog post [Run Changed Traced Specs On GitHub Actions](https://glebbahmutov.com/blog/trace-changed-specs/) 438 | - 📝 blog post [Quickly Run The Changed Specs on GitHub Actions](https://glebbahmutov.com/blog/quick-changed-specs/) 439 | - [chat.io](https://github.com/bahmutov/chat.io) as described in the blog post [Get Faster Feedback From Your Cypress Tests Running On CircleCI](https://glebbahmutov.com/blog/faster-ci-feedback-on-circleci/) 440 | 441 | ## NPM module 442 | 443 | You can use this module via its NPM module API. 444 | 445 | ### getSpecs 446 | 447 | ```js 448 | const { getSpecs } = require('find-cypress-specs') 449 | // somewhere in the cypress.config.js 450 | setupNodeEvents(on, config) { 451 | const specs = getSpecs(config) 452 | // specs is a list of filenames 453 | } 454 | ``` 455 | 456 | You can pass the `config` object to the `getSpecs` method. If there is no `config` parameter, it will read the config file automatically. 457 | 458 | ```js 459 | const specs = getSpecs({ 460 | e2e: { 461 | specPattern: '*/e2e/featureA/*.cy.ts', 462 | }, 463 | }) 464 | // ['cypress/e2e/featureA/spec.cy.ts'] 465 | ``` 466 | 467 | ### getTests 468 | 469 | Returns an object with individual test information 470 | 471 | ```js 472 | const { getTests } = require('find-cypress-specs') 473 | const { jsonResults, tagTestCounts } = getTests() 474 | // jsonResults is an object 475 | // with an entry per spec file 476 | ``` 477 | 478 | See [get-tests.js](./test/npm/get-tests.js) for details and examples. 479 | 480 | ## Small print 481 | 482 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2022 483 | 484 | - [@bahmutov](https://twitter.com/bahmutov) 485 | - [glebbahmutov.com](https://glebbahmutov.com) 486 | - [blog](https://glebbahmutov.com/blog) 487 | - [videos](https://www.youtube.com/glebbahmutov) 488 | - [presentations](https://slides.com/bahmutov) 489 | - [cypress.tips](https://cypress.tips) 490 | - [Cypress Tips&Tricks](https://cypresstips.substack.com/) newsletter 491 | - [my Cypress courses](https://cypress.tips/courses) 492 | 493 | License: MIT - do anything with the code, but don't blame me if it does not work. 494 | 495 | Support: if you find any problems with this module, email / tweet / 496 | [open issue](https://github.com/bahmutov/find-cypress-specs/issues) on Github 497 | 498 | ## MIT License 499 | 500 | Copyright (c) 2022 Gleb Bahmutov <gleb.bahmutov@gmail.com> 501 | 502 | Permission is hereby granted, free of charge, to any person 503 | obtaining a copy of this software and associated documentation 504 | files (the "Software"), to deal in the Software without 505 | restriction, including without limitation the rights to use, 506 | copy, modify, merge, publish, distribute, sublicense, and/or sell 507 | copies of the Software, and to permit persons to whom the 508 | Software is furnished to do so, subject to the following 509 | conditions: 510 | 511 | The above copyright notice and this permission notice shall be 512 | included in all copies or substantial portions of the Software. 513 | 514 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 515 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 516 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 517 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 518 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 519 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 520 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 521 | OTHER DEALINGS IN THE SOFTWARE. 522 | 523 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 524 | [renovate-app]: https://renovateapp.com/ 525 | -------------------------------------------------------------------------------- /bin/find.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const arg = require('arg') 4 | const { getSpecs, findChangedFiles, getTests } = require('../src') 5 | const { getTestCounts } = require('../src/tests-counts') 6 | const { stringAllInfo, stringMarkdownTests } = require('../src/print') 7 | const { updateBadge } = require('../src/badge') 8 | const { filterByGrep } = require('../src/grep') 9 | const { toHtml } = require('../src/output-html') 10 | const fs = require('fs') 11 | const path = require('path') 12 | const { getTestNames, countTags } = require('find-test-names') 13 | const consoleTable = require('console.table') 14 | const debug = require('debug')('find-cypress-specs') 15 | const { getDependsInFolder } = require('spec-change') 16 | const core = require('@actions/core') 17 | const pluralize = require('pluralize') 18 | const shell = require('shelljs') 19 | 20 | const args = arg({ 21 | '--names': Boolean, 22 | '--tags': Boolean, 23 | // output in JSON format 24 | '--json': Boolean, 25 | // find the specs that have changed against this Git branch 26 | '--branch': String, 27 | // find the specs that have changed against the parent of the branch 28 | '--parent': Boolean, 29 | '--count': Boolean, 30 | // filter all tests to those that have the given tag 31 | '--tagged': String, 32 | // print only the "it.only" tests 33 | '--skipped': Boolean, 34 | // when finding specs changed against a given parent of the branch 35 | // also look at the import and require statements to trace dependencies 36 | // and consider specs that import a changes source file changed to 37 | // The value of this argument is the subfolder with Cypress tests, like "cypress" 38 | '--trace-imports': String, 39 | // when enabled, this code uses GitHub Actions Core package 40 | // to set two named outputs, one for number of changed specs 41 | // another for actual list of files 42 | '--set-gha-outputs': Boolean, 43 | // print a summary to the GitHub Actions job summary 44 | '--gha-summary': Boolean, 45 | // save a JSON file with traced dependencies to save time 46 | '--cache-trace': Boolean, 47 | '--time-trace': Boolean, 48 | // do not add more than this number of extra specs after tracing 49 | '--max-added-traced-specs': Number, 50 | // find component specs 51 | '--component': Boolean, 52 | // count total number of E2E and component tests 53 | '--test-counts': Boolean, 54 | // if we count the tests, we can update the README badge 55 | '--update-badge': Boolean, 56 | // output the list in Markdown format 57 | '--markdown': Boolean, 58 | // optional: output the number of machines needed to run the tests 59 | '--specs-per-machine': Number, 60 | '--max-machines': Number, 61 | // find all specs and tests with part of the title 62 | '--grep': String, 63 | // output all files sorted by the last modified date 64 | '--sort-by-modified': Boolean, 65 | // output the HTML file with all tests 66 | '--write-html-filename': String, 67 | // aliases 68 | '-n': '--names', 69 | '--name': '--names', 70 | '-t': '--tags', 71 | '--tag': '--tags', 72 | '-j': '--json', 73 | '-b': '--branch', 74 | '--deps': '--trace-imports', 75 | // Cypress test status (just like Mocha) 76 | // calls "it.skip" pending tests 77 | // https://glebbahmutov.com/blog/cypress-test-statuses/ 78 | '--pending': '--skipped', 79 | '--tc': '--test-counts', 80 | '--md': '--markdown', 81 | }) 82 | 83 | debug('arguments %o', args) 84 | 85 | if (args['--test-counts']) { 86 | debug('finding test counts') 87 | const { nE2E, nComponent } = getTestCounts() 88 | console.log( 89 | '%d e2e %s, %d component %s', 90 | nE2E, 91 | pluralize('test', nE2E, false), 92 | nComponent, 93 | pluralize('test', nComponent, false), 94 | ) 95 | if (args['--update-badge']) { 96 | debug('updating the README test count badge') 97 | updateBadge({ nE2E, nComponent }) 98 | } 99 | } else { 100 | const specType = args['--component'] ? 'component' : 'e2e' 101 | 102 | const specs = getSpecs(undefined, specType) 103 | 104 | if (args['--sort-by-modified']) { 105 | // get the git source code last modified date for each spec filename 106 | const specsWithDates = specs.map((spec) => { 107 | // https://git-scm.com/docs/pretty-formats 108 | const output = shell.exec(`git log -1 --pretty=format:"%ct" -- ${spec}`, { 109 | silent: true, 110 | }) 111 | const lastModifiedTimestamp = output.toString().trim() 112 | return { 113 | filename: spec, 114 | lastModifiedTimestamp, 115 | human: new Date(lastModifiedTimestamp * 1000).toLocaleString(), 116 | } 117 | }) 118 | const sortedSpecs = specsWithDates.sort( 119 | (a, b) => b.lastModifiedTimestamp - a.lastModifiedTimestamp, 120 | ) 121 | debug('specs sorted by the last modified Git date') 122 | debug(sortedSpecs) 123 | 124 | const sortedSpecNames = sortedSpecs.map((spec) => spec.filename) 125 | console.log(sortedSpecNames.join(',')) 126 | 127 | if (args['--set-gha-outputs']) { 128 | debug('setting GitHub Actions outputs changedSpecsN and changedSpecs') 129 | debug('changedSpecsN %d', sortedSpecNames.length) 130 | debug('plus changedSpecs') 131 | core.setOutput('changedSpecsN', sortedSpecNames.length) 132 | core.setOutput('changedSpecs', sortedSpecNames.join(',')) 133 | } 134 | process.exit(0) 135 | } 136 | 137 | // if the user passes "--tagged ''" we want to find the specs 138 | // but then filter them all out 139 | const isTaggedPresent = args['--tagged'] || args['--tagged'] === '' 140 | 141 | if (args['--branch']) { 142 | debug('determining specs changed against branch %s', args['--branch']) 143 | let changedFiles = findChangedFiles( 144 | args['--branch'], 145 | Boolean(args['--parent']), 146 | ) 147 | debug('changed files %o', changedFiles) 148 | debug('comparing against the specs %o', specs) 149 | if (args['--trace-imports']) { 150 | debug('tracing dependent changes in folder %s', args['--trace-imports']) 151 | 152 | const saveDependenciesFile = 'deps.json' 153 | let deps 154 | if (args['--cache-trace']) { 155 | if (fs.existsSync(saveDependenciesFile)) { 156 | debug( 157 | 'loading cached traced dependencies from file %s', 158 | saveDependenciesFile, 159 | ) 160 | deps = JSON.parse(fs.readFileSync(saveDependenciesFile, 'utf-8')).deps 161 | } 162 | } 163 | 164 | if (!deps) { 165 | const absoluteFolder = path.join(process.cwd(), args['--trace-imports']) 166 | const depsOptions = { 167 | folder: absoluteFolder, 168 | time: Boolean(args['--time-trace']), 169 | } 170 | if (args['--cache-trace']) { 171 | depsOptions.saveDepsFilename = saveDependenciesFile 172 | debug( 173 | 'will save found dependencies into the file %s', 174 | saveDependenciesFile, 175 | ) 176 | } 177 | debug('tracing options %o', depsOptions) 178 | deps = getDependsInFolder(depsOptions) 179 | } 180 | debug('traced dependencies via imports and require') 181 | debug(deps) 182 | 183 | // add a sensible limit to the number of extra specs to add 184 | // when we trace the dependencies in the changed source files 185 | const addedTracedFiles = [] 186 | const maxAddTracedFiles = args['--max-added-traced-specs'] || 1000 187 | debug('maximum traced files to add %d', maxAddTracedFiles) 188 | 189 | Object.entries(deps).forEach(([filename, fileDependents]) => { 190 | const f = path.join(args['--trace-imports'], filename) 191 | if (changedFiles.includes(f)) { 192 | debug( 193 | 'the source file %s has changed, including its dependents %o in the list of changed files', 194 | f, 195 | fileDependents, 196 | ) 197 | fileDependents.forEach((name) => { 198 | const nameInCypressFolder = path.join(args['--trace-imports'], name) 199 | if (!changedFiles.includes(nameInCypressFolder)) { 200 | if (addedTracedFiles.length < maxAddTracedFiles) { 201 | changedFiles.push(nameInCypressFolder) 202 | addedTracedFiles.push(nameInCypressFolder) 203 | } 204 | } 205 | }) 206 | } 207 | }) 208 | debug( 209 | 'added %d traced specs %o', 210 | addedTracedFiles.length, 211 | addedTracedFiles, 212 | ) 213 | } 214 | 215 | let changedSpecs = specs.filter((file) => changedFiles.includes(file)) 216 | debug('changed %d specs %o', changedSpecs.length, changedSpecs) 217 | 218 | if (args['--tagged']) { 219 | const splitTags = args['--tagged'] 220 | .split(',') 221 | .map((s) => s.trim()) 222 | .filter(Boolean) 223 | debug('filtering changed specs by tags %o', splitTags) 224 | changedSpecs = changedSpecs.filter((file) => { 225 | const source = fs.readFileSync(file, 'utf8') 226 | const result = getTestNames(source, true) 227 | const specTagCounts = countTags(result.structure) 228 | // debug(specTagCounts) 229 | const specHasTags = Object.keys(specTagCounts).some((tag) => 230 | splitTags.includes(tag), 231 | ) 232 | debug('spec %s has any of the tags? %o', file, specHasTags) 233 | return specHasTags 234 | }) 235 | } 236 | 237 | let machinesNeeded 238 | if (args['--specs-per-machine'] > 0 && args['--max-machines'] > 0) { 239 | const specsPerMachine = args['--specs-per-machine'] 240 | const maxMachines = args['--max-machines'] 241 | machinesNeeded = Math.min( 242 | Math.ceil(changedSpecs.length / specsPerMachine), 243 | maxMachines, 244 | ) 245 | debug( 246 | 'specs per machine %d with max %d machines', 247 | specsPerMachine, 248 | maxMachines, 249 | ) 250 | debug( 251 | 'from %d specs, set %d output machinesNeeded', 252 | changedSpecs.length, 253 | machinesNeeded, 254 | ) 255 | } 256 | 257 | if (args['--set-gha-outputs']) { 258 | debug('setting GitHub Actions outputs changedSpecsN and changedSpecs') 259 | debug('changedSpecsN %d', changedSpecs.length) 260 | debug('plus changedSpecs') 261 | core.setOutput('changedSpecsN', changedSpecs.length) 262 | core.setOutput('changedSpecs', changedSpecs.join(',')) 263 | core.setOutput('machinesNeeded', machinesNeeded) 264 | } 265 | 266 | if (args['--gha-summary']) { 267 | debug('writing GitHub Actions summary') 268 | let summary 269 | 270 | if (changedSpecs.length > 0) { 271 | summary = `Found that ${pluralize( 272 | 'spec', 273 | changedSpecs.length, 274 | true, 275 | )} changed: ${changedSpecs.join(', ')}` 276 | if (machinesNeeded > 0) { 277 | summary += `\nestimated ${pluralize( 278 | 'machine', 279 | machinesNeeded, 280 | true, 281 | )} needed` 282 | } 283 | } else { 284 | summary = 'No specs changed' 285 | } 286 | if (process.env.GITHUB_STEP_SUMMARY) { 287 | core.summary.addRaw(summary).write() 288 | } else { 289 | console.log('GitHub summary') 290 | console.log(summary) 291 | } 292 | } 293 | 294 | if (args['--count']) { 295 | console.log(changedSpecs.length) 296 | } else { 297 | console.log(changedSpecs.join(',')) 298 | } 299 | } else if ( 300 | args['--names'] || 301 | args['--tags'] || 302 | isTaggedPresent || 303 | args['--write-html-filename'] 304 | ) { 305 | // counts the number of tests for each tag across all specs 306 | debug('count number of tests') 307 | const needAllTags = args['--tags'] || args['--write-html-filename'] 308 | const { jsonResults, tagTestCounts } = getTests(specs, { 309 | tags: needAllTags, 310 | tagged: args['--tagged'], 311 | skipped: args['--skipped'], 312 | }) 313 | debug('json results from getTests') 314 | debug(jsonResults) 315 | debug('tag test counts') 316 | debug(tagTestCounts) 317 | 318 | if (args['--names']) { 319 | debug('output test names') 320 | if (args['--count']) { 321 | debug('names and count') 322 | let n = 0 323 | Object.keys(jsonResults).forEach((filename) => { 324 | const skippedCount = jsonResults[filename].counts.pending 325 | n += skippedCount 326 | }) 327 | console.log(n) 328 | } else { 329 | if (args['--json']) { 330 | debug('names in json format') 331 | console.log(JSON.stringify(jsonResults, null, 2)) 332 | } else if (args['--markdown']) { 333 | debug('names in Markdown format') 334 | const str = stringMarkdownTests(jsonResults) 335 | console.log(str) 336 | } else { 337 | debug('names to standard out') 338 | const str = stringAllInfo(jsonResults) 339 | console.log(str) 340 | } 341 | console.log('') 342 | } 343 | } 344 | 345 | if (args['--write-html-filename']) { 346 | const outputFile = args['--write-html-filename'] 347 | debug('writing HTML file %s', outputFile) 348 | const html = toHtml(jsonResults, tagTestCounts) 349 | // create the directory if it does not exist 350 | const dir = path.dirname(outputFile) 351 | if (!fs.existsSync(dir)) { 352 | fs.mkdirSync(dir, { recursive: true }) 353 | } 354 | fs.writeFileSync(outputFile, html) 355 | console.log('wrote HTML file %s', outputFile) 356 | } 357 | 358 | if (args['--tags']) { 359 | const tagEntries = Object.entries(tagTestCounts) 360 | const sortedTagEntries = tagEntries.sort((a, b) => { 361 | // every entry is [tag, count], so compare the tags 362 | return a[0].localeCompare(b[0]) 363 | }) 364 | if (args['--json']) { 365 | // assemble a json object with the tag counts 366 | const tagResults = Object.fromEntries(sortedTagEntries) 367 | console.log(JSON.stringify(tagResults, null, 2)) 368 | } else { 369 | const table = consoleTable.getTable(['Tag', 'Tests'], sortedTagEntries) 370 | console.log(table) 371 | } 372 | } 373 | 374 | if (!args['--names'] && !args['--tags'] && !args['--write-html-filename']) { 375 | const specs = Object.keys(jsonResults) 376 | if (args['--count']) { 377 | debug('printing the number of specs %d', specs.length) 378 | console.log(specs.length) 379 | } else { 380 | debug('printing the spec names list only') 381 | const specNames = specs.join(',') 382 | console.log(specNames) 383 | 384 | if (args['--set-gha-outputs']) { 385 | debug('setting GitHub Actions outputs taggedSpecsN and taggedSpecs') 386 | debug('taggedSpecsN %d', specs.length) 387 | debug('plus taggedSpecs') 388 | core.setOutput('taggedSpecsN', specs.length) 389 | core.setOutput('taggedSpecs', specNames) 390 | } 391 | } 392 | } 393 | } else if (args['--grep']) { 394 | const grep = args['--grep'] 395 | // grep is a comma-separated string with parts of titles to find 396 | debug('finding tests with title containing "%s"', grep) 397 | const { jsonResults } = getTests(specs) 398 | const filtered = filterByGrep(jsonResults, grep) 399 | debug('printing the spec names list only') 400 | const specNames = filtered.join(',') 401 | console.log(specNames) 402 | 403 | if (args['--set-gha-outputs']) { 404 | debug( 405 | 'setting GitHub Actions outputs grepSpecsN to %d and grepSpecs', 406 | filtered.length, 407 | ) 408 | core.setOutput('grepSpecsN', filtered.length) 409 | core.setOutput('grepSpecs', specNames) 410 | } 411 | } else { 412 | if (args['--count']) { 413 | debug('printing the number of specs %d', specs.length) 414 | console.log(specs.length) 415 | } else { 416 | debug('printing just %d spec names', specs.length) 417 | console.log(specs.join(',')) 418 | } 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | const { getSpecs, getTests } = require('.') 3 | 4 | module.exports = defineConfig({ 5 | fixturesFolder: false, 6 | defaultBrowser: 'electron', 7 | defaultCommandTimeout: 500, 8 | e2e: { 9 | setupNodeEvents(on, config) { 10 | console.log('e2e setupNodeEvents') 11 | const specs = getSpecs(config) 12 | console.log('found specs') 13 | console.log(specs.join(',')) 14 | 15 | const tests = getTests(specs) 16 | console.log('found tests') 17 | console.log(tests) 18 | }, 19 | supportFile: false, 20 | specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', 21 | excludeSpecPattern: ['utils.js'], 22 | }, 23 | component: { 24 | specPattern: 'test-components/**/*.cy.{js,ts}', 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /cypress/e2e/featureA/user.cy.ts: -------------------------------------------------------------------------------- 1 | import './utils' 2 | 3 | // empty test 4 | // @ts-ignore 5 | it('works', { tags: '@user' }, () => {}) 6 | 7 | // pending test needs to use it.skip 8 | // since just not having a callback function is not enough 9 | // https://github.com/cypress-io/cypress/issues/19701 10 | // @ts-ignore 11 | it.skip('needs to be written', { tags: '@alpha' }) 12 | -------------------------------------------------------------------------------- /cypress/e2e/featureA/utils.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // just a file with utils, no tests 4 | // this file was changed in the branch 5 | -------------------------------------------------------------------------------- /cypress/e2e/spec-b.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // a typical test 4 | it('works in spec B', () => {}) 5 | -------------------------------------------------------------------------------- /cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import './utils' 4 | 5 | describe('parent suite', { tags: '@main' }, () => { 6 | it('works well enough', () => {}) 7 | 8 | context('inner suite', () => { 9 | // beautiful test. 10 | it('shows something!', { tags: '@user' }, () => {}) 11 | }) 12 | }) 13 | 14 | describe('empty parent suite', () => { 15 | context('inner suite', () => { 16 | it('shows something!', () => {}) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /cypress/e2e/utils.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // just a file with utils, no tests 4 | -------------------------------------------------------------------------------- /cypress/html-e2e/test-html.cy.js: -------------------------------------------------------------------------------- 1 | import 'cypress-map' 2 | import 'cypress-plugin-steps' 3 | import { toHtml } from '../../src/output-html' 4 | 5 | describe('HTML output', () => { 6 | const json = { 7 | 'cypress/e2e/spec-b.cy.js': { 8 | counts: { 9 | tests: 1, 10 | pending: 0, 11 | }, 12 | tests: [ 13 | { 14 | name: 'works in spec B', 15 | type: 'test', 16 | }, 17 | ], 18 | }, 19 | 'cypress/e2e/spec.cy.js': { 20 | counts: { 21 | tests: 3, 22 | pending: 0, 23 | }, 24 | tests: [ 25 | { 26 | name: 'parent suite', 27 | type: 'suite', 28 | tags: ['@main'], 29 | suites: [ 30 | { 31 | name: 'inner suite A', 32 | type: 'suite', 33 | tests: [ 34 | { 35 | name: 'shows something!', 36 | type: 'test', 37 | tags: ['@user'], 38 | }, 39 | ], 40 | }, 41 | ], 42 | tests: [ 43 | { 44 | name: 'works well enough', 45 | type: 'test', 46 | }, 47 | ], 48 | }, 49 | { 50 | name: 'Another suite', 51 | type: 'suite', 52 | suites: [ 53 | { 54 | name: 'inner suite B', 55 | type: 'suite', 56 | tests: [ 57 | { 58 | name: 'deep test', 59 | type: 'test', 60 | }, 61 | ], 62 | }, 63 | ], 64 | }, 65 | ], 66 | }, 67 | 'cypress/e2e/featureA/user.cy.ts': { 68 | counts: { 69 | tests: 2, 70 | pending: 1, 71 | }, 72 | tests: [ 73 | { 74 | name: 'works', 75 | type: 'test', 76 | tags: ['@user'], 77 | }, 78 | { 79 | name: 'needs to be written', 80 | type: 'test', 81 | tags: ['@alpha'], 82 | pending: true, 83 | }, 84 | ], 85 | }, 86 | } 87 | const tagTestCounts = { '@main': 2, '@user': 2, '@alpha': 1 } 88 | 89 | beforeEach(() => { 90 | const html = toHtml(json, tagTestCounts) 91 | cy.document({ log: false }).invoke('write', html) 92 | }) 93 | 94 | it('should output HTML', () => { 95 | cy.step('Title and header') 96 | cy.title().should('eq', 'Cypress Tests') 97 | cy.get('header').within(() => { 98 | cy.contains('h1', 'Cypress Tests') 99 | cy.contains('p', '3 specs, 6 tests') 100 | }) 101 | cy.get('main').within(() => { 102 | cy.get('ul.specs').find('li.spec').should('have.length', 3) 103 | cy.get('.filename').should('read', [ 104 | 'cypress/e2e/spec-b.cy.js', 105 | 'cypress/e2e/spec.cy.js', 106 | 'cypress/e2e/featureA/user.cy.ts', 107 | ]) 108 | }) 109 | 110 | cy.step('List of specs') 111 | cy.contains('.spec', 'cypress/e2e/featureA/user.cy.ts').within(() => { 112 | cy.get('.tests .test').should('have.length', 2) 113 | cy.get('.tests .test') 114 | .first() 115 | .within(() => { 116 | cy.get('.name').should('have.text', 'works') 117 | cy.get('.tag').should('read', ['@user']) 118 | }) 119 | cy.get('.tests .test') 120 | .last() 121 | .should('have.class', 'pending') 122 | .within(() => { 123 | cy.get('.name').should('have.text', 'needs to be written') 124 | cy.get('.tag').should('read', ['@alpha']) 125 | }) 126 | }) 127 | 128 | cy.step('Nested suite checks') 129 | cy.contains('.suite', 'parent suite').within(() => { 130 | // a suite has a test inside 131 | cy.contains('.test', 'works well enough') 132 | // a suite has a nested suite inside 133 | cy.contains('.suite', 'inner suite A').within(() => { 134 | cy.contains('.test', 'shows something!') 135 | }) 136 | }) 137 | 138 | cy.step('Another suite') 139 | cy.contains('.suite', 'Another suite').within(() => { 140 | cy.contains('.test', 'deep test') 141 | }) 142 | 143 | cy.step('Has filter tags') 144 | cy.get('.filter-tags').within(() => { 145 | cy.get('.filter-tag-name').should('have.length', 3) 146 | }) 147 | }) 148 | 149 | it('includes the test object', () => { 150 | cy.window() 151 | .should('have.property', 'findCypressSpecs') 152 | .should('have.keys', [ 153 | 'tests', 154 | 'tags', 155 | 'selectedTags', 156 | 'allTags', 157 | 'render', 158 | ]) 159 | }) 160 | }) 161 | -------------------------------------------------------------------------------- /cypress/html-e2e/test-tags.cy.js: -------------------------------------------------------------------------------- 1 | import 'cypress-map' 2 | import 'cypress-plugin-steps' 3 | import { toHtml } from '../../src/output-html' 4 | 5 | describe('HTML output', () => { 6 | const json = { 7 | 'cypress/e2e/spec-b.cy.js': { 8 | counts: { 9 | tests: 1, 10 | pending: 0, 11 | }, 12 | tests: [ 13 | { 14 | name: 'works in spec B', 15 | type: 'test', 16 | }, 17 | ], 18 | }, 19 | 'cypress/e2e/spec.cy.js': { 20 | counts: { 21 | tests: 3, 22 | pending: 0, 23 | }, 24 | tests: [ 25 | { 26 | name: 'parent suite', 27 | type: 'suite', 28 | tags: ['@main'], 29 | suites: [ 30 | { 31 | name: 'inner suite A', 32 | type: 'suite', 33 | tests: [ 34 | { 35 | name: 'shows something!', 36 | type: 'test', 37 | tags: ['@user'], 38 | }, 39 | ], 40 | }, 41 | ], 42 | tests: [ 43 | { 44 | name: 'works well enough', 45 | type: 'test', 46 | }, 47 | ], 48 | }, 49 | { 50 | name: 'Another suite', 51 | type: 'suite', 52 | suites: [ 53 | { 54 | name: 'inner suite B', 55 | type: 'suite', 56 | tests: [ 57 | { 58 | name: 'deep test', 59 | type: 'test', 60 | }, 61 | ], 62 | }, 63 | ], 64 | }, 65 | ], 66 | }, 67 | 'cypress/e2e/featureA/user.cy.ts': { 68 | counts: { 69 | tests: 2, 70 | pending: 1, 71 | }, 72 | tests: [ 73 | { 74 | name: 'works', 75 | type: 'test', 76 | tags: ['@user'], 77 | }, 78 | { 79 | name: 'needs to be written', 80 | type: 'test', 81 | tags: ['@alpha'], 82 | pending: true, 83 | }, 84 | ], 85 | }, 86 | } 87 | const tagTestCounts = { '@main': 2, '@user': 2, '@alpha': 1 } 88 | 89 | beforeEach(() => { 90 | const html = toHtml(json, tagTestCounts) 91 | cy.document({ log: false }).invoke('write', html) 92 | }) 93 | 94 | it('shows filter tags in alphabetical order', () => { 95 | cy.get('.filter-tags').within(() => { 96 | cy.get('.filter-tag-name').should('read', ['@alpha', '@main', '@user']) 97 | }) 98 | }) 99 | 100 | it('filters tests by a tag', () => { 101 | cy.get('#specs-count').should('have.text', '3') 102 | cy.get('#tests-count').should('have.text', '6') 103 | 104 | cy.get('input[value="@user"]').check() 105 | 106 | cy.step('Check filtered tests') 107 | cy.get('#specs-count').should('have.text', '2') 108 | cy.get('#tests-count').should('have.text', '2') 109 | 110 | cy.step('Shows the specs') 111 | 112 | cy.get('ul.specs') 113 | .find('li.spec') 114 | .should('have.length', 2) 115 | .find('.filename') 116 | .should('read', [ 117 | 'cypress/e2e/spec.cy.js', 118 | 'cypress/e2e/featureA/user.cy.ts', 119 | ]) 120 | 121 | cy.step('Shows the tests names') 122 | cy.contains('li.spec', 'spec.cy.js').within(() => { 123 | cy.contains('li.test', 'shows something!') 124 | }) 125 | 126 | cy.step('Check the second spec') 127 | cy.contains('li.spec', 'featureA/user.cy.ts').within(() => { 128 | cy.contains('li.test', 'works') 129 | }) 130 | }) 131 | 132 | it('shows all tests if no tag is selected', () => { 133 | cy.get('#specs-count').should('have.text', '3') 134 | cy.get('#tests-count').should('have.text', '6') 135 | 136 | cy.step('Filter by @user') 137 | cy.get('input[value="@user"]').check() 138 | cy.get('#specs-count').should('have.text', '2') 139 | cy.get('#tests-count').should('have.text', '2') 140 | cy.get('li.test').should('have.length', 2) 141 | 142 | cy.step('Back to all tests') 143 | cy.get('input[value="@user"]').uncheck() 144 | cy.get('#specs-count').should('have.text', '3') 145 | cy.get('#tests-count').should('have.text', '6') 146 | cy.get('li.test').should('have.length', 6) 147 | }) 148 | 149 | it('uses OR to combine tags', () => { 150 | cy.step('Filter by @user') 151 | cy.get('input[value="@user"]').check() 152 | cy.get('#specs-count').should('have.text', '2') 153 | cy.get('#tests-count').should('have.text', '2') 154 | cy.get('li.test').should('have.length', 2) 155 | 156 | cy.step('Add filter @alpha') 157 | cy.get('input[value="@alpha"]').check() 158 | cy.get('input[value="@user"]').should('be.checked') 159 | cy.get('#specs-count').should('have.text', '2') 160 | cy.get('#tests-count').should('have.text', '3') 161 | cy.get('li.test').should('have.length', 3) 162 | }) 163 | 164 | it('applies the parent suite tags', () => { 165 | cy.get('#specs-count').should('have.text', '3') 166 | cy.get('#tests-count').should('have.text', '6') 167 | 168 | cy.step('Filter by @main') 169 | cy.get('input[value="@main"]').check() 170 | cy.get('#specs-count').should('have.text', '1') 171 | cy.get('#tests-count').should('have.text', '2') 172 | cy.get('li.test') 173 | .should('have.length', 2) 174 | .find('.name') 175 | .should('read', ['shows something!', 'works well enough']) 176 | }) 177 | 178 | it('shows the tag counts', () => { 179 | cy.get('.filter-tag-container').should('have.length', 3) 180 | cy.get('.filter-tag-container') 181 | .find('.filter-tag-name') 182 | .should('read', ['@alpha', '@main', '@user']) 183 | cy.get('.filter-tag-container') 184 | .find('.filter-tag-count') 185 | .should('read', ['(1)', '(2)', '(2)']) 186 | }) 187 | }) 188 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["e2e/featureA/user.cy.ts"], 3 | "compilerOptions": { 4 | "types": ["cypress", "cypress-map", "cypress-plugin-steps"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /images/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/find-cypress-specs/c76be1f986e9cdac2c7384ec8fe7745a9ad333a6/images/debug.png -------------------------------------------------------------------------------- /images/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/find-cypress-specs/c76be1f986e9cdac2c7384ec8fe7745a9ad333a6/images/report.png -------------------------------------------------------------------------------- /mocks/branch-1.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const shell = require('shelljs') 3 | 4 | const command = `git diff --name-only --diff-filter=AMR origin/branch-test-1` 5 | sinon.stub(shell, 'exec').withArgs(command).returns({ 6 | code: 0, 7 | stdout: 'cypress/e2e/featureA/user.cy.ts', 8 | }) 9 | -------------------------------------------------------------------------------- /mocks/branch-tagged-1-no-alpha.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const shell = require('shelljs') 3 | 4 | const command = `git diff --name-only --diff-filter=AMR origin/tagged-1` 5 | sinon 6 | .stub(shell, 'exec') 7 | .withArgs(command) 8 | .returns({ 9 | code: 0, 10 | // return just one file as changed against the branch "tagged-1" 11 | // this spec file does NOT have any tests tagged "@alpha" 12 | stdout: ` 13 | cypress/e2e/spec.cy.js 14 | `, 15 | }) 16 | -------------------------------------------------------------------------------- /mocks/branch-tagged-1.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const shell = require('shelljs') 3 | 4 | const command = `git diff --name-only --diff-filter=AMR origin/tagged-1` 5 | sinon 6 | .stub(shell, 'exec') 7 | .withArgs(command) 8 | .returns({ 9 | code: 0, 10 | // return both spec files as changed against the branch "tagged-1" 11 | stdout: ` 12 | cypress/e2e/featureA/user.cy.ts 13 | cypress/e2e/spec.cy.js 14 | `, 15 | }) 16 | -------------------------------------------------------------------------------- /mocks/changed-imported-file.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const shell = require('shelljs') 3 | const path = require('path') 4 | const specChange = require('spec-change') 5 | 6 | // first stub the finding of the parent commit 7 | const command1 = 'git merge-base origin/parent-3 HEAD' 8 | // then stub the finding of the files changed 9 | const command2 = 'git diff --name-only --diff-filter=AMR commit-sha..' 10 | 11 | // Note: the stubs should work with the current Cypress specs in this repo 12 | 13 | sinon 14 | .stub(shell, 'exec') 15 | .withArgs(command1) 16 | .returns({ 17 | code: 0, 18 | // the commit sha should be trimmed by the code 19 | stdout: ' commit-sha ', 20 | }) 21 | .withArgs(command2) 22 | .returns({ 23 | code: 0, 24 | // Git return utils file only for changed files 25 | stdout: ` 26 | cypress/e2e/utils.js 27 | `, 28 | }) 29 | 30 | // stub the utility used to trace imports 31 | sinon.stub(specChange, 'getDependsInFolder').returns({ 32 | 'e2e/utils.js': ['e2e/spec.cy.js'], 33 | }) 34 | -------------------------------------------------------------------------------- /mocks/my-app/e2e/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | // baseUrl, etc 6 | supportFile: false, 7 | fixturesFolder: false, 8 | // Cypress likes path with respect to the root folder, sigh 9 | specPattern: 'mocks/my-app/e2e/e2e-tests/*.cy.js', 10 | setupNodeEvents(on, config) { 11 | console.log('project root is %s', config.projectRoot) 12 | console.log('working directory is %s', process.cwd()) 13 | 14 | on('after:spec', (spec) => { 15 | console.log('spec %s finished', spec.relative) 16 | }) 17 | }, 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /mocks/my-app/e2e/e2e-tests/spec-a.cy.js: -------------------------------------------------------------------------------- 1 | it('works A', () => {}) 2 | -------------------------------------------------------------------------------- /mocks/parent-1.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const shell = require('shelljs') 3 | 4 | // first stub the finding of the parent commit 5 | const command1 = 'git merge-base origin/parent-1 HEAD' 6 | // then stub the finding of the files changed 7 | const command2 = 'git diff --name-only --diff-filter=AMR commit-sha..' 8 | 9 | sinon 10 | .stub(shell, 'exec') 11 | .withArgs(command1) 12 | .returns({ 13 | code: 0, 14 | // the commit sha should be trimmed by the code 15 | stdout: ' commit-sha ', 16 | }) 17 | .withArgs(command2) 18 | .returns({ 19 | code: 0, 20 | stdout: 'cypress/e2e/spec.cy.js', 21 | }) 22 | -------------------------------------------------------------------------------- /mocks/parent-2.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const shell = require('shelljs') 3 | 4 | // first stub the finding of the parent commit 5 | const command1 = 'git merge-base origin/parent-2 HEAD' 6 | // then stub the finding of the files changed 7 | const command2 = 'git diff --name-only --diff-filter=AMR commit-sha..' 8 | 9 | sinon 10 | .stub(shell, 'exec') 11 | .withArgs(command1) 12 | .returns({ 13 | code: 0, 14 | // the commit sha should be trimmed by the code 15 | stdout: ' commit-sha ', 16 | }) 17 | .withArgs(command2) 18 | .returns({ 19 | code: 0, 20 | // return both spec files 21 | stdout: ` 22 | cypress/e2e/spec.cy.js 23 | cypress/e2e/featureA/user.cy.ts 24 | `, 25 | }) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "find-cypress-specs", 3 | "version": "0.0.0-development", 4 | "description": "Find Cypress spec files using the config settings", 5 | "main": "src", 6 | "files": [ 7 | "bin", 8 | "src" 9 | ], 10 | "bin": { 11 | "find-cypress-specs": "bin/find.js" 12 | }, 13 | "engines": { 14 | "node": ">=18" 15 | }, 16 | "ava": { 17 | "files": [ 18 | "test/**/*.js" 19 | ] 20 | }, 21 | "scripts": { 22 | "test": "ava", 23 | "prod-deps": "npm audit --report --omit dev", 24 | "cy:run": "DEBUG=cypress:cli,cypress:server:specs cypress run", 25 | "demo": "DEBUG=find-cypress-specs node ./bin/find", 26 | "demo-names": "DEBUG=find-cypress-specs node ./bin/find --names", 27 | "demo-sorted-by-modified": "DEBUG=find-cypress-specs node ./bin/find --sort-by-modified --set-gha-outputs", 28 | "demo-names-markdown": "node ./bin/find --names --markdown", 29 | "demo-skipped-tests": "DEBUG=find-cypress-specs node ./bin/find --names --skipped", 30 | "demo-count-skipped-tests": "DEBUG=find-cypress-specs node ./bin/find --names --skipped --count", 31 | "demo-custom-cypress-config": "DEBUG=find-cypress-specs CYPRESS_CONFIG_FILE=test-ts/cypress.config.custom.ts node ./bin/find", 32 | "demo-tags": "node ./bin/find --tags", 33 | "demo-tags-json": "node ./bin/find --tags --json", 34 | "demo-names-and-tags": "node ./bin/find --names --tags", 35 | "demo-names-and-tags-json": "node ./bin/find --names --tags --json", 36 | "demo-names-json": "node ./bin/find --names --json", 37 | "demo-names-tagged": "node ./bin/find --names --tagged @user", 38 | "demo-tagged-empty-string": "node ./bin/find --tagged ''", 39 | "demo-subfolder": "DEBUG=find-cypress-specs CYPRESS_CONFIG_FILE=mocks/my-app/e2e/cypress.config.js node ./bin/find --names", 40 | "print-changed-specs": "node ./bin/find --branch main", 41 | "count-changed-specs": "node ./bin/find --branch main --count", 42 | "demo-test-counts": "node ./bin/find --test-counts", 43 | "demo-test-counts-badge": "node ./bin/find --test-counts --update-badge", 44 | "semantic-release": "semantic-release", 45 | "deps": "spec-change --folder . --mask 'cypress/**/*.{js,ts}'", 46 | "deps-changed": "DEBUG=find-cypress-specs node ./bin/find --branch main --parent --trace-imports cypress --time-trace --cache-trace", 47 | "demo-component": "DEBUG=find-cypress-specs node ./bin/find --component --names", 48 | "demo-exclusive": "npm run test-names --prefix test-exclusive --silent", 49 | "version-badge": "update-badge cypress", 50 | "count-all-tags": "npm run count-all-tags --prefix test-required-tags", 51 | "html-e2e": "cypress open --config specPattern=cypress/html-e2e/**/*.cy.js", 52 | "test-report": "node ./bin/find --write-html-filename report/index.html", 53 | "audit": "npm audit --report --omit dev" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/bahmutov/find-cypress-specs.git" 58 | }, 59 | "keywords": [ 60 | "cypress-plugin" 61 | ], 62 | "author": "Gleb Bahmutov ", 63 | "license": "MIT", 64 | "bugs": { 65 | "url": "https://github.com/bahmutov/find-cypress-specs/issues" 66 | }, 67 | "homepage": "https://github.com/bahmutov/find-cypress-specs#readme", 68 | "devDependencies": { 69 | "ava": "^6.2.0", 70 | "cypress": "14.2.0", 71 | "cypress-map": "^1.46.0", 72 | "cypress-plugin-steps": "^1.1.1", 73 | "dependency-version-badge": "^1.11.0", 74 | "execa-wrap": "^1.4.0", 75 | "prettier": "^2.5.1", 76 | "really-need": "^1.9.2", 77 | "semantic-release": "24.2.3", 78 | "sinon": "^13.0.1", 79 | "typescript": "^4.6.3" 80 | }, 81 | "dependencies": { 82 | "@actions/core": "^1.10.0", 83 | "arg": "^5.0.1", 84 | "console.table": "^0.10.0", 85 | "debug": "^4.3.3", 86 | "find-test-names": "1.29.11", 87 | "globby": "^11.1.0", 88 | "minimatch": "^3.0.4", 89 | "pluralize": "^8.0.0", 90 | "require-and-forget": "^1.0.1", 91 | "shelljs": "^0.8.5", 92 | "spec-change": "^1.11.17", 93 | "tsx": "^4.19.3" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "prHourlyLimit": 4, 5 | "updateNotScheduled": false, 6 | "timezone": "America/New_York", 7 | "schedule": ["after 10pm and before 5am on every weekday", "every weekend"], 8 | "masterIssue": true, 9 | "labels": ["type: dependencies", "renovate"], 10 | "ignorePaths": ["**/node_modules/**", "**/test*/**", "**/.github/**"], 11 | "packageRules": [ 12 | { 13 | "packagePatterns": ["*"], 14 | "excludePackagePatterns": [ 15 | "cypress", 16 | "find-test-names", 17 | "semantic-release", 18 | "spec-change" 19 | ], 20 | "enabled": false 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/badge.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const debug = require('debug')('find-cypress-specs') 4 | const os = require('os') 5 | const fs = require('fs') 6 | 7 | function isNumber(n) { 8 | return typeof n === 'number' && !isNaN(n) 9 | } 10 | 11 | /** 12 | * Returns Markdown for a badge with the given number of tests. 13 | * @param {number} nE2E Number of end-to-end tests 14 | * @param {number|undefined} nComponent Number of component tests (optional) 15 | * @see https://shields.io/ 16 | */ 17 | function getBadgeMarkdown(nE2E, nComponent) { 18 | debug('forming new test count badge with %o', { nE2E, nComponent }) 19 | if (isNumber(nE2E)) { 20 | if (isNumber(nComponent)) { 21 | // we have both e2e and component tests 22 | return `![Cypress tests](https://img.shields.io/badge/cy%20tests-E2E%20${nE2E}%20%7C%20component%20${nComponent}-blue)` 23 | } else { 24 | // only e2e tests 25 | return `![Cypress tests](https://img.shields.io/badge/cy%20tests-E2E%20${nE2E}-blue)` 26 | } 27 | } else { 28 | if (isNumber(nComponent)) { 29 | // we have only the number of component tests 30 | return `![Cypress tests](https://img.shields.io/badge/cy%20tests-component%20${nComponent}-blue)` 31 | } else { 32 | // we have nothing 33 | return `![Cypress tests](https://img.shields.io/badge/cy%20tests-unknown-inactive)` 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * Replaces the whole Markdown image badge with new badge with test counters. 40 | */ 41 | function replaceBadge({ 42 | markdown, // current markdown text 43 | nE2E, // number of E2E tests 44 | nComponent, // number of component tests 45 | }) { 46 | debug('replacing badge with new numbers %o', { nE2E, nComponent }) 47 | const badgeRe = new RegExp( 48 | `\\!\\[Cypress tests\\]` + 49 | '\\(https://img\\.shields\\.io/badge/cy%20tests\\-' + 50 | '.+-(?:blue|inactive)\\)', 51 | ) 52 | 53 | const badge = getBadgeMarkdown(nE2E, nComponent) 54 | debug('new badge contents "%s"', badge) 55 | let found 56 | 57 | let updatedReadmeText = markdown.replace(badgeRe, (match) => { 58 | found = true 59 | return badge 60 | }) 61 | 62 | if (!found) { 63 | console.log('⚠️ Could not find test count badge') 64 | console.log('Insert new badge on the first line') 65 | debug('inserting new badge: %s', badge) 66 | 67 | const lines = markdown.split(os.EOL) 68 | if (lines.length < 1) { 69 | console.error('README file has no lines, cannot insert test count badge') 70 | return markdown 71 | } 72 | lines[0] += ' ' + badge 73 | updatedReadmeText = lines.join(os.EOL) 74 | } else { 75 | debug('replaced badge') 76 | } 77 | return updatedReadmeText 78 | } 79 | 80 | function updateBadge({ nE2E, nComponent }) { 81 | debug('reading in README file') 82 | // TODO: make the filename a parameter 83 | const filename = 'README.md' 84 | const readmeText = fs.readFileSync(filename, 'utf8') 85 | const maybeChangedText = replaceBadge({ 86 | markdown: readmeText, 87 | nE2E, 88 | nComponent, 89 | }) 90 | if (maybeChangedText !== readmeText) { 91 | console.log('saving updated readme with new test counts') 92 | fs.writeFileSync(filename, maybeChangedText, 'utf8') 93 | } else { 94 | debug('no updates to test counts') 95 | } 96 | } 97 | 98 | module.exports = { getBadgeMarkdown, replaceBadge, updateBadge } 99 | -------------------------------------------------------------------------------- /src/count.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For each file and each 3 | */ 4 | function addCounts(json) { 5 | Object.keys(json).forEach((filename) => { 6 | const fileInfo = json[filename] 7 | if (!fileInfo.counts) { 8 | fileInfo.counts = { 9 | tests: 0, 10 | pending: 0, 11 | } 12 | } 13 | 14 | fileInfo.counts.tests = 15 | countTests(fileInfo.tests) + countTests(fileInfo.suites) 16 | fileInfo.counts.pending = 17 | countPendingTests(fileInfo.tests) + countPendingTests(fileInfo.suites) 18 | }) 19 | } 20 | 21 | function countTests(testsOrSuites) { 22 | if (!testsOrSuites) { 23 | return 0 24 | } 25 | 26 | return testsOrSuites.reduce((count, test) => { 27 | if (test.type === 'test') { 28 | return count + 1 29 | } else if (test.type === 'suite') { 30 | return count + countTests(test.tests) + countTests(test.suites) 31 | } 32 | }, 0) 33 | } 34 | 35 | function countPendingTests(testsOrSuites) { 36 | if (!testsOrSuites) { 37 | return 0 38 | } 39 | 40 | return testsOrSuites.reduce((count, test) => { 41 | if (test.type === 'test') { 42 | return test.pending ? count + 1 : count 43 | } else if (test.type === 'suite') { 44 | if (test.pending) { 45 | // all tests inside should count as pending 46 | return count + countTests(test.tests) + countTests(test.suites) 47 | } 48 | 49 | return ( 50 | count + countPendingTests(test.tests) + countPendingTests(test.suites) 51 | ) 52 | } 53 | }, 0) 54 | } 55 | 56 | /** 57 | * Goes through the object with all specs and adds up 58 | * all test counts to find the total number of tests 59 | * and the total number of pending tests 60 | */ 61 | function sumTestCounts(allInfo) { 62 | const counts = { 63 | tests: 0, 64 | pending: 0, 65 | } 66 | Object.keys(allInfo).forEach((fileName) => { 67 | const fileInfo = allInfo[fileName] 68 | counts.tests += fileInfo.counts.tests 69 | counts.pending += fileInfo.counts.pending 70 | }) 71 | 72 | return counts 73 | } 74 | 75 | module.exports = { addCounts, countTests, sumTestCounts, countPendingTests } 76 | -------------------------------------------------------------------------------- /src/files.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { relative } = require('path') 3 | 4 | /** 5 | * Converts a list of absolute filenames to the list of relative filenames 6 | * @param {string[]} filenames List of absolute filenames 7 | * @returns {string[]} List of relative filenames to the current working directory 8 | */ 9 | function toRelative(filenames) { 10 | const cwd = process.cwd() 11 | return filenames.map((filename) => relative(cwd, filename)) 12 | } 13 | 14 | module.exports = { toRelative } 15 | -------------------------------------------------------------------------------- /src/grep.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('find-cypress-specs') 2 | 3 | /** 4 | * Recursively checks the tests structure to see 5 | * if any of the leaf test objects have a name that includes 6 | * any of the given strings 7 | */ 8 | function checkIncludesTestTitle(tests, greps) { 9 | if (!tests) { 10 | return false 11 | } 12 | debug('greps', greps) 13 | return tests.some((test) => { 14 | if (test.type === 'test' && test.name) { 15 | debug('checking test "%s"', test.name) 16 | return greps.some((grep) => test.name.includes(grep)) 17 | } else if (test.type === 'suite') { 18 | debug('checking suite "%s"', test.name) 19 | return ( 20 | checkIncludesTestTitle(test.tests, greps) || 21 | checkIncludesTestTitle(test.suites, greps) 22 | ) 23 | } else if (Array.isArray(test.tests)) { 24 | return checkIncludesTestTitle(test.tests, greps) 25 | } else if (Array.isArray(test.suites)) { 26 | return checkIncludesTestTitle(test.suites, greps) 27 | } 28 | }) 29 | } 30 | 31 | /** 32 | * returns a list of specs where any test title contains the given string. 33 | * Leaves the original structure intact. 34 | * @param {object} jsonResults - parsed JSON spec file structure 35 | * @param {string} grep - comma-separated list of strings to filter by 36 | * @returns {string[]} - list of spec filenames 37 | */ 38 | function filterByGrep(jsonResults, grep) { 39 | const result = [] 40 | const greps = grep 41 | .split(',') 42 | .map((s) => s.trim()) 43 | .filter(Boolean) 44 | 45 | Object.keys(jsonResults).forEach((specFilename) => { 46 | debug('checking spec "%s"', specFilename) 47 | const spec = jsonResults[specFilename] 48 | if (checkIncludesTestTitle(spec.tests, greps)) { 49 | result.push(specFilename) 50 | } 51 | }) 52 | 53 | return result 54 | } 55 | 56 | module.exports = { filterByGrep } 57 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { addCounts } = require('../src/count') 2 | const { getTestNames, countTags } = require('find-test-names') 3 | const { pickTaggedTestsFrom, leavePendingTestsOnly } = require('../src/tagged') 4 | 5 | const debug = require('debug')('find-cypress-specs') 6 | const debugGit = require('debug')('find-cypress-specs:git') 7 | const fs = require('fs') 8 | const path = require('path') 9 | const globby = require('globby') 10 | const minimatch = require('minimatch') 11 | const shell = require('shelljs') 12 | const pluralize = require('pluralize') 13 | const requireEveryTime = require('require-and-forget') 14 | 15 | // Require CJS loader to resolve: 16 | // https://github.com/bahmutov/find-cypress-specs/issues/228 17 | // https://github.com/bahmutov/find-cypress-specs/issues/222 18 | // https://github.com/privatenumber/tsx 19 | require('tsx/cjs') 20 | 21 | const MINIMATCH_OPTIONS = { dot: true, matchBase: true } 22 | 23 | /** 24 | * Reads the Cypress config JSON file (Cypress v9) and returns the relevant properties 25 | */ 26 | function getConfigJson(filename = 'cypress.json') { 27 | const s = fs.readFileSync(filename, 'utf8') 28 | const config = JSON.parse(s) 29 | const options = { 30 | integrationFolder: config.integrationFolder, 31 | testFiles: config.testFiles, 32 | ignoreTestFiles: config.ignoreTestFiles, 33 | } 34 | debug('got config options %o', options) 35 | return options 36 | } 37 | 38 | function getConfigJs(filename) { 39 | const jsFile = path.join(process.cwd(), filename) 40 | debug('loading Cypress config from %s', jsFile) 41 | const definedConfig = requireEveryTime(jsFile) 42 | return definedConfig 43 | } 44 | 45 | function getConfigTs(filename) { 46 | const configFilename = path.join(process.cwd(), filename) 47 | debug('loading Cypress config from %s', configFilename) 48 | const definedConfig = requireEveryTime(configFilename) 49 | debug('loaded config %o', definedConfig) 50 | if (definedConfig && definedConfig.default) { 51 | // due to TS / ES6 module transpile we got the default export 52 | debug('returning default export as config from %s', filename) 53 | return definedConfig.default 54 | } 55 | 56 | return definedConfig 57 | } 58 | 59 | function getConfig() { 60 | if (typeof process.env.CYPRESS_CONFIG_FILE !== 'undefined') { 61 | const configFile = process.env.CYPRESS_CONFIG_FILE 62 | if ( 63 | configFile.endsWith('.js') || 64 | configFile.endsWith('.cjs') || 65 | configFile.endsWith('.mjs') 66 | ) { 67 | debug(`found file ${configFile}`) 68 | return getConfigJs(`./${configFile}`) 69 | } else if (configFile.endsWith('.ts')) { 70 | debug(`found file ${configFile}`) 71 | return getConfigTs(`./${configFile}`) 72 | } else if (configFile.endsWith('.json')) { 73 | debug(`found file ${configFile}`) 74 | return getConfigJson(`./${configFile}`) 75 | } 76 | throw new Error( 77 | 'Config file should be .ts, .js, cjs, mjs, or .json file even when using CYPRESS_CONFIG_FILE env var', 78 | ) 79 | } 80 | 81 | if (fs.existsSync('./cypress.config.js')) { 82 | debug('found file cypress.config.js') 83 | return getConfigJs('./cypress.config.js') 84 | } 85 | if (fs.existsSync('./cypress.config.cjs')) { 86 | debug('found file cypress.config.cjs') 87 | return getConfigJs('./cypress.config.cjs') 88 | } 89 | if (fs.existsSync('./cypress.config.mjs')) { 90 | debug('found file cypress.config.mjs') 91 | return getConfigJs('./cypress.config.mjs') 92 | } 93 | 94 | if (fs.existsSync('./cypress.config.ts')) { 95 | debug('found file cypress.config.ts') 96 | return getConfigTs('./cypress.config.ts') 97 | } 98 | 99 | if (fs.existsSync('./cypress.json')) { 100 | debug('found file cypress.json') 101 | return getConfigJson('./cypress.json') 102 | } 103 | 104 | throw new Error('Config file should be .ts, .js, .cjs, mjs, or .json file') 105 | } 106 | 107 | function findCypressSpecsV9(opts = {}, returnAbsolute = false) { 108 | const defaults = { 109 | integrationFolder: 'cypress/integration', 110 | testFiles: '**/*.{js,ts}', 111 | ignoreTestFiles: [], 112 | } 113 | const options = { 114 | integrationFolder: opts.integrationFolder || defaults.integrationFolder, 115 | testFiles: opts.testFiles || defaults.testFiles, 116 | ignoreTestFiles: opts.ignoreTestFiles || defaults.ignoreTestFiles, 117 | } 118 | debug('options %o', options) 119 | 120 | const files = globby.sync(options.testFiles, { 121 | sort: true, 122 | cwd: options.integrationFolder, 123 | ignore: options.ignoreTestFiles, 124 | absolute: returnAbsolute, 125 | }) 126 | debug('found %d file(s) %o', files.length, files) 127 | 128 | // go through the files again and eliminate files that match 129 | // the ignore patterns 130 | const ignorePatterns = [].concat(options.ignoreTestFiles) 131 | debug('ignore patterns %o', ignorePatterns) 132 | 133 | // a function which returns true if the file does NOT match 134 | // all of our ignored patterns 135 | const doesNotMatchAllIgnoredPatterns = (file) => { 136 | // using {dot: true} here so that folders with a '.' in them are matched 137 | // as regular characters without needing an '.' in the 138 | // using {matchBase: true} here so that patterns without a globstar ** 139 | // match against the basename of the file 140 | return ignorePatterns.every((pattern) => { 141 | return !minimatch(file, pattern, MINIMATCH_OPTIONS) 142 | }) 143 | } 144 | 145 | const filtered = files.filter(doesNotMatchAllIgnoredPatterns) 146 | debug('filtered %d specs', filtered.length) 147 | debug(filtered.join('\n')) 148 | 149 | // return spec files with the added integration folder prefix 150 | return filtered.map((file) => path.join(options.integrationFolder, file)) 151 | } 152 | 153 | function findCypressSpecsV10(opts = {}, type = 'e2e', returnAbsolute = false) { 154 | debug('findCypressSpecsV10') 155 | if (type !== 'e2e' && type !== 'component') { 156 | throw new Error(`Unknown spec type ${type}`) 157 | } 158 | // handle the interoperability loading of default export 159 | if (Object.keys(opts).length === 1 && Object.keys(opts)[0] === 'default') { 160 | opts = opts.default 161 | } 162 | 163 | if (!(type in opts)) { 164 | debug('options %o', opts) 165 | throw new Error(`Missing "${type}" object in the Cypress config object`) 166 | } 167 | const e2eDefaults = { 168 | specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', 169 | excludeSpecPattern: [], 170 | } 171 | const componentDefaults = { 172 | specPattern: '**/*.cy.{js,jsx,ts,tsx}', 173 | excludeSpecPattern: ['/snapshots/*', '/image_snapshots/*'], 174 | } 175 | // https://on.cypress.io/configuration 176 | const options = {} 177 | 178 | if (type === 'e2e') { 179 | options.specPattern = opts.e2e.specPattern || e2eDefaults.specPattern 180 | options.excludeSpecPattern = 181 | opts.e2e.excludeSpecPattern || e2eDefaults.excludeSpecPattern 182 | } else if (type === 'component') { 183 | options.specPattern = 184 | opts.component.specPattern || componentDefaults.specPattern 185 | options.excludeSpecPattern = 186 | opts.component.excludeSpecPattern || componentDefaults.excludeSpecPattern 187 | } 188 | 189 | debug('options v10 %o', options) 190 | 191 | const ignore = Array.isArray(options.excludeSpecPattern) 192 | ? options.excludeSpecPattern 193 | : options.excludeSpecPattern 194 | ? [options.excludeSpecPattern] 195 | : [] 196 | 197 | debug('ignore patterns %o', ignore) 198 | const userIgnoresNodeModules = ignore.some((pattern) => 199 | pattern.includes('node_modules'), 200 | ) 201 | if (userIgnoresNodeModules) { 202 | debug('user ignored node_modules') 203 | } else { 204 | debug('user did not ignore node_modules, adding it') 205 | ignore.push('**/node_modules/**') 206 | } 207 | 208 | const globbyOptions = { 209 | sort: true, 210 | ignore, 211 | absolute: returnAbsolute, 212 | } 213 | if (opts.projectRoot) { 214 | globbyOptions.cwd = opts.projectRoot 215 | } 216 | debug('globby options %s %o', options.specPattern, globbyOptions) 217 | 218 | /** @type string[] */ 219 | const files = globby.sync(options.specPattern, globbyOptions) 220 | debug('found %d file(s) %o', files.length, files) 221 | 222 | // go through the files again and eliminate files that match 223 | // the ignore patterns 224 | const ignorePatterns = [].concat(options.excludeSpecPattern) 225 | 226 | // when using component spec pattern, ignore all E2E specs 227 | if (type === 'component') { 228 | const e2eIgnorePattern = options.e2e?.specPattern || e2eDefaults.specPattern 229 | ignorePatterns.push(e2eIgnorePattern) 230 | ignorePatterns.push('node_modules/**') 231 | } 232 | 233 | debug('ignore patterns %o', ignorePatterns) 234 | 235 | // a function which returns true if the file does NOT match 236 | // all of our ignored patterns 237 | const doesNotMatchAllIgnoredPatterns = (file) => { 238 | // using {dot: true} here so that folders with a '.' in them are matched 239 | // as regular characters without needing an '.' in the 240 | // using {matchBase: true} here so that patterns without a globstar ** 241 | // match against the basename of the file 242 | const MINIMATCH_OPTIONS = { dot: true, matchBase: true } 243 | return ignorePatterns.every((pattern) => { 244 | return !minimatch(file, pattern, MINIMATCH_OPTIONS) 245 | }) 246 | } 247 | 248 | const filtered = files.filter(doesNotMatchAllIgnoredPatterns) 249 | debug('filtered %d specs', filtered.length) 250 | debug(filtered.join('\n')) 251 | 252 | return filtered 253 | } 254 | 255 | function getSpecs(options, type, returnAbsolute = false) { 256 | if (typeof options === 'undefined') { 257 | options = getConfig() 258 | if (typeof type === 'undefined') { 259 | type = 'e2e' 260 | } 261 | } else { 262 | // we might have resolved config object 263 | // passed from the "setupNode..." callback 264 | if ('testingType' in options) { 265 | type = options.testingType 266 | options = { 267 | version: options.version, 268 | projectRoot: options.projectRoot, 269 | [type]: { 270 | specPattern: options.specPattern, 271 | excludeSpecPattern: options.excludeSpecPattern, 272 | }, 273 | } 274 | } 275 | } 276 | 277 | return findCypressSpecs(options, type, returnAbsolute) 278 | } 279 | 280 | /** 281 | * Finds Cypress specs. 282 | * @param {boolean} returnAbsolute Return the list of absolute spec filenames 283 | * @returns {string[]} List of filenames 284 | */ 285 | function findCypressSpecs(options, type = 'e2e', returnAbsolute = false) { 286 | debug('finding specs of type %s', type) 287 | if (options.version) { 288 | debug('Cypress version %s', options.version) 289 | } 290 | 291 | let cyVersion = options.version 292 | if (typeof cyVersion !== 'string') { 293 | if ('integrationFolder' in options) { 294 | cyVersion = '9.0.0' 295 | } else { 296 | cyVersion = '10.0.0' 297 | } 298 | } 299 | const [major] = cyVersion.split('.').map(Number) 300 | debug('treating options as Cypress version %d', major) 301 | 302 | if (type === 'e2e') { 303 | if (major >= 10) { 304 | debug('config has "e2e" property, treating as Cypress v10+') 305 | const specs = findCypressSpecsV10(options, type, returnAbsolute) 306 | return specs 307 | } 308 | 309 | debug('reading Cypress config < v10') 310 | const specs = findCypressSpecsV9(options, returnAbsolute) 311 | return specs 312 | } else if (type === 'component') { 313 | debug('finding component specs') 314 | const specs = findCypressSpecsV10(options, type, returnAbsolute) 315 | return specs 316 | } else { 317 | console.error('Do not know how to find specs of type "%s"', type) 318 | console.error('returning an empty list') 319 | return [] 320 | } 321 | } 322 | 323 | function collectResults(structure, results) { 324 | structure.forEach((t) => { 325 | const info = { 326 | name: t.name, 327 | type: t.type, 328 | tags: t.tags, 329 | requiredTags: t.requiredTags, 330 | } 331 | if (t.pending) { 332 | info.pending = t.pending 333 | } 334 | if (t.exclusive) { 335 | info.exclusive = t.exclusive 336 | } 337 | debug('structure for %s', t.name) 338 | debug(info) 339 | 340 | results.push(info) 341 | if (t.type === 'suite') { 342 | if (t.suites && t.suites.length) { 343 | // skip empty nested suites 344 | info.suites = [] 345 | collectResults(t.suites, info.suites) 346 | } 347 | 348 | if (t.tests && t.tests.length) { 349 | // skip empty nested tests 350 | info.tests = [] 351 | collectResults(t.tests, info.tests) 352 | } 353 | } 354 | }) 355 | } 356 | 357 | /** 358 | * Finds files changed or added in the current branch when compared to the "origin/branch". 359 | * Returns a list of filenames. If there are no files, returns an empty list. 360 | * @param {string} branch The branch to compare against. 361 | * @param {boolean} useParent Determine the changes only against the parent commit. 362 | */ 363 | function findChangedFiles(branch, useParent) { 364 | if (!branch) { 365 | throw new Error('branch is required') 366 | } 367 | 368 | if (!shell.which('git')) { 369 | shell.echo('Sorry, this script requires git') 370 | return [] 371 | } 372 | 373 | // can we find updated and added files? 374 | debug( 375 | 'finding changed files against %s using parent?', 376 | branch, 377 | Boolean(useParent), 378 | ) 379 | 380 | if (useParent) { 381 | let result = shell.exec(`git merge-base origin/${branch} HEAD`, { 382 | silent: true, 383 | }) 384 | if (result.code !== 0) { 385 | debugGit('git failed to find merge base with the branch %s', branch) 386 | return [] 387 | } 388 | 389 | const commit = result.stdout.trim() 390 | debugGit('merge commit with branch "%s" is %s', branch, commit) 391 | result = shell.exec(`git diff --name-only --diff-filter=AMR ${commit}..`, { 392 | silent: true, 393 | }) 394 | if (result.code !== 0) { 395 | debugGit('git diff failed with code %d', result.code) 396 | return [] 397 | } 398 | 399 | const filenames = result.stdout 400 | .split('\n') 401 | .map((s) => s.trim()) 402 | .filter(Boolean) 403 | debugGit( 404 | 'found %d changed files against branch %s', 405 | filenames.length, 406 | branch, 407 | ) 408 | return filenames 409 | } else { 410 | const command = `git diff --name-only --diff-filter=AMR origin/${branch}` 411 | debugGit('command: %s', command) 412 | 413 | const result = shell.exec(command, { silent: true }) 414 | if (result.code !== 0) { 415 | debugGit('git diff failed with code %d', result.code) 416 | return [] 417 | } 418 | 419 | const filenames = result.stdout 420 | .split('\n') 421 | .map((s) => s.trim()) 422 | .filter(Boolean) 423 | debugGit( 424 | 'found %d changed %s', 425 | filenames.length, 426 | pluralize('file', filenames.length), 427 | ) 428 | return filenames 429 | } 430 | } 431 | 432 | /** 433 | * Collects all specs and for each finds all suits and tests with their tags. 434 | */ 435 | function getTests(specs, options = {}) { 436 | if (!specs) { 437 | specs = getSpecs() 438 | } 439 | 440 | const { tags, tagged, skipped } = options 441 | 442 | // counts the number of tests for each tag across all specs 443 | const tagTestCounts = {} 444 | const jsonResults = {} 445 | 446 | specs.forEach((filename) => { 447 | try { 448 | const source = fs.readFileSync(filename, 'utf8') 449 | const result = getTestNames(source, true) 450 | 451 | jsonResults[filename] = { 452 | counts: { 453 | tests: 0, 454 | pending: 0, 455 | }, 456 | tests: [], 457 | } 458 | // enable if need to debug the parsed test 459 | // console.dir(result.structure, { depth: null }) 460 | collectResults(result.structure, jsonResults[filename].tests) 461 | debug('collected results for file %s', filename) 462 | 463 | if (tags) { 464 | debug('counting tags', tags) 465 | const specTagCounts = countTags(result.structure) 466 | debug('spec tag counts') 467 | debug(specTagCounts) 468 | 469 | Object.keys(specTagCounts).forEach((tag) => { 470 | if (!(tag in tagTestCounts)) { 471 | tagTestCounts[tag] = specTagCounts[tag] 472 | } else { 473 | tagTestCounts[tag] += specTagCounts[tag] 474 | } 475 | }) 476 | } 477 | } catch (e) { 478 | console.error('find-cypress-specs: problem parsing file %s', filename) 479 | delete jsonResults[filename] 480 | } 481 | }) 482 | 483 | addCounts(jsonResults) 484 | debug('added counts') 485 | debug(jsonResults) 486 | 487 | if (tagged || tagged === '') { 488 | // filter all collected tests to those that have the given tag(s) 489 | const splitTags = tagged 490 | .split(',') 491 | .map((s) => s.trim()) 492 | .filter(Boolean) 493 | debug('filtering all tests by tag %o', splitTags) 494 | pickTaggedTestsFrom(jsonResults, splitTags) 495 | debug('after picking tagged tests') 496 | debug(jsonResults) 497 | // recompute the number of tests 498 | debug('adding counts to json results') 499 | addCounts(jsonResults) 500 | } else if (skipped) { 501 | debug('leaving only skipped (pending) tests') 502 | leavePendingTestsOnly(jsonResults) 503 | // recompute the number of tests 504 | addCounts(jsonResults) 505 | } 506 | 507 | return { jsonResults, tagTestCounts } 508 | } 509 | 510 | module.exports = { 511 | getSpecs, 512 | // individual utilities 513 | getConfig, 514 | findCypressSpecs, 515 | collectResults, 516 | findChangedFiles, 517 | getTests, 518 | } 519 | -------------------------------------------------------------------------------- /src/output-html.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { 4 | pickTaggedTestsFrom, 5 | pickTaggedTests, 6 | removeEmptyNodes, 7 | doTagsMatch, 8 | combineTags, 9 | preprocessAndTags, 10 | } from './tagged' 11 | const { addCounts, countTests, countPendingTests } = require('./count') 12 | 13 | function countTheseTests(testsJson) { 14 | const specsN = Object.keys(testsJson).length 15 | let testsN = 0 16 | Object.keys(testsJson).forEach((filename) => { 17 | const n = testsJson[filename].counts.tests 18 | testsN += n 19 | }) 20 | return { specsN, testsN } 21 | } 22 | 23 | // poor man's bundle 24 | const htmlScripts = ` 25 | ${countTheseTests.toString()} 26 | 27 | ${countPendingTests.toString()} 28 | 29 | ${addCounts.toString()} 30 | 31 | ${countTests.toString()} 32 | 33 | ${preprocessAndTags.toString()} 34 | 35 | ${doTagsMatch.toString()} 36 | 37 | ${combineTags.toString()} 38 | 39 | ${removeEmptyNodes.toString()} 40 | 41 | ${pickTaggedTests.toString()} 42 | 43 | ${pickTaggedTestsFrom.toString()} 44 | 45 | ${testsToHtml.toString()} 46 | ` 47 | 48 | const styles = ` 49 | :root { 50 | --primary-color: #2c3e50; 51 | --secondary-color: #3498db; 52 | --background-color: #f8f9fa; 53 | --text-color: #2c3e50; 54 | --border-color: #e9ecef; 55 | --tag-bg: #e3f2fd; 56 | --tag-color: #1976d2; 57 | --pending-color: #9e9e9e; 58 | } 59 | 60 | body { 61 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 62 | Oxygen, Ubuntu, Cantarell, sans-serif; 63 | line-height: 1.6; 64 | color: var(--text-color); 65 | background-color: var(--background-color); 66 | margin: 0; 67 | padding: 2rem; 68 | } 69 | 70 | header { 71 | background-color: white; 72 | padding: 1.5rem; 73 | border-radius: 8px; 74 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 75 | margin-bottom: 2rem; 76 | } 77 | 78 | h1 { 79 | margin: 0 0 1rem 0; 80 | color: var(--primary-color); 81 | } 82 | 83 | .filter-tags { 84 | margin: 1rem 0; 85 | display: flex; 86 | gap: 1rem; 87 | flex-wrap: wrap; 88 | align-items: center; 89 | } 90 | 91 | .filter-tag-container { 92 | display: flex; 93 | align-items: center; 94 | gap: 0.5rem; 95 | white-space: nowrap; 96 | } 97 | 98 | .filter-tag { 99 | margin-right: 0.1rem; 100 | } 101 | 102 | .filter-tag-name { 103 | color: var(--secondary-color); 104 | font-weight: 500; 105 | } 106 | 107 | .filter-tag-count { 108 | color: var(--pending-color); 109 | font-weight: 300; 110 | } 111 | 112 | .specs { 113 | list-style-type: none; 114 | padding: 0; 115 | margin: 0; 116 | } 117 | 118 | .spec { 119 | background-color: white; 120 | border-radius: 8px; 121 | padding: 1.5rem; 122 | margin-bottom: 1.5rem; 123 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 124 | } 125 | 126 | .filename { 127 | color: var(--primary-color); 128 | margin: 0 0 1rem 0; 129 | font-size: 1.2rem; 130 | border-bottom: 2px solid var(--border-color); 131 | padding-bottom: 0.5rem; 132 | } 133 | 134 | .tests { 135 | list-style-type: none; 136 | padding-left: 1.5rem; 137 | margin: 0; 138 | } 139 | 140 | .suite { 141 | margin: 0.5rem 0; 142 | color: var(--primary-color); 143 | font-weight: 500; 144 | } 145 | 146 | .test { 147 | margin: 0.25rem 0; 148 | color: var(--text-color); 149 | } 150 | 151 | .tag { 152 | background-color: var(--tag-bg); 153 | color: var(--tag-color); 154 | padding: 0.2em 0.5em; 155 | border-radius: 4px; 156 | font-size: 0.9em; 157 | margin-left: 0.5rem; 158 | font-weight: normal; 159 | } 160 | 161 | .pending { 162 | color: var(--pending-color); 163 | font-style: italic; 164 | } 165 | 166 | .name { 167 | font-weight: 500; 168 | } 169 | 170 | .test .name { 171 | font-weight: 300; 172 | } 173 | ` 174 | 175 | function testsToHtml(tests) { 176 | if (!Array.isArray(tests)) { 177 | return '' 178 | } 179 | return ` 180 |
    181 | ${tests 182 | .map((test) => { 183 | const tags = test.tags || [] 184 | const tagList = tags 185 | .map((tag) => `${tag}`) 186 | .join('\n') 187 | if (test.type === 'test') { 188 | const classNames = test.pending ? 'test pending' : 'test' 189 | return `
  • ${test.name} ${tagList}
  • ` 190 | } else if (test.type === 'suite') { 191 | const suitesHtml = testsToHtml(test.suites) 192 | const testsHtml = testsToHtml(test.tests) 193 | return ` 194 |
  • 195 | ${test.name} ${tagList} 196 | ${suitesHtml} 197 | ${testsHtml} 198 |
  • 199 | ` 200 | } else { 201 | throw new Error(`Unknown test type: ${test.type}`) 202 | } 203 | }) 204 | .join('\n')} 205 |
206 | ` 207 | } 208 | 209 | /** 210 | * Takes the test JSON object with specs and tags 211 | * and returns a full static self-contained HTML file 212 | * that can be used to display the tests in a browser. 213 | * @param {Object} testsJson - The test JSON object with specs and tags 214 | * @param {Object} tagTestCounts - The tag test counts 215 | * @returns {string} - The HTML string 216 | */ 217 | function toHtml(testsJson, tagTestCounts = {}) { 218 | const { specsN, testsN } = countTheseTests(testsJson) 219 | 220 | // show tags in alphabetical order 221 | const allTags = Object.keys(tagTestCounts).sort() 222 | 223 | const html = ` 224 | 225 | 226 | Cypress Tests 227 | 230 | 266 | 267 | 268 |
269 |

Cypress Tests

270 |

271 | ${specsN} specs, ${testsN} tests 272 |

273 |

274 | ${allTags 275 | .map( 276 | (tag) => 277 | ` 278 | ${tag} 279 | (${tagTestCounts[tag]}) 280 | `, 281 | ) 282 | .join(' ')} 283 |

284 |
285 |
286 |
    287 | ${Object.keys(testsJson) 288 | .map((filename) => { 289 | return ` 290 |
  • 291 |

    ${filename}

    292 | ${testsToHtml(testsJson[filename].tests)} 293 |
  • 294 | ` 295 | }) 296 | .join('\n')} 297 |
298 |
299 | 300 | 301 | ` 302 | return html 303 | } 304 | 305 | module.exports = { 306 | toHtml, 307 | } 308 | -------------------------------------------------------------------------------- /src/print.js: -------------------------------------------------------------------------------- 1 | const pluralize = require('pluralize') 2 | const { formatTestList } = require('find-test-names') 3 | const { sumTestCounts } = require('./count') 4 | 5 | /** 6 | * Outputs a string representation of the json test results object, 7 | * like a tree of suites and tests. 8 | */ 9 | function stringFileTests(fileName, fileInfo, tagged) { 10 | if (tagged) { 11 | const headerLine = fileName 12 | return headerLine 13 | } else { 14 | const testCount = pluralize('test', fileInfo.counts.tests, true) 15 | const headerLine = fileInfo.counts.pending 16 | ? `${fileName} (${testCount}, ${fileInfo.counts.pending} pending)` 17 | : `${fileName} (${testCount})` 18 | 19 | // console.log(fileInfo.tests) 20 | const body = formatTestList(fileInfo.tests) 21 | 22 | return headerLine + '\n' + body + '\n' 23 | } 24 | } 25 | 26 | function stringAllInfo(allInfo, tagged) { 27 | if (!tagged) { 28 | let fileCount = 0 29 | let testCount = 0 30 | let pendingTestCount = 0 31 | const allInfoString = Object.keys(allInfo) 32 | .map((fileName) => { 33 | const fileInfo = allInfo[fileName] 34 | fileCount += 1 35 | testCount += fileInfo.counts.tests 36 | pendingTestCount += fileInfo.counts.pending 37 | return stringFileTests(fileName, fileInfo, tagged) 38 | }) 39 | .join('\n') 40 | // footer line is something like 41 | // found 2 specs (4 tests, 1 pending) 42 | let footer = `found ${pluralize('spec', fileCount, true)}` 43 | const testWord = pluralize('test', testCount, true) 44 | if (pendingTestCount) { 45 | footer += ` (${testWord}, ${pendingTestCount} pending)` 46 | } else { 47 | footer += ` (${testWord})` 48 | } 49 | 50 | return allInfoString + '\n' + footer 51 | } else { 52 | const allInfoString = Object.keys(allInfo) 53 | .map((fileName) => { 54 | const fileInfo = allInfo[fileName] 55 | return stringFileTests(fileName, fileInfo, tagged) 56 | }) 57 | .join(',') 58 | 59 | return allInfoString 60 | } 61 | } 62 | 63 | function getJustTheTestMarkdown(tests, parentName = '', markdown = []) { 64 | if (!tests) { 65 | return 66 | } 67 | 68 | const name = parentName + tests.name 69 | 70 | if (tests.type === 'test') { 71 | const tags = tests?.tags?.join(', ') ?? '' 72 | 73 | markdown.push(`| \`${name}\` | ${tags} |`) 74 | 75 | return 76 | } else if (tests.type === 'suite') { 77 | const prefix = `${name} / ` 78 | 79 | getJustTheTestMarkdown(tests.tests, prefix, markdown) 80 | 81 | tests.suites?.forEach((suite) => { 82 | getJustTheTestMarkdown(suite, prefix, markdown) 83 | }) 84 | 85 | return 86 | } 87 | 88 | // tests can include regular tests plus suites nodes 89 | tests.forEach((testOrSuite) => 90 | getJustTheTestMarkdown(testOrSuite, parentName, markdown), 91 | ) 92 | 93 | return markdown 94 | } 95 | 96 | /** 97 | * Returns a single string with all specs and test names 98 | * as a Markdown table 99 | * @returns {string} 100 | */ 101 | function stringMarkdownTests(allInfo) { 102 | const testWord = (counts) => pluralize('test', counts.tests, true) 103 | 104 | const counts = sumTestCounts(allInfo) 105 | const specNames = Object.keys(allInfo) 106 | const specWord = pluralize('Spec', specNames.length, true) 107 | 108 | const markdown = [ 109 | `| ${specWord} with ${testWord(counts)} | Tags |`, 110 | '| --- | --- |', 111 | ] 112 | 113 | specNames.forEach((fileName) => { 114 | const { counts, tests } = allInfo[fileName] 115 | 116 | markdown.push(`| **\`${fileName}\`** (${testWord(counts)}) ||`) 117 | markdown.push(...getJustTheTestMarkdown(tests)) 118 | }) 119 | 120 | return markdown.join('\n') 121 | } 122 | 123 | module.exports = { 124 | stringFileTests, 125 | stringAllInfo, 126 | stringMarkdownTests, 127 | } 128 | -------------------------------------------------------------------------------- /src/tagged.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // const debug = require('debug')('find-cypress-specs:tagged') 4 | const { addCounts } = require('./count') 5 | 6 | function arraysOverlap(a, b) { 7 | if (!Array.isArray(a) || !Array.isArray(b)) { 8 | return false 9 | } 10 | 11 | if (a.length < b.length) { 12 | return a.some((item) => b.includes(item)) 13 | } else { 14 | return b.some((item) => a.includes(item)) 15 | } 16 | } 17 | 18 | function combineTags(tags, requiredTags) { 19 | if (tags && requiredTags) { 20 | return [].concat(tags).concat(requiredTags) 21 | } 22 | if (tags) { 23 | return [...tags] 24 | } 25 | if (requiredTags) { 26 | return [...requiredTags] 27 | } 28 | } 29 | 30 | /** 31 | * @param {string[]} tagExpressions 32 | */ 33 | function preprocessAndTags(tagExpressions) { 34 | return tagExpressions.map((tagExpression) => { 35 | if (tagExpression.includes('+')) { 36 | return tagExpression.split('+') 37 | } 38 | return tagExpression 39 | }) 40 | } 41 | 42 | /** 43 | * Tag expressions can be a single tag, like `@user` 44 | * or a combination of tags, like `@user+@smoke` to match using AND 45 | * @param {string[]} effectiveTestTags 46 | * @param {string[]} tagExpressions 47 | */ 48 | function doTagsMatch(effectiveTestTags, tagExpressions) { 49 | const orTags = preprocessAndTags(tagExpressions) 50 | 51 | // debug('doTagsMatch', { effectiveTestTags, orTags }) 52 | 53 | return orTags.some((orTag) => { 54 | if (Array.isArray(orTag)) { 55 | return orTag.every((tag) => effectiveTestTags.includes(tag)) 56 | } 57 | return effectiveTestTags.includes(orTag) 58 | }) 59 | } 60 | 61 | /** 62 | * Finds all tests tagged with specific tag(s) 63 | * @param {object[]} tests List of tests 64 | * @param {string|string[]} tag Tag or array of tags to filter by 65 | * @param {string[]} effectiveTags List of effective test tags from the parents 66 | * @warning modifies the tests in place 67 | */ 68 | function pickTaggedTests(tests, tag, effectiveTags = []) { 69 | if (!Array.isArray(tests)) { 70 | return false 71 | } 72 | const tags = Array.isArray(tag) ? tag : [tag] 73 | const filteredTests = tests.filter((test) => { 74 | if (test.type === 'test') { 75 | // TODO: combine all tags plus effective tags 76 | const combinedTestTags = combineTags(test.tags, test.requiredTags) 77 | const effectiveTestTags = combineTags(combinedTestTags, effectiveTags) 78 | if (effectiveTestTags) { 79 | if (doTagsMatch(effectiveTestTags, tags)) { 80 | return true 81 | } 82 | } 83 | } else if (test.type === 'suite') { 84 | const combinedTestTags = combineTags(test.tags, test.requiredTags) 85 | const effectiveTestTags = combineTags(combinedTestTags, effectiveTags) 86 | if (effectiveTestTags) { 87 | if (doTagsMatch(effectiveTestTags, tags)) { 88 | return true 89 | } 90 | } 91 | 92 | // maybe there is some test inside this suite 93 | // with the tag? Filter all other tests 94 | // make sure the copy is non-destructive to the current array 95 | const suiteTags = [...(effectiveTestTags || [])] 96 | return ( 97 | pickTaggedTests(test.tests, tags, suiteTags) || 98 | pickTaggedTests(test.suites, tags, suiteTags) 99 | ) 100 | } 101 | }) 102 | tests.length = 0 103 | tests.push(...filteredTests) 104 | return filteredTests.length > 0 105 | } 106 | 107 | // note: modifies the tests in place 108 | function leavePendingTests(tests) { 109 | if (!Array.isArray(tests)) { 110 | return false 111 | } 112 | const filteredTests = tests.filter((test) => { 113 | if (test.type === 'test') { 114 | return test.pending === true 115 | } else if (test.type === 'suite') { 116 | // maybe there is some test inside this suite 117 | // with the tag? Filter all other tests 118 | return leavePendingTests(test.tests) || leavePendingTests(test.suites) 119 | } 120 | }) 121 | tests.length = 0 122 | tests.push(...filteredTests) 123 | return filteredTests.length > 0 124 | } 125 | 126 | function removeEmptyNodes(json) { 127 | Object.keys(json).forEach((filename) => { 128 | const fileTests = json[filename].tests 129 | if (!fileTests.length) { 130 | delete json[filename] 131 | } 132 | }) 133 | return json 134 | } 135 | 136 | /** 137 | * Takes an object of tests collected from all files, 138 | * and removes all tests that do not have the given tag applied. 139 | * Modifies the given object in place. 140 | * @param {object} json Processed tests as a json tree 141 | * @param {string|string[]} tag Tag or array of tags to filter by 142 | */ 143 | function pickTaggedTestsFrom(json, tag = []) { 144 | // console.log(JSON.stringify(json, null, 2)) 145 | Object.keys(json).forEach((filename) => { 146 | const fileTests = json[filename].tests 147 | // debug('picking tagged tests from %s', filename) 148 | pickTaggedTests(fileTests, tag) 149 | }) 150 | 151 | const result = removeEmptyNodes(json) 152 | addCounts(result) 153 | return result 154 | } 155 | 156 | /** 157 | * Takes an object of tests collected from all files, 158 | * and leaves only the pending tests. 159 | * Modifies the given object in place. 160 | */ 161 | function leavePendingTestsOnly(json) { 162 | Object.keys(json).forEach((filename) => { 163 | const fileTests = json[filename].tests 164 | leavePendingTests(fileTests) 165 | }) 166 | 167 | const result = removeEmptyNodes(json) 168 | addCounts(result) 169 | return result 170 | } 171 | 172 | module.exports = { 173 | arraysOverlap, 174 | pickTaggedTestsFrom, 175 | removeEmptyNodes, 176 | pickTaggedTests, 177 | leavePendingTestsOnly, 178 | preprocessAndTags, 179 | doTagsMatch, 180 | combineTags, 181 | } 182 | -------------------------------------------------------------------------------- /src/tests-counts.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const debug = require('debug')('find-cypress-specs') 3 | const { getSpecs, getTests } = require('.') 4 | 5 | /** 6 | * Finds all E2E and component tests and returns totals 7 | */ 8 | function getTestCounts() { 9 | debug('finding all e2e specs') 10 | const e2eSpecs = getSpecs(undefined, 'e2e') 11 | debug('found %d e2e specs', e2eSpecs.length) 12 | debug('finding all component specs') 13 | const componentSpecs = getSpecs(undefined, 'component') 14 | debug('found %d component specs', componentSpecs.length) 15 | 16 | debug('counting all e2e tests') 17 | const { jsonResults: e2eResults } = getTests(e2eSpecs) 18 | debug(e2eResults) 19 | let nE2E = 0 20 | Object.keys(e2eResults).forEach((filename) => { 21 | const n = e2eResults[filename].counts.tests 22 | nE2E += n 23 | }) 24 | debug('found %d E2E tests', nE2E) 25 | 26 | debug('counting all component tests') 27 | const { jsonResults: componentResults } = getTests(componentSpecs) 28 | debug(componentResults) 29 | let nComponent = 0 30 | Object.keys(componentResults).forEach((filename) => { 31 | const n = componentResults[filename].counts.tests 32 | nComponent += n 33 | }) 34 | debug('found %d component tests', nComponent) 35 | return { nE2E, nComponent } 36 | } 37 | 38 | module.exports = { getTestCounts } 39 | -------------------------------------------------------------------------------- /test-components/comp1.cy.js: -------------------------------------------------------------------------------- 1 | describe('Counter', () => { 2 | it('works', () => { 3 | cy.mount() 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /test-components/comp2.cy.ts: -------------------------------------------------------------------------------- 1 | describe('mock component suite 2', () => { 2 | it('works', () => {}) 3 | }) 4 | -------------------------------------------------------------------------------- /test-effective-tags/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /test-effective-tags/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test-effective-tags/cypress/integration/spec1.js: -------------------------------------------------------------------------------- 1 | describe('user tests', { tags: '@user' }, () => { 2 | it('loads', { tags: '@sanity' }, () => { 3 | // something here 4 | // this test should have effective tags ['@user', '@sanity'] 5 | }) 6 | 7 | it('works again', () => { 8 | // something here 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test-effective-tags/cypress/integration/spec2.js: -------------------------------------------------------------------------------- 1 | describe('user home tests', () => { 2 | // both tags are listed at the test level 3 | it('loads', { tags: ['@user', '@sanity'] }, () => { 4 | // this test should have effective tags ['@user', '@sanity'] 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /test-effective-tags/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-required-tags", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-required-tags", 9 | "version": "1.0.0", 10 | "license": "ISC" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test-effective-tags/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-effective-tags", 3 | "version": "1.0.0", 4 | "description": "Spec has tests with effective tags", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "cypress run", 9 | "effective": "DEBUG=find-cypress-specs,find-test-names node ../bin/find --names --tagged @user+@sanity" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /test-exclusive/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /test-exclusive/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test-exclusive/cypress/integration/spec1.js: -------------------------------------------------------------------------------- 1 | it.only('runs by itself', () => { 2 | // something here 3 | }) 4 | 5 | it('another test', () => { 6 | // something here 7 | }) 8 | -------------------------------------------------------------------------------- /test-exclusive/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-ts", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "typescript": "^4.8.3" 13 | } 14 | }, 15 | "node_modules/typescript": { 16 | "version": "4.8.3", 17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 18 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 19 | "dev": true, 20 | "bin": { 21 | "tsc": "bin/tsc", 22 | "tsserver": "bin/tsserver" 23 | }, 24 | "engines": { 25 | "node": ">=4.2.0" 26 | } 27 | } 28 | }, 29 | "dependencies": { 30 | "typescript": { 31 | "version": "4.8.3", 32 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 33 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 34 | "dev": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test-exclusive/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-parsing-error", 3 | "version": "1.0.0", 4 | "description": "Spec has exclusive tests", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "cypress run", 9 | "test-names": "DEBUG=find-cypress-specs,find-test-names node ../bin/find --names" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /test-json/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /test-json/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test-json/cypress/integration/spec1.js: -------------------------------------------------------------------------------- 1 | it('has first test') 2 | 3 | it('has second test') 4 | -------------------------------------------------------------------------------- /test-json/cypress/integration/spec2.js: -------------------------------------------------------------------------------- 1 | it('has first test') 2 | 3 | it('has second test') 4 | -------------------------------------------------------------------------------- /test-json/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-ts", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "typescript": "^4.8.3" 13 | } 14 | }, 15 | "node_modules/typescript": { 16 | "version": "4.8.3", 17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 18 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 19 | "dev": true, 20 | "bin": { 21 | "tsc": "bin/tsc", 22 | "tsserver": "bin/tsserver" 23 | }, 24 | "engines": { 25 | "node": ">=4.2.0" 26 | } 27 | } 28 | }, 29 | "dependencies": { 30 | "typescript": { 31 | "version": "4.8.3", 32 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 33 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 34 | "dev": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-json", 3 | "version": "1.0.0", 4 | "description": "Test finding Cypress specs with config.json file", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "cypress run", 9 | "demo-tests": "DEBUG=find-cypress-specs node ../bin/find", 10 | "deps": "../node_modules/.bin/spec-change --folder cypress" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /test-modules/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /test-modules/cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | // baseUrl, etc 6 | supportFile: false, 7 | fixturesFolder: false, 8 | setupNodeEvents(on, config) { 9 | // implement node event listeners here 10 | // and load any plugins that require the Node environment 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /test-modules/cypress/e2e/spec1.cy.js: -------------------------------------------------------------------------------- 1 | it('has first test') 2 | 3 | it('has second test') 4 | -------------------------------------------------------------------------------- /test-modules/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-ts", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "typescript": "^4.8.3" 13 | } 14 | }, 15 | "node_modules/typescript": { 16 | "version": "4.8.3", 17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 18 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 19 | "dev": true, 20 | "bin": { 21 | "tsc": "bin/tsc", 22 | "tsserver": "bin/tsserver" 23 | }, 24 | "engines": { 25 | "node": ">=4.2.0" 26 | } 27 | } 28 | }, 29 | "dependencies": { 30 | "typescript": { 31 | "version": "4.8.3", 32 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 33 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 34 | "dev": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test-modules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-modules", 3 | "version": "1.0.0", 4 | "description": "Test finding Cypress specs in module projects", 5 | "private": true, 6 | "main": "index.js", 7 | "type": "module", 8 | "scripts": { 9 | "test": "cypress run", 10 | "demo-tests": "DEBUG=find-cypress-specs node ../bin/find", 11 | "deps": "../node_modules/.bin/spec-change --folder cypress" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC" 16 | } 17 | -------------------------------------------------------------------------------- /test-parsing-error/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /test-parsing-error/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test-parsing-error/cypress/integration/spec1.js: -------------------------------------------------------------------------------- 1 | /* a few syntax errors */ 2 | ff /.. it('has first test') 3 | 4 | it('has second test') 5 | -------------------------------------------------------------------------------- /test-parsing-error/cypress/integration/spec2.js: -------------------------------------------------------------------------------- 1 | it('has first test') 2 | 3 | it('has second test') 4 | -------------------------------------------------------------------------------- /test-parsing-error/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-ts", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "typescript": "^4.8.3" 13 | } 14 | }, 15 | "node_modules/typescript": { 16 | "version": "4.8.3", 17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 18 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 19 | "dev": true, 20 | "bin": { 21 | "tsc": "bin/tsc", 22 | "tsserver": "bin/tsserver" 23 | }, 24 | "engines": { 25 | "node": ">=4.2.0" 26 | } 27 | } 28 | }, 29 | "dependencies": { 30 | "typescript": { 31 | "version": "4.8.3", 32 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 33 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 34 | "dev": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test-parsing-error/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-parsing-error", 3 | "version": "1.0.0", 4 | "description": "Spec js is invalid", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "cypress run", 9 | "test-names": "DEBUG=find-cypress-specs node ../bin/find --names" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /test-required-tags/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /test-required-tags/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test-required-tags/cypress/integration/spec1.js: -------------------------------------------------------------------------------- 1 | it('works', { tags: 'user', requiredTags: '@foo' }, () => { 2 | // something here 3 | }) 4 | 5 | it('does not work', { requiredTags: ['@foo', '@bar'] }, () => { 6 | // something here 7 | }) 8 | -------------------------------------------------------------------------------- /test-required-tags/cypress/integration/spec2.js: -------------------------------------------------------------------------------- 1 | it('spec 2 works', { tags: 'user', requiredTags: '@foo' }, () => { 2 | // something here 3 | }) 4 | -------------------------------------------------------------------------------- /test-required-tags/cypress/integration/spec3.js: -------------------------------------------------------------------------------- 1 | // the parent required tags should apply to the tests inside 2 | // https://github.com/bahmutov/find-cypress-specs/issues/213 3 | 4 | // required test tag 5 | describe('parent1', { requiredTags: '@parent1' }, () => { 6 | it('child1', () => {}) 7 | }) 8 | 9 | // normal test tag 10 | describe('parent2', { tags: '@parent2' }, () => { 11 | it('child2', () => {}) 12 | }) 13 | -------------------------------------------------------------------------------- /test-required-tags/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-required-tags", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-required-tags", 9 | "version": "1.0.0", 10 | "license": "ISC" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test-required-tags/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-required-tags", 3 | "version": "1.0.0", 4 | "description": "Spec has tests with required tags", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "cypress run", 9 | "test-names": "DEBUG=find-cypress-specs,find-test-names node ../bin/find --names", 10 | "test-parent1": "DEBUG=find-cypress-specs,find-test-names,find-cypress-specs:tagged DEBUG_DEPTH=5 node ../bin/find --names --tagged @parent1", 11 | "test-parent2": "DEBUG=find-cypress-specs,find-test-names,find-cypress-specs:tagged DEBUG_DEPTH=5 node ../bin/find --names --tagged @parent2", 12 | "count-all-tags": "DEBUG=find-cypress-specs,find-test-names DEBUG_DEPTH=10 node ../bin/find --tags" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC" 17 | } 18 | -------------------------------------------------------------------------------- /test-skip-node-modules/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /test-skip-node-modules/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | // baseUrl, etc 6 | supportFile: false, 7 | fixturesFolder: false, 8 | setupNodeEvents(on, config) { 9 | // implement node event listeners here 10 | // and load any plugins that require the Node environment 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /test-skip-node-modules/cypress/e2e/node_modules/spec2.cy.js: -------------------------------------------------------------------------------- 1 | it('has first test') 2 | 3 | it('has second test') 4 | -------------------------------------------------------------------------------- /test-skip-node-modules/cypress/e2e/spec1.cy.js: -------------------------------------------------------------------------------- 1 | it('has first test') 2 | 3 | it('has second test') 4 | -------------------------------------------------------------------------------- /test-skip-node-modules/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-ts", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "typescript": "^4.8.3" 13 | } 14 | }, 15 | "node_modules/typescript": { 16 | "version": "4.8.3", 17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 18 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 19 | "dev": true, 20 | "bin": { 21 | "tsc": "bin/tsc", 22 | "tsserver": "bin/tsserver" 23 | }, 24 | "engines": { 25 | "node": ">=4.2.0" 26 | } 27 | } 28 | }, 29 | "dependencies": { 30 | "typescript": { 31 | "version": "4.8.3", 32 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 33 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 34 | "dev": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test-skip-node-modules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-skip-node-modules", 3 | "version": "1.0.0", 4 | "description": "Test ignoring node_modules folder", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "cypress run", 9 | "demo-tests": "DEBUG=find-cypress-specs node ../bin/find", 10 | "deps": "../node_modules/.bin/spec-change --folder cypress" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /test-ts-aliases/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /test-ts-aliases/cypress.config.custom.ts: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | supportFile: false, 6 | fixturesFolder: false, 7 | setupNodeEvents(on, config) { 8 | }, 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /test-ts-aliases/cypress.config.ts: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | // baseUrl, etc 6 | supportFile: false, 7 | fixturesFolder: false, 8 | setupNodeEvents(on, config) { 9 | // implement node event listeners here 10 | // and load any plugins that require the Node environment 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /test-ts-aliases/cypress/e2e/spec1.cy.ts: -------------------------------------------------------------------------------- 1 | import { getName } from '@support/utils' 2 | 3 | it('has first test', () => { 4 | expect(getName()).to.equal('Joe') 5 | }) 6 | 7 | it('has second test') 8 | -------------------------------------------------------------------------------- /test-ts-aliases/cypress/e2e/spec2.cy.ts: -------------------------------------------------------------------------------- 1 | import { getName } from '@support/utils' 2 | 3 | it('has first test') 4 | 5 | it('has second test', () => { 6 | expect(getName()).to.equal('Joe') 7 | }) 8 | -------------------------------------------------------------------------------- /test-ts-aliases/cypress/support/utils.ts: -------------------------------------------------------------------------------- 1 | export function getName() { 2 | return 'Joe' 3 | } 4 | -------------------------------------------------------------------------------- /test-ts-aliases/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts-aliases", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-ts-aliases", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "typescript": "^4.8.3" 13 | } 14 | }, 15 | "node_modules/typescript": { 16 | "version": "4.8.3", 17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 18 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 19 | "dev": true, 20 | "bin": { 21 | "tsc": "bin/tsc", 22 | "tsserver": "bin/tsserver" 23 | }, 24 | "engines": { 25 | "node": ">=4.2.0" 26 | } 27 | } 28 | }, 29 | "dependencies": { 30 | "typescript": { 31 | "version": "4.8.3", 32 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 33 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 34 | "dev": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test-ts-aliases/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts-aliases", 3 | "version": "1.0.0", 4 | "description": "Test tracing dependencies with TS import aliases", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "cypress run", 9 | "demo-tests": "DEBUG=find-cypress-specs node ../bin/find", 10 | "deps": "../node_modules/.bin/spec-change --folder cypress" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "typescript": "^4.8.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test-ts-aliases/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["cypress.config.ts", "cypress/e2e/*.cy.ts"], 3 | "compilerOptions": { 4 | "baseUrl": "cypress", 5 | "paths": { 6 | "@support/*": ["support/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test-ts-import/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /test-ts-import/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | // baseUrl, etc 6 | supportFile: false, 7 | fixturesFolder: false, 8 | setupNodeEvents(on, config) { 9 | // implement node event listeners here 10 | // and load any plugins that require the Node environment 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /test-ts-import/cypress/e2e/spec1.cy.ts: -------------------------------------------------------------------------------- 1 | it('has first test') 2 | 3 | it('has second test') 4 | -------------------------------------------------------------------------------- /test-ts-import/cypress/e2e/spec2.cy.ts: -------------------------------------------------------------------------------- 1 | it('has first test') 2 | 3 | it('has second test') 4 | -------------------------------------------------------------------------------- /test-ts-import/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-ts", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "typescript": "^4.8.3" 13 | } 14 | }, 15 | "node_modules/typescript": { 16 | "version": "4.8.3", 17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 18 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 19 | "dev": true, 20 | "bin": { 21 | "tsc": "bin/tsc", 22 | "tsserver": "bin/tsserver" 23 | }, 24 | "engines": { 25 | "node": ">=4.2.0" 26 | } 27 | } 28 | }, 29 | "dependencies": { 30 | "typescript": { 31 | "version": "4.8.3", 32 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 33 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 34 | "dev": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test-ts-import/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts", 3 | "version": "1.0.0", 4 | "description": "Test finding Cypress specs with TS config file", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "cypress run", 9 | "demo-tests": "DEBUG=find-cypress-specs node ../bin/find", 10 | "deps": "../node_modules/.bin/spec-change --folder cypress" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "typescript": "^4.8.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test-ts-import/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["cypress.config.ts", "cypress/e2e/*.cy.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "ES2015"], 5 | "module": "es6", 6 | "target": "es6", 7 | "strict": true, 8 | "allowJs": true, 9 | "noEmit": true, 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true 12 | }, 13 | "ts-node": { 14 | "compilerOptions": { 15 | "module": "commonjs" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test-ts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /test-ts/cypress.config.custom.ts: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | supportFile: false, 6 | fixturesFolder: false, 7 | setupNodeEvents(on, config) { 8 | }, 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /test-ts/cypress.config.ts: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | // baseUrl, etc 6 | supportFile: false, 7 | fixturesFolder: false, 8 | setupNodeEvents(on, config) { 9 | // implement node event listeners here 10 | // and load any plugins that require the Node environment 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /test-ts/cypress/e2e/spec1.cy.ts: -------------------------------------------------------------------------------- 1 | it('has first test') 2 | 3 | it('has second test') 4 | -------------------------------------------------------------------------------- /test-ts/cypress/e2e/spec2.cy.ts: -------------------------------------------------------------------------------- 1 | it('has first test') 2 | 3 | it('has second test') 4 | -------------------------------------------------------------------------------- /test-ts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-ts", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "typescript": "^4.8.3" 13 | } 14 | }, 15 | "node_modules/typescript": { 16 | "version": "4.8.3", 17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 18 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 19 | "dev": true, 20 | "bin": { 21 | "tsc": "bin/tsc", 22 | "tsserver": "bin/tsserver" 23 | }, 24 | "engines": { 25 | "node": ">=4.2.0" 26 | } 27 | } 28 | }, 29 | "dependencies": { 30 | "typescript": { 31 | "version": "4.8.3", 32 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", 33 | "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", 34 | "dev": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ts", 3 | "version": "1.0.0", 4 | "description": "Test finding Cypress specs with TS config file", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "cypress run", 9 | "demo-tests": "DEBUG=find-cypress-specs node ../bin/find", 10 | "deps": "../node_modules/.bin/spec-change --folder cypress" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "typescript": "^4.8.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["cypress.config.ts", "cypress/e2e/*.cy.ts"] 3 | } 4 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { findCypressSpecs } = require('..') 3 | const { toRelative } = require('../src/files') 4 | 5 | test('string ignore pattern v9', (t) => { 6 | t.plan(1) 7 | const specs = findCypressSpecs({ 8 | integrationFolder: 'cypress/e2e', 9 | testFiles2: '**/*.js', 10 | ignoreTestFiles: 'utils.js', 11 | version: '9.7.0', 12 | }) 13 | t.deepEqual(specs, [ 14 | 'cypress/e2e/spec-b.cy.js', 15 | 'cypress/e2e/spec.cy.js', 16 | 'cypress/e2e/featureA/user.cy.ts', 17 | ]) 18 | }) 19 | 20 | test('array string ignore pattern v10', (t) => { 21 | t.plan(1) 22 | 23 | const specs = findCypressSpecs({ 24 | version: '10.0.0', 25 | e2e: { 26 | specPattern: 'cypress/e2e/**/*.cy.{js,ts}', 27 | excludeSpecPattern: ['utils.js'], 28 | }, 29 | }) 30 | 31 | t.deepEqual(specs, [ 32 | 'cypress/e2e/spec-b.cy.js', 33 | 'cypress/e2e/spec.cy.js', 34 | 'cypress/e2e/featureA/user.cy.ts', 35 | ]) 36 | }) 37 | 38 | test('string ignore pattern v10', (t) => { 39 | t.plan(1) 40 | const specs = findCypressSpecs({ 41 | version: '10.0.0', 42 | e2e: { 43 | specPattern: 'cypress/e2e/**/*.cy.{js,ts}', 44 | excludeSpecPattern: 'utils.js', 45 | }, 46 | }) 47 | t.deepEqual(specs, [ 48 | 'cypress/e2e/spec-b.cy.js', 49 | 'cypress/e2e/spec.cy.js', 50 | 'cypress/e2e/featureA/user.cy.ts', 51 | ]) 52 | }) 53 | 54 | test('specific files', (t) => { 55 | t.plan(1) 56 | const specs = findCypressSpecs({ 57 | version: '10.0.0', 58 | e2e: { 59 | specPattern: 'cypress/e2e/featureA/user.cy*.ts', 60 | excludeSpecPattern: ['utils.js'], 61 | }, 62 | }) 63 | 64 | t.deepEqual(specs, ['cypress/e2e/featureA/user.cy.ts']) 65 | }) 66 | 67 | test('list of specific files', (t) => { 68 | t.plan(1) 69 | const specs = findCypressSpecs({ 70 | version: '10.0.0', 71 | e2e: { 72 | specPattern: [ 73 | 'cypress/e2e/featureA/user.cy*.ts', 74 | 'cypress/e2e/spec-b.cy.js', 75 | ], 76 | excludeSpecPattern: ['utils.js'], 77 | }, 78 | }) 79 | t.deepEqual(specs, [ 80 | 'cypress/e2e/featureA/user.cy.ts', 81 | 'cypress/e2e/spec-b.cy.js', 82 | ]) 83 | }) 84 | 85 | test('returns absolute filenames', (t) => { 86 | t.plan(1) 87 | const specs = toRelative( 88 | findCypressSpecs( 89 | { 90 | version: '10.0.0', 91 | e2e: { 92 | specPattern: [ 93 | 'cypress/e2e/featureA/user.cy*.ts', 94 | 'cypress/e2e/spec-b.cy.js', 95 | ], 96 | excludeSpecPattern: ['utils.js'], 97 | }, 98 | }, 99 | 'e2e', 100 | true, 101 | ), 102 | ) 103 | t.deepEqual(specs, [ 104 | 'cypress/e2e/featureA/user.cy.ts', 105 | 'cypress/e2e/spec-b.cy.js', 106 | ]) 107 | }) 108 | -------------------------------------------------------------------------------- /test/branch-tagged.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const execa = require('execa-wrap') 3 | 4 | test('prints changed spec files that have the given tag', async (t) => { 5 | t.plan(1) 6 | // note: all paths are with respect to the repo's root folder 7 | // find all changed specs against the given branch 8 | // but only the specs that have the tests with tag "@alpha" 9 | const result = await execa( 10 | 'node', 11 | [ 12 | '-r', 13 | './mocks/branch-tagged-1.js', 14 | './bin/find', 15 | '--branch', 16 | 'tagged-1', 17 | '--tagged', 18 | '@alpha', 19 | ], 20 | { 21 | filter: ['code', 'stdout'], 22 | }, 23 | ) 24 | // only a single spec should be printed for the given tag 25 | // console.log(result) 26 | t.snapshot(result) 27 | }) 28 | 29 | test('prints number of changed spec files that have the given tag', async (t) => { 30 | t.plan(1) 31 | // note: all paths are with respect to the repo's root folder 32 | // find all changed specs against the given branch 33 | // but only the specs that have the tests with tag "@alpha" 34 | const result = await execa( 35 | 'node', 36 | [ 37 | '-r', 38 | './mocks/branch-tagged-1.js', 39 | './bin/find', 40 | '--branch', 41 | 'tagged-1', 42 | '--tagged', 43 | '@alpha', 44 | '--count', 45 | ], 46 | { 47 | filter: ['code', 'stdout'], 48 | }, 49 | ) 50 | // only a single spec count 1 should be printed for the given tag 51 | // console.log(result) 52 | t.snapshot(result) 53 | }) 54 | 55 | test('no changed specs tagged with the tag', async (t) => { 56 | t.plan(1) 57 | // note: all paths are with respect to the repo's root folder 58 | // find all changed specs against the given branch 59 | // but only the specs that have the tests with tag "@alpha" 60 | const result = await execa( 61 | 'node', 62 | [ 63 | '-r', 64 | './mocks/branch-tagged-1-no-alpha.js', 65 | './bin/find', 66 | '--branch', 67 | 'tagged-1', 68 | '--tagged', 69 | '@alpha', 70 | ], 71 | { 72 | filter: ['code', 'stdout'], 73 | }, 74 | ) 75 | // no changed specs with tag @alpha should be found, empty space 76 | // console.log(result) 77 | t.snapshot(result) 78 | }) 79 | 80 | test('zero changed specs tagged with the tag', async (t) => { 81 | t.plan(1) 82 | // note: all paths are with respect to the repo's root folder 83 | // find all changed specs against the given branch 84 | // but only the specs that have the tests with tag "@alpha" 85 | const result = await execa( 86 | 'node', 87 | [ 88 | '-r', 89 | './mocks/branch-tagged-1-no-alpha.js', 90 | './bin/find', 91 | '--branch', 92 | 'tagged-1', 93 | '--tagged', 94 | '@alpha', 95 | '--count', 96 | ], 97 | { 98 | filter: ['code', 'stdout'], 99 | }, 100 | ) 101 | // no changed specs with tag @alpha should be found, prints 0 102 | // console.log(result) 103 | t.snapshot(result) 104 | }) 105 | -------------------------------------------------------------------------------- /test/branch.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const execa = require('execa-wrap') 3 | 4 | test('prints changed spec files against another branch', async (t) => { 5 | t.plan(1) 6 | // note: all paths are with respect to the repo's root folder 7 | const result = await execa( 8 | 'node', 9 | ['-r', './mocks/branch-1.js', './bin/find', '--branch', 'branch-test-1'], 10 | { 11 | filter: ['code', 'stdout'], 12 | }, 13 | ) 14 | t.snapshot(result) 15 | }) 16 | 17 | test('prints changed spec files against the parent commit', async (t) => { 18 | t.plan(1) 19 | // note: all paths are with respect to the repo's root folder 20 | const result = await execa( 21 | 'node', 22 | [ 23 | '-r', 24 | './mocks/parent-1.js', 25 | './bin/find', 26 | '--branch', 27 | 'parent-1', 28 | '--parent', 29 | ], 30 | { 31 | filter: ['code', 'stdout'], 32 | }, 33 | ) 34 | t.snapshot(result) 35 | }) 36 | 37 | test('prints count of changed spec files against the parent commit', async (t) => { 38 | t.plan(1) 39 | // note: all paths are with respect to the repo's root folder 40 | const result = await execa( 41 | 'node', 42 | [ 43 | '-r', 44 | './mocks/parent-2.js', 45 | './bin/find', 46 | '--branch', 47 | 'parent-2', 48 | '--parent', 49 | '--count', 50 | ], 51 | { 52 | filter: ['code', 'stdout'], 53 | }, 54 | ) 55 | t.snapshot(result) 56 | }) 57 | 58 | test('changed spec files that change because an imported file changed', async (t) => { 59 | t.plan(1) 60 | // note: all paths are with respect to the repo's root folder 61 | const result = await execa( 62 | 'node', 63 | [ 64 | '-r', 65 | './mocks/changed-imported-file.js', 66 | './bin/find', 67 | '--branch', 68 | 'parent-3', 69 | '--parent', 70 | '--trace-imports', 71 | 'cypress', 72 | ], 73 | { 74 | filter: ['code', 'stdout'], 75 | }, 76 | ) 77 | // the mock returns utils.js file changed 78 | // but by tracing the specs that import it 79 | // we should determine that the "spec.cy.js" 80 | // imports it and thus should be considered changed too 81 | t.snapshot(result) 82 | }) 83 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const execa = require('execa-wrap') 3 | 4 | test('prints tags', async (t) => { 5 | t.plan(1) 6 | const result = await execa('node', ['./bin/find', '--tags'], { 7 | filter: ['code', 'stdout'], 8 | }) 9 | t.snapshot(result) 10 | }) 11 | 12 | test('prints test names', async (t) => { 13 | t.plan(1) 14 | const result = await execa('node', ['./bin/find', '--names'], { 15 | filter: ['code', 'stdout'], 16 | }) 17 | t.snapshot(result) 18 | }) 19 | 20 | test('prints test tagged @alpha', async (t) => { 21 | t.plan(1) 22 | const result = await execa( 23 | 'node', 24 | ['./bin/find', '--names', '--tagged', '@alpha'], 25 | { 26 | filter: ['code', 'stdout'], 27 | }, 28 | ) 29 | // console.log(result) 30 | t.snapshot(result) 31 | }) 32 | 33 | test('prints test tagged @user', async (t) => { 34 | t.plan(1) 35 | const result = await execa( 36 | 'node', 37 | ['./bin/find', '--names', '--tagged', '@user'], 38 | { 39 | filter: ['code', 'stdout'], 40 | }, 41 | ) 42 | // console.log(result) 43 | t.snapshot(result) 44 | }) 45 | 46 | test('prints test tagged @main', async (t) => { 47 | t.plan(1) 48 | const result = await execa( 49 | 'node', 50 | ['./bin/find', '--names', '--tagged', '@main'], 51 | { 52 | filter: ['code', 'stdout'], 53 | }, 54 | ) 55 | // console.log(result) 56 | t.snapshot(result) 57 | }) 58 | 59 | test('prints test tagged @main and @alpha', async (t) => { 60 | t.plan(1) 61 | const result = await execa( 62 | 'node', 63 | ['./bin/find', '--names', '--tagged', '@main,@alpha'], 64 | { 65 | filter: ['code', 'stdout'], 66 | }, 67 | ) 68 | // console.log(result) 69 | t.snapshot(result) 70 | }) 71 | 72 | test('prints only the skipped tests', async (t) => { 73 | // prints only the tests that have "it.skip" 74 | t.plan(1) 75 | const result = await execa('node', ['./bin/find', '--names', '--skipped'], { 76 | filter: ['code', 'stdout'], 77 | }) 78 | // console.log(result) 79 | t.snapshot(result) 80 | }) 81 | 82 | test('prints only the skipped test count', async (t) => { 83 | // counts only the tests that have "it.skip" 84 | t.plan(1) 85 | const result = await execa( 86 | 'node', 87 | ['./bin/find', '--names', '--skipped', '--count'], 88 | { 89 | filter: ['code', 'stdout'], 90 | }, 91 | ) 92 | // console.log(result) 93 | t.snapshot(result) 94 | }) 95 | 96 | test('--pending is an alias to --skipped', async (t) => { 97 | // prints only the tests that have "it.skip" 98 | t.plan(1) 99 | const result = await execa('node', ['./bin/find', '--names', '--pending'], { 100 | filter: ['code', 'stdout'], 101 | }) 102 | // console.log(result) 103 | t.snapshot(result) 104 | }) 105 | 106 | test('jsx components', async (t) => { 107 | t.plan(1) 108 | const result = await execa('npm', ['run', '--silent', 'demo-component'], { 109 | filter: ['code', 'stdout'], 110 | }) 111 | t.snapshot(result) 112 | }) 113 | 114 | test('exclusive tests', async (t) => { 115 | t.plan(1) 116 | const result = await execa('npm', ['run', '--silent', 'demo-exclusive'], { 117 | filter: ['code', 'stdout'], 118 | }) 119 | t.snapshot(result) 120 | }) 121 | 122 | test('prints test file names --tagged @alpha', async (t) => { 123 | t.plan(1) 124 | const result = await execa('node', ['./bin/find', '--tagged', '@alpha'], { 125 | filter: ['code', 'stdout'], 126 | }) 127 | t.snapshot(result) 128 | }) 129 | 130 | test('prints test file names --tagged @alpha,@main,@user', async (t) => { 131 | t.plan(1) 132 | const result = await execa('node', ['./bin/find', '--tagged', '@alpha'], { 133 | filter: ['code', 'stdout'], 134 | }) 135 | t.snapshot(result) 136 | }) 137 | 138 | test('prints test file names --tagged empty string', async (t) => { 139 | t.plan(1) 140 | const result = await execa('node', ['./bin/find', '--tagged', ''], { 141 | filter: ['code', 'stdout'], 142 | }) 143 | // there should be no specs tagged an empty string "" 144 | // console.log(result) 145 | t.snapshot(result) 146 | }) 147 | 148 | test('prints the count of specs', async (t) => { 149 | t.plan(1) 150 | const result = await execa('node', ['./bin/find', '--count'], { 151 | filter: ['code', 'stdout'], 152 | }) 153 | // console.log(result) 154 | t.snapshot(result) 155 | }) 156 | 157 | test('prints the count of test files --tagged @alpha with --count', async (t) => { 158 | t.plan(1) 159 | const result = await execa( 160 | 'node', 161 | ['./bin/find', '--tagged', '@alpha', '--count'], 162 | { 163 | filter: ['code', 'stdout'], 164 | }, 165 | ) 166 | // console.log(result) 167 | t.snapshot(result) 168 | }) 169 | 170 | test('prints the number of E2E and component tests', async (t) => { 171 | t.plan(1) 172 | const result = await execa('node', ['./bin/find', '--test-counts'], { 173 | filter: ['code', 'stdout'], 174 | }) 175 | // console.log(result) 176 | t.snapshot(result) 177 | }) 178 | 179 | test('prints tags and required tags', async (t) => { 180 | t.plan(1) 181 | const result = await execa('node', ['../bin/find', '--names'], { 182 | cwd: './test-required-tags', 183 | filter: ['code', 'stdout'], 184 | }) 185 | // console.log(result) 186 | t.snapshot(result) 187 | }) 188 | 189 | test('filters by required tag', async (t) => { 190 | t.plan(1) 191 | const result = await execa( 192 | 'node', 193 | ['../bin/find', '--names', '--tagged', '@bar'], 194 | { 195 | cwd: './test-required-tags', 196 | filter: ['code', 'stdout'], 197 | }, 198 | ) 199 | // console.log(result) 200 | t.snapshot(result) 201 | }) 202 | 203 | test('counts by required tag', async (t) => { 204 | t.plan(1) 205 | const result = await execa( 206 | 'node', 207 | ['../bin/find', '--count', '--tagged', '@bar'], 208 | { 209 | cwd: './test-required-tags', 210 | filter: ['code', 'stdout'], 211 | }, 212 | ) 213 | // console.log(result) 214 | t.snapshot(result) 215 | }) 216 | 217 | test('counts all tags including required', async (t) => { 218 | t.plan(1) 219 | const result = await execa('node', ['../bin/find', '--count', '--tags'], { 220 | cwd: './test-required-tags', 221 | filter: ['code', 'stdout'], 222 | }) 223 | // console.log(result) 224 | t.snapshot(result) 225 | }) 226 | 227 | test('skips specs in the node_modules automatically', async (t) => { 228 | t.plan(2) 229 | const result = await execa('node', ['../bin/find'], { 230 | cwd: './test-skip-node-modules', 231 | filter: ['code', 'stdout'], 232 | }) 233 | // console.log(result) 234 | t.false(result.includes('node_modules')) 235 | t.snapshot(result) 236 | }) 237 | 238 | test('finds tests with BOTH tags using AND syntax', async (t) => { 239 | t.plan(1) 240 | const result = await execa( 241 | 'node', 242 | ['../bin/find', '--names', '--tagged', '@foo+@bar'], 243 | { 244 | cwd: './test-required-tags', 245 | filter: ['code', 'stdout'], 246 | }, 247 | ) 248 | // should find ONLY the test tagged with both @foo and @bar 249 | // console.log(result) 250 | t.snapshot(result) 251 | }) 252 | 253 | test('applies AND syntax to the effective tags', async (t) => { 254 | t.plan(3) 255 | const result = await execa( 256 | 'node', 257 | ['../bin/find', '--names', '--tagged', '@user+@sanity'], 258 | { 259 | cwd: './test-effective-tags', 260 | filter: ['code', 'stdout'], 261 | }, 262 | ) 263 | // should find ONLY the test tagged with both @foo and @bar 264 | console.log(result) 265 | t.true( 266 | result.includes('cypress/integration/spec2.js'), 267 | 'finds the second spec', 268 | ) 269 | t.true( 270 | result.includes('cypress/integration/spec1.js'), 271 | 'finds the first spec', 272 | ) 273 | t.snapshot(result) 274 | }) 275 | -------------------------------------------------------------------------------- /test/count.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const input = require('./tagged.json') 3 | const { addCounts } = require('../src/count') 4 | 5 | test('counts all tests', (t) => { 6 | t.plan(2) 7 | const json = JSON.parse(JSON.stringify(input)) 8 | addCounts(json) 9 | // console.dir(json, { depth: null }) 10 | t.deepEqual(json['cypress/e2e/spec.js'].counts, { 11 | tests: 3, 12 | pending: 0, 13 | }) 14 | t.deepEqual(json['cypress/e2e/featureA/user.js'].counts, { 15 | tests: 2, 16 | pending: 1, 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/grep.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const input = require('./tagged.json') 3 | const { filterByGrep } = require('../src/grep') 4 | 5 | test('filters tests by part of one title', (t) => { 6 | t.plan(1) 7 | const result = filterByGrep(input, 'to be written') 8 | t.deepEqual(result, ['cypress/e2e/featureA/user.js']) 9 | }) 10 | 11 | test('filters tests by several parts', (t) => { 12 | t.plan(1) 13 | const result = filterByGrep(input, 'something,written') 14 | t.deepEqual(result, ['cypress/e2e/spec.js', 'cypress/e2e/featureA/user.js']) 15 | }) 16 | 17 | test('trims the grep strings', (t) => { 18 | t.plan(1) 19 | const result = filterByGrep(input, ' something , written') 20 | t.deepEqual(result, ['cypress/e2e/spec.js', 'cypress/e2e/featureA/user.js']) 21 | }) 22 | 23 | test('skips empty grep strings', (t) => { 24 | t.plan(1) 25 | const result = filterByGrep(input, ' something ,,,, written, ,') 26 | t.deepEqual(result, ['cypress/e2e/spec.js', 'cypress/e2e/featureA/user.js']) 27 | }) 28 | -------------------------------------------------------------------------------- /test/json.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const execa = require('execa-wrap') 3 | 4 | test('prints test names in json', async (t) => { 5 | t.plan(1) 6 | const result = await execa('node', ['./bin/find', '--names', '--json'], { 7 | filter: ['code', 'stdout'], 8 | }) 9 | t.snapshot(result) 10 | }) 11 | 12 | test('prints test names in json using -j', async (t) => { 13 | t.plan(1) 14 | const result = await execa('node', ['./bin/find', '--names', '-j'], { 15 | filter: ['code', 'stdout'], 16 | }) 17 | t.snapshot(result) 18 | }) 19 | 20 | test('prints tags in json', async (t) => { 21 | t.plan(1) 22 | const result = await execa('node', ['./bin/find', '--tags', '--json'], { 23 | filter: ['code', 'stdout'], 24 | }) 25 | t.snapshot(result) 26 | }) 27 | 28 | test('prints tags in json using -j', async (t) => { 29 | t.plan(1) 30 | const result = await execa('node', ['./bin/find', '--tags', '-j'], { 31 | filter: ['code', 'stdout'], 32 | }) 33 | t.snapshot(result) 34 | }) 35 | 36 | test('prints tests tagged with @user in json', async (t) => { 37 | t.plan(1) 38 | const result = await execa( 39 | 'node', 40 | ['./bin/find', '--names', '--json', '--tagged', '@user'], 41 | { 42 | filter: ['code', 'stdout'], 43 | }, 44 | ) 45 | // console.log(result) 46 | t.snapshot(result) 47 | }) 48 | 49 | test('prints tests tagged with @alpha in json', async (t) => { 50 | t.plan(1) 51 | const result = await execa( 52 | 'node', 53 | ['./bin/find', '--names', '--json', '--tagged', '@alpha'], 54 | { 55 | filter: ['code', 'stdout'], 56 | }, 57 | ) 58 | // console.log(result) 59 | t.snapshot(result) 60 | }) 61 | -------------------------------------------------------------------------------- /test/npm/get-specs.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getSpecs } = require('../..') 3 | const { toRelative } = require('../../src/files') 4 | const sinon = require('sinon') 5 | const path = require('path') 6 | 7 | test('finds the specs', (t) => { 8 | t.plan(1) 9 | const specs = getSpecs() 10 | t.deepEqual(specs, [ 11 | 'cypress/e2e/spec-b.cy.js', 12 | 'cypress/e2e/spec.cy.js', 13 | 'cypress/e2e/featureA/user.cy.ts', 14 | ]) 15 | }) 16 | 17 | test('finds the specs with custom spec pattern', (t) => { 18 | t.plan(1) 19 | const specs = getSpecs({ 20 | e2e: { 21 | specPattern: '*/e2e/featureA/*.cy.ts', 22 | }, 23 | }) 24 | t.deepEqual(specs, ['cypress/e2e/featureA/user.cy.ts']) 25 | }) 26 | 27 | test('returns an empty list for unknown spec type', (t) => { 28 | t.plan(1) 29 | const specs = getSpecs({}, 'api') 30 | t.deepEqual(specs, []) 31 | }) 32 | 33 | test('returns a list of component specs', (t) => { 34 | t.plan(1) 35 | const specs = getSpecs(undefined, 'component') 36 | t.deepEqual(specs, [ 37 | 'test-components/comp1.cy.js', 38 | 'test-components/comp2.cy.ts', 39 | ]) 40 | }) 41 | 42 | test('finds the specs passing resolved config (e2e)', (t) => { 43 | // imagine we are getting the config in the "e2e setupNodeEvents" 44 | const config = { 45 | specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', 46 | excludeSpecPattern: '*.hot-update.js', 47 | testingType: 'e2e', 48 | } 49 | t.plan(1) 50 | const specs = getSpecs(config) 51 | t.deepEqual(specs, [ 52 | 'cypress/e2e/spec-b.cy.js', 53 | 'cypress/e2e/spec.cy.js', 54 | 'cypress/e2e/featureA/user.cy.ts', 55 | ]) 56 | }) 57 | 58 | test('finds the specs passing resolved config (component)', (t) => { 59 | // imagine we are getting the config in the "component setupNodeEvents" 60 | const config = { 61 | specPattern: 'test-components/*.cy.{js,jsx,ts,tsx}', 62 | testingType: 'component', 63 | } 64 | t.plan(1) 65 | const specs = getSpecs(config) 66 | t.deepEqual(specs, [ 67 | 'test-components/comp1.cy.js', 68 | 'test-components/comp2.cy.ts', 69 | ]) 70 | }) 71 | 72 | test('supports list of specs in specPattern', (t) => { 73 | const specPattern = [ 74 | 'cypress/e2e/spec.cy.js', 75 | 'cypress/e2e/featureA/user.cy.ts', 76 | ] 77 | const config = { 78 | specPattern, 79 | testingType: 'e2e', 80 | } 81 | t.plan(1) 82 | const specs = getSpecs(config) 83 | t.deepEqual(specs, specPattern) 84 | }) 85 | 86 | test('supports wildcards in the list of specs', (t) => { 87 | const specPattern = [ 88 | 'cypress/e2e/spec*.cy.js', 89 | 'cypress/e2e/featureA/user*.cy.ts', 90 | ] 91 | const config = { 92 | specPattern, 93 | testingType: 'e2e', 94 | } 95 | t.plan(1) 96 | const specs = getSpecs(config) 97 | t.deepEqual(specs, [ 98 | 'cypress/e2e/spec-b.cy.js', 99 | 'cypress/e2e/spec.cy.js', 100 | 'cypress/e2e/featureA/user.cy.ts', 101 | ]) 102 | }) 103 | 104 | test('returns absolute filenames', (t) => { 105 | const specPattern = [ 106 | 'cypress/e2e/spec*.cy.js', 107 | 'cypress/e2e/featureA/user*.cy.ts', 108 | ] 109 | const config = { 110 | specPattern, 111 | testingType: 'e2e', 112 | } 113 | t.plan(2) 114 | const specs = getSpecs(config, 'e2e', true) 115 | const cwd = process.cwd() 116 | t.truthy(specs.every((filename) => filename.startsWith(cwd))) 117 | 118 | const relativeSpecs = toRelative(specs) 119 | t.deepEqual(relativeSpecs, [ 120 | 'cypress/e2e/spec-b.cy.js', 121 | 'cypress/e2e/spec.cy.js', 122 | 'cypress/e2e/featureA/user.cy.ts', 123 | ]) 124 | }) 125 | 126 | test('uses project root', (t) => { 127 | const projectRoot = process.cwd() 128 | console.log('project root', projectRoot) 129 | sinon.stub(process, 'cwd').returns(path.join(projectRoot, 'mocks/my-app')) 130 | 131 | const config = { 132 | projectRoot, 133 | e2e: { 134 | specPattern: 'mocks/my-app/e2e/e2e-tests/*.cy.js', 135 | }, 136 | } 137 | t.plan(1) 138 | const specs = getSpecs(config) 139 | t.deepEqual(specs, ['mocks/my-app/e2e/e2e-tests/spec-a.cy.js']) 140 | }) 141 | -------------------------------------------------------------------------------- /test/npm/get-tests.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { getTests } = require('../..') 3 | 4 | test('finds the specs and the tests', (t) => { 5 | t.plan(2) 6 | const { jsonResults, tagTestCounts } = getTests() 7 | t.deepEqual(tagTestCounts, {}, 'no tag counts') 8 | t.snapshot(jsonResults, 'json object with tests') 9 | }) 10 | -------------------------------------------------------------------------------- /test/npm/snapshots/get-tests.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/npm/get-tests.js` 2 | 3 | The actual snapshot is saved in `get-tests.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## finds the specs and the tests 8 | 9 | > json object with tests 10 | 11 | { 12 | 'cypress/e2e/featureA/user.cy.ts': { 13 | counts: { 14 | pending: 1, 15 | tests: 2, 16 | }, 17 | tests: [ 18 | { 19 | name: 'works', 20 | requiredTags: undefined, 21 | tags: [ 22 | '@user', 23 | ], 24 | type: 'test', 25 | }, 26 | { 27 | name: 'needs to be written', 28 | pending: true, 29 | requiredTags: undefined, 30 | tags: [ 31 | '@alpha', 32 | ], 33 | type: 'test', 34 | }, 35 | ], 36 | }, 37 | 'cypress/e2e/spec-b.cy.js': { 38 | counts: { 39 | pending: 0, 40 | tests: 1, 41 | }, 42 | tests: [ 43 | { 44 | name: 'works in spec B', 45 | requiredTags: undefined, 46 | tags: undefined, 47 | type: 'test', 48 | }, 49 | ], 50 | }, 51 | 'cypress/e2e/spec.cy.js': { 52 | counts: { 53 | pending: 0, 54 | tests: 3, 55 | }, 56 | tests: [ 57 | { 58 | name: 'parent suite', 59 | requiredTags: undefined, 60 | suites: [ 61 | { 62 | name: 'inner suite', 63 | requiredTags: undefined, 64 | tags: undefined, 65 | tests: [ 66 | { 67 | name: 'shows something!', 68 | requiredTags: undefined, 69 | tags: [ 70 | '@user', 71 | ], 72 | type: 'test', 73 | }, 74 | ], 75 | type: 'suite', 76 | }, 77 | ], 78 | tags: [ 79 | '@main', 80 | ], 81 | tests: [ 82 | { 83 | name: 'works well enough', 84 | requiredTags: undefined, 85 | tags: undefined, 86 | type: 'test', 87 | }, 88 | ], 89 | type: 'suite', 90 | }, 91 | { 92 | name: 'empty parent suite', 93 | requiredTags: undefined, 94 | suites: [ 95 | { 96 | name: 'inner suite', 97 | requiredTags: undefined, 98 | tags: undefined, 99 | tests: [ 100 | { 101 | name: 'shows something!', 102 | requiredTags: undefined, 103 | tags: undefined, 104 | type: 'test', 105 | }, 106 | ], 107 | type: 'suite', 108 | }, 109 | ], 110 | tags: undefined, 111 | type: 'suite', 112 | }, 113 | ], 114 | }, 115 | } 116 | -------------------------------------------------------------------------------- /test/npm/snapshots/get-tests.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/find-cypress-specs/c76be1f986e9cdac2c7384ec8fe7745a9ad333a6/test/npm/snapshots/get-tests.js.snap -------------------------------------------------------------------------------- /test/print.js: -------------------------------------------------------------------------------- 1 | const { 2 | stringFileTests, 3 | stringAllInfo, 4 | stringMarkdownTests, 5 | } = require('../src/print') 6 | const test = require('ava') 7 | const input = require('./tagged.json') 8 | 9 | test('prints tests from one file', (t) => { 10 | t.plan(2) 11 | const filename = 'cypress/e2e/featureA/user.js' 12 | const fileInfo = input[filename] 13 | const copy = JSON.parse(JSON.stringify(fileInfo)) 14 | const str = stringFileTests(filename, copy) 15 | 16 | // does not change the input 17 | t.deepEqual(fileInfo, copy) 18 | // console.log(str) 19 | t.snapshot(str) 20 | }) 21 | 22 | test('prints tests with inner suites', (t) => { 23 | t.plan(2) 24 | const filename = 'cypress/e2e/spec.js' 25 | const fileInfo = input[filename] 26 | const copy = JSON.parse(JSON.stringify(fileInfo)) 27 | const str = stringFileTests(filename, copy) 28 | 29 | // does not change the input 30 | t.deepEqual(fileInfo, copy) 31 | // console.log(str) 32 | t.snapshot(str) 33 | }) 34 | 35 | test('prints all tests', (t) => { 36 | t.plan(2) 37 | const copy = JSON.parse(JSON.stringify(input)) 38 | const str = stringAllInfo(copy) 39 | 40 | // does not change the input 41 | t.deepEqual(input, copy) 42 | // console.log(str) 43 | t.snapshot(str) 44 | }) 45 | 46 | test('prints markdown', (t) => { 47 | t.plan(2) 48 | const copy = JSON.parse(JSON.stringify(input)) 49 | const str = stringMarkdownTests(copy) 50 | 51 | // does not change the input 52 | t.deepEqual(input, copy) 53 | // console.log(str) 54 | t.snapshot(str) 55 | }) 56 | -------------------------------------------------------------------------------- /test/snapshots/branch-tagged.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/branch-tagged.js` 2 | 3 | The actual snapshot is saved in `branch-tagged.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## prints changed spec files that have the given tag 8 | 9 | > Snapshot 1 10 | 11 | ` code: 0␊ 12 | stdout:␊ 13 | -------␊ 14 | cypress/e2e/featureA/user.cy.ts␊ 15 | -------␊ 16 | ` 17 | 18 | ## prints number of changed spec files that have the given tag 19 | 20 | > Snapshot 1 21 | 22 | ` code: 0␊ 23 | stdout:␊ 24 | -------␊ 25 | 1␊ 26 | -------␊ 27 | ` 28 | 29 | ## no changed specs tagged with the tag 30 | 31 | > Snapshot 1 32 | 33 | ` code: 0␊ 34 | stdout:␊ 35 | -------␊ 36 | ␊ 37 | -------␊ 38 | ` 39 | 40 | ## zero changed specs tagged with the tag 41 | 42 | > Snapshot 1 43 | 44 | ` code: 0␊ 45 | stdout:␊ 46 | -------␊ 47 | 0␊ 48 | -------␊ 49 | ` 50 | -------------------------------------------------------------------------------- /test/snapshots/branch-tagged.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/find-cypress-specs/c76be1f986e9cdac2c7384ec8fe7745a9ad333a6/test/snapshots/branch-tagged.js.snap -------------------------------------------------------------------------------- /test/snapshots/branch.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/branch.js` 2 | 3 | The actual snapshot is saved in `branch.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## prints changed spec files against another branch 8 | 9 | > Snapshot 1 10 | 11 | ` code: 0␊ 12 | stdout:␊ 13 | -------␊ 14 | cypress/e2e/featureA/user.cy.ts␊ 15 | -------␊ 16 | ` 17 | 18 | ## prints changed spec files against the parent commit 19 | 20 | > Snapshot 1 21 | 22 | ` code: 0␊ 23 | stdout:␊ 24 | -------␊ 25 | cypress/e2e/spec.cy.js␊ 26 | -------␊ 27 | ` 28 | 29 | ## prints count of changed spec files against the parent commit 30 | 31 | > Snapshot 1 32 | 33 | ` code: 0␊ 34 | stdout:␊ 35 | -------␊ 36 | 2␊ 37 | -------␊ 38 | ` 39 | 40 | ## changed spec files that change because an imported file changed 41 | 42 | > Snapshot 1 43 | 44 | ` code: 0␊ 45 | stdout:␊ 46 | -------␊ 47 | cypress/e2e/spec.cy.js␊ 48 | -------␊ 49 | ` 50 | -------------------------------------------------------------------------------- /test/snapshots/branch.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/find-cypress-specs/c76be1f986e9cdac2c7384ec8fe7745a9ad333a6/test/snapshots/branch.js.snap -------------------------------------------------------------------------------- /test/snapshots/cli.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/cli.js` 2 | 3 | The actual snapshot is saved in `cli.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## prints tags 8 | 9 | > Snapshot 1 10 | 11 | ` code: 0␊ 12 | stdout:␊ 13 | -------␊ 14 | Tag Tests␊ 15 | ------ -----␊ 16 | @alpha 1 ␊ 17 | @main 2 ␊ 18 | @user 2␊ 19 | -------␊ 20 | ` 21 | 22 | ## prints test names 23 | 24 | > Snapshot 1 25 | 26 | ` code: 0␊ 27 | stdout:␊ 28 | -------␊ 29 | cypress/e2e/spec-b.cy.js (1 test)␊ 30 | └─ works in spec B␊ 31 | ␊ 32 | cypress/e2e/spec.cy.js (3 tests)␊ 33 | ├─ parent suite [@main]␊ 34 | │ ├─ works well enough␊ 35 | │ └─ inner suite␊ 36 | │ └─ shows something! [@user]␊ 37 | └─ empty parent suite␊ 38 | └─ inner suite␊ 39 | └─ shows something!␊ 40 | ␊ 41 | cypress/e2e/featureA/user.cy.ts (2 tests, 1 pending)␊ 42 | ├─ works [@user]␊ 43 | └⊙ needs to be written [@alpha]␊ 44 | ␊ 45 | found 3 specs (6 tests, 1 pending)␊ 46 | -------␊ 47 | ` 48 | 49 | ## prints test tagged @alpha 50 | 51 | > Snapshot 1 52 | 53 | ` code: 0␊ 54 | stdout:␊ 55 | -------␊ 56 | cypress/e2e/featureA/user.cy.ts (1 test, 1 pending)␊ 57 | └⊙ needs to be written [@alpha]␊ 58 | ␊ 59 | found 1 spec (1 test, 1 pending)␊ 60 | -------␊ 61 | ` 62 | 63 | ## prints test tagged @user 64 | 65 | > Snapshot 1 66 | 67 | ` code: 0␊ 68 | stdout:␊ 69 | -------␊ 70 | cypress/e2e/spec.cy.js (1 test)␊ 71 | └─ parent suite [@main]␊ 72 | └─ inner suite␊ 73 | └─ shows something! [@user]␊ 74 | ␊ 75 | cypress/e2e/featureA/user.cy.ts (1 test)␊ 76 | └─ works [@user]␊ 77 | ␊ 78 | found 2 specs (2 tests)␊ 79 | -------␊ 80 | ` 81 | 82 | ## prints test tagged @main 83 | 84 | > Snapshot 1 85 | 86 | ` code: 0␊ 87 | stdout:␊ 88 | -------␊ 89 | cypress/e2e/spec.cy.js (2 tests)␊ 90 | └─ parent suite [@main]␊ 91 | ├─ works well enough␊ 92 | └─ inner suite␊ 93 | └─ shows something! [@user]␊ 94 | ␊ 95 | found 1 spec (2 tests)␊ 96 | -------␊ 97 | ` 98 | 99 | ## prints test tagged @main and @alpha 100 | 101 | > Snapshot 1 102 | 103 | ` code: 0␊ 104 | stdout:␊ 105 | -------␊ 106 | cypress/e2e/spec.cy.js (2 tests)␊ 107 | └─ parent suite [@main]␊ 108 | ├─ works well enough␊ 109 | └─ inner suite␊ 110 | └─ shows something! [@user]␊ 111 | ␊ 112 | cypress/e2e/featureA/user.cy.ts (1 test, 1 pending)␊ 113 | └⊙ needs to be written [@alpha]␊ 114 | ␊ 115 | found 2 specs (3 tests, 1 pending)␊ 116 | -------␊ 117 | ` 118 | 119 | ## prints only the skipped tests 120 | 121 | > Snapshot 1 122 | 123 | ` code: 0␊ 124 | stdout:␊ 125 | -------␊ 126 | cypress/e2e/featureA/user.cy.ts (1 test, 1 pending)␊ 127 | └⊙ needs to be written [@alpha]␊ 128 | ␊ 129 | found 1 spec (1 test, 1 pending)␊ 130 | -------␊ 131 | ` 132 | 133 | ## prints only the skipped test count 134 | 135 | > Snapshot 1 136 | 137 | ` code: 0␊ 138 | stdout:␊ 139 | -------␊ 140 | 1␊ 141 | -------␊ 142 | ` 143 | 144 | ## --pending is an alias to --skipped 145 | 146 | > Snapshot 1 147 | 148 | ` code: 0␊ 149 | stdout:␊ 150 | -------␊ 151 | cypress/e2e/featureA/user.cy.ts (1 test, 1 pending)␊ 152 | └⊙ needs to be written [@alpha]␊ 153 | ␊ 154 | found 1 spec (1 test, 1 pending)␊ 155 | -------␊ 156 | ` 157 | 158 | ## jsx components 159 | 160 | > Snapshot 1 161 | 162 | ` code: 0␊ 163 | stdout:␊ 164 | -------␊ 165 | test-components/comp1.cy.js (1 test)␊ 166 | └─ Counter␊ 167 | └─ works␊ 168 | ␊ 169 | test-components/comp2.cy.ts (1 test)␊ 170 | └─ mock component suite 2␊ 171 | └─ works␊ 172 | ␊ 173 | found 2 specs (2 tests)␊ 174 | -------␊ 175 | ` 176 | 177 | ## exclusive tests 178 | 179 | > Snapshot 1 180 | 181 | ` code: 0␊ 182 | stdout:␊ 183 | -------␊ 184 | cypress/integration/spec1.js (2 tests)␊ 185 | ├> runs by itself␊ 186 | └─ another test␊ 187 | ␊ 188 | found 1 spec (2 tests)␊ 189 | -------␊ 190 | ` 191 | 192 | ## prints test file names --tagged @alpha 193 | 194 | > Snapshot 1 195 | 196 | ` code: 0␊ 197 | stdout:␊ 198 | -------␊ 199 | cypress/e2e/featureA/user.cy.ts␊ 200 | -------␊ 201 | ` 202 | 203 | ## prints test file names --tagged @alpha,@main,@user 204 | 205 | > Snapshot 1 206 | 207 | ` code: 0␊ 208 | stdout:␊ 209 | -------␊ 210 | cypress/e2e/featureA/user.cy.ts␊ 211 | -------␊ 212 | ` 213 | 214 | ## prints test file names --tagged empty string 215 | 216 | > Snapshot 1 217 | 218 | ` code: 0␊ 219 | stdout:␊ 220 | -------␊ 221 | ␊ 222 | -------␊ 223 | ` 224 | 225 | ## prints the count of specs 226 | 227 | > Snapshot 1 228 | 229 | ` code: 0␊ 230 | stdout:␊ 231 | -------␊ 232 | 3␊ 233 | -------␊ 234 | ` 235 | 236 | ## prints the count of test files --tagged @alpha with --count 237 | 238 | > Snapshot 1 239 | 240 | ` code: 0␊ 241 | stdout:␊ 242 | -------␊ 243 | 1␊ 244 | -------␊ 245 | ` 246 | 247 | ## prints the number of E2E and component tests 248 | 249 | > Snapshot 1 250 | 251 | ` code: 0␊ 252 | stdout:␊ 253 | -------␊ 254 | 6 e2e tests, 2 component tests␊ 255 | -------␊ 256 | ` 257 | 258 | ## prints tags and required tags 259 | 260 | > Snapshot 1 261 | 262 | ` code: 0␊ 263 | stdout:␊ 264 | -------␊ 265 | cypress/integration/spec1.js (2 tests)␊ 266 | ├─ works [user] [[@foo]]␊ 267 | └─ does not work [[@foo, @bar]]␊ 268 | ␊ 269 | cypress/integration/spec2.js (1 test)␊ 270 | └─ spec 2 works [user] [[@foo]]␊ 271 | ␊ 272 | cypress/integration/spec3.js (2 tests)␊ 273 | ├─ parent1 [[@parent1]]␊ 274 | │ └─ child1␊ 275 | └─ parent2 [@parent2]␊ 276 | └─ child2␊ 277 | ␊ 278 | found 3 specs (5 tests)␊ 279 | -------␊ 280 | ` 281 | 282 | ## filters by required tag 283 | 284 | > Snapshot 1 285 | 286 | ` code: 0␊ 287 | stdout:␊ 288 | -------␊ 289 | cypress/integration/spec1.js (1 test)␊ 290 | └─ does not work [[@foo, @bar]]␊ 291 | ␊ 292 | found 1 spec (1 test)␊ 293 | -------␊ 294 | ` 295 | 296 | ## counts by required tag 297 | 298 | > Snapshot 1 299 | 300 | ` code: 0␊ 301 | stdout:␊ 302 | -------␊ 303 | 1␊ 304 | -------␊ 305 | ` 306 | 307 | ## counts all tags including required 308 | 309 | > Snapshot 1 310 | 311 | ` code: 0␊ 312 | stdout:␊ 313 | -------␊ 314 | Tag Tests␊ 315 | -------- -----␊ 316 | @bar 1 ␊ 317 | @foo 3 ␊ 318 | @parent1 1 ␊ 319 | @parent2 1 ␊ 320 | user 2␊ 321 | -------␊ 322 | ` 323 | 324 | ## skips specs in the node_modules automatically 325 | 326 | > Snapshot 1 327 | 328 | ` code: 0␊ 329 | stdout:␊ 330 | -------␊ 331 | cypress/e2e/spec1.cy.js␊ 332 | -------␊ 333 | ` 334 | 335 | ## finds tests with BOTH tags using AND syntax 336 | 337 | > Snapshot 1 338 | 339 | ` code: 0␊ 340 | stdout:␊ 341 | -------␊ 342 | cypress/integration/spec1.js (1 test)␊ 343 | └─ does not work [[@foo, @bar]]␊ 344 | ␊ 345 | found 1 spec (1 test)␊ 346 | -------␊ 347 | ` 348 | 349 | ## applies AND syntax to the effective tags 350 | 351 | > Snapshot 1 352 | 353 | ` code: 0␊ 354 | stdout:␊ 355 | -------␊ 356 | cypress/integration/spec1.js (1 test)␊ 357 | └─ user tests [@user]␊ 358 | └─ loads [@sanity]␊ 359 | ␊ 360 | cypress/integration/spec2.js (1 test)␊ 361 | └─ user home tests␊ 362 | └─ loads [@user, @sanity]␊ 363 | ␊ 364 | found 2 specs (2 tests)␊ 365 | -------␊ 366 | ` 367 | -------------------------------------------------------------------------------- /test/snapshots/cli.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/find-cypress-specs/c76be1f986e9cdac2c7384ec8fe7745a9ad333a6/test/snapshots/cli.js.snap -------------------------------------------------------------------------------- /test/snapshots/json.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/json.js` 2 | 3 | The actual snapshot is saved in `json.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## prints test names in json 8 | 9 | > Snapshot 1 10 | 11 | ` code: 0␊ 12 | stdout:␊ 13 | -------␊ 14 | {␊ 15 | "cypress/e2e/spec-b.cy.js": {␊ 16 | "counts": {␊ 17 | "tests": 1,␊ 18 | "pending": 0␊ 19 | },␊ 20 | "tests": [␊ 21 | {␊ 22 | "name": "works in spec B",␊ 23 | "type": "test"␊ 24 | }␊ 25 | ]␊ 26 | },␊ 27 | "cypress/e2e/spec.cy.js": {␊ 28 | "counts": {␊ 29 | "tests": 3,␊ 30 | "pending": 0␊ 31 | },␊ 32 | "tests": [␊ 33 | {␊ 34 | "name": "parent suite",␊ 35 | "type": "suite",␊ 36 | "tags": [␊ 37 | "@main"␊ 38 | ],␊ 39 | "suites": [␊ 40 | {␊ 41 | "name": "inner suite",␊ 42 | "type": "suite",␊ 43 | "tests": [␊ 44 | {␊ 45 | "name": "shows something!",␊ 46 | "type": "test",␊ 47 | "tags": [␊ 48 | "@user"␊ 49 | ]␊ 50 | }␊ 51 | ]␊ 52 | }␊ 53 | ],␊ 54 | "tests": [␊ 55 | {␊ 56 | "name": "works well enough",␊ 57 | "type": "test"␊ 58 | }␊ 59 | ]␊ 60 | },␊ 61 | {␊ 62 | "name": "empty parent suite",␊ 63 | "type": "suite",␊ 64 | "suites": [␊ 65 | {␊ 66 | "name": "inner suite",␊ 67 | "type": "suite",␊ 68 | "tests": [␊ 69 | {␊ 70 | "name": "shows something!",␊ 71 | "type": "test"␊ 72 | }␊ 73 | ]␊ 74 | }␊ 75 | ]␊ 76 | }␊ 77 | ]␊ 78 | },␊ 79 | "cypress/e2e/featureA/user.cy.ts": {␊ 80 | "counts": {␊ 81 | "tests": 2,␊ 82 | "pending": 1␊ 83 | },␊ 84 | "tests": [␊ 85 | {␊ 86 | "name": "works",␊ 87 | "type": "test",␊ 88 | "tags": [␊ 89 | "@user"␊ 90 | ]␊ 91 | },␊ 92 | {␊ 93 | "name": "needs to be written",␊ 94 | "type": "test",␊ 95 | "tags": [␊ 96 | "@alpha"␊ 97 | ],␊ 98 | "pending": true␊ 99 | }␊ 100 | ]␊ 101 | }␊ 102 | }␊ 103 | -------␊ 104 | ` 105 | 106 | ## prints test names in json using -j 107 | 108 | > Snapshot 1 109 | 110 | ` code: 0␊ 111 | stdout:␊ 112 | -------␊ 113 | {␊ 114 | "cypress/e2e/spec-b.cy.js": {␊ 115 | "counts": {␊ 116 | "tests": 1,␊ 117 | "pending": 0␊ 118 | },␊ 119 | "tests": [␊ 120 | {␊ 121 | "name": "works in spec B",␊ 122 | "type": "test"␊ 123 | }␊ 124 | ]␊ 125 | },␊ 126 | "cypress/e2e/spec.cy.js": {␊ 127 | "counts": {␊ 128 | "tests": 3,␊ 129 | "pending": 0␊ 130 | },␊ 131 | "tests": [␊ 132 | {␊ 133 | "name": "parent suite",␊ 134 | "type": "suite",␊ 135 | "tags": [␊ 136 | "@main"␊ 137 | ],␊ 138 | "suites": [␊ 139 | {␊ 140 | "name": "inner suite",␊ 141 | "type": "suite",␊ 142 | "tests": [␊ 143 | {␊ 144 | "name": "shows something!",␊ 145 | "type": "test",␊ 146 | "tags": [␊ 147 | "@user"␊ 148 | ]␊ 149 | }␊ 150 | ]␊ 151 | }␊ 152 | ],␊ 153 | "tests": [␊ 154 | {␊ 155 | "name": "works well enough",␊ 156 | "type": "test"␊ 157 | }␊ 158 | ]␊ 159 | },␊ 160 | {␊ 161 | "name": "empty parent suite",␊ 162 | "type": "suite",␊ 163 | "suites": [␊ 164 | {␊ 165 | "name": "inner suite",␊ 166 | "type": "suite",␊ 167 | "tests": [␊ 168 | {␊ 169 | "name": "shows something!",␊ 170 | "type": "test"␊ 171 | }␊ 172 | ]␊ 173 | }␊ 174 | ]␊ 175 | }␊ 176 | ]␊ 177 | },␊ 178 | "cypress/e2e/featureA/user.cy.ts": {␊ 179 | "counts": {␊ 180 | "tests": 2,␊ 181 | "pending": 1␊ 182 | },␊ 183 | "tests": [␊ 184 | {␊ 185 | "name": "works",␊ 186 | "type": "test",␊ 187 | "tags": [␊ 188 | "@user"␊ 189 | ]␊ 190 | },␊ 191 | {␊ 192 | "name": "needs to be written",␊ 193 | "type": "test",␊ 194 | "tags": [␊ 195 | "@alpha"␊ 196 | ],␊ 197 | "pending": true␊ 198 | }␊ 199 | ]␊ 200 | }␊ 201 | }␊ 202 | -------␊ 203 | ` 204 | 205 | ## prints tags in json 206 | 207 | > Snapshot 1 208 | 209 | ` code: 0␊ 210 | stdout:␊ 211 | -------␊ 212 | {␊ 213 | "@alpha": 1,␊ 214 | "@main": 2,␊ 215 | "@user": 2␊ 216 | }␊ 217 | -------␊ 218 | ` 219 | 220 | ## prints tags in json using -j 221 | 222 | > Snapshot 1 223 | 224 | ` code: 0␊ 225 | stdout:␊ 226 | -------␊ 227 | {␊ 228 | "@alpha": 1,␊ 229 | "@main": 2,␊ 230 | "@user": 2␊ 231 | }␊ 232 | -------␊ 233 | ` 234 | 235 | ## prints tests tagged with @user in json 236 | 237 | > Snapshot 1 238 | 239 | ` code: 0␊ 240 | stdout:␊ 241 | -------␊ 242 | {␊ 243 | "cypress/e2e/spec.cy.js": {␊ 244 | "counts": {␊ 245 | "tests": 1,␊ 246 | "pending": 0␊ 247 | },␊ 248 | "tests": [␊ 249 | {␊ 250 | "name": "parent suite",␊ 251 | "type": "suite",␊ 252 | "tags": [␊ 253 | "@main"␊ 254 | ],␊ 255 | "suites": [␊ 256 | {␊ 257 | "name": "inner suite",␊ 258 | "type": "suite",␊ 259 | "tests": [␊ 260 | {␊ 261 | "name": "shows something!",␊ 262 | "type": "test",␊ 263 | "tags": [␊ 264 | "@user"␊ 265 | ]␊ 266 | }␊ 267 | ]␊ 268 | }␊ 269 | ],␊ 270 | "tests": []␊ 271 | }␊ 272 | ]␊ 273 | },␊ 274 | "cypress/e2e/featureA/user.cy.ts": {␊ 275 | "counts": {␊ 276 | "tests": 1,␊ 277 | "pending": 0␊ 278 | },␊ 279 | "tests": [␊ 280 | {␊ 281 | "name": "works",␊ 282 | "type": "test",␊ 283 | "tags": [␊ 284 | "@user"␊ 285 | ]␊ 286 | }␊ 287 | ]␊ 288 | }␊ 289 | }␊ 290 | -------␊ 291 | ` 292 | 293 | ## prints tests tagged with @alpha in json 294 | 295 | > Snapshot 1 296 | 297 | ` code: 0␊ 298 | stdout:␊ 299 | -------␊ 300 | {␊ 301 | "cypress/e2e/featureA/user.cy.ts": {␊ 302 | "counts": {␊ 303 | "tests": 1,␊ 304 | "pending": 1␊ 305 | },␊ 306 | "tests": [␊ 307 | {␊ 308 | "name": "needs to be written",␊ 309 | "type": "test",␊ 310 | "tags": [␊ 311 | "@alpha"␊ 312 | ],␊ 313 | "pending": true␊ 314 | }␊ 315 | ]␊ 316 | }␊ 317 | }␊ 318 | -------␊ 319 | ` 320 | -------------------------------------------------------------------------------- /test/snapshots/json.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/find-cypress-specs/c76be1f986e9cdac2c7384ec8fe7745a9ad333a6/test/snapshots/json.js.snap -------------------------------------------------------------------------------- /test/snapshots/print.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/print.js` 2 | 3 | The actual snapshot is saved in `print.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## prints tests from one file 8 | 9 | > Snapshot 1 10 | 11 | `cypress/e2e/featureA/user.js (2 tests, 1 pending)␊ 12 | ├─ works [@user]␊ 13 | └⊙ needs to be written [@alpha]␊ 14 | ` 15 | 16 | ## prints tests with inner suites 17 | 18 | > Snapshot 1 19 | 20 | `cypress/e2e/spec.js (2 tests)␊ 21 | ├─ parent suite [@main]␊ 22 | │ ├─ works well enough␊ 23 | │ └─ inner suite␊ 24 | │ └─ shows something! [@user]␊ 25 | └─ empty parent suite␊ 26 | └─ inner suite␊ 27 | └─ shows something!␊ 28 | ` 29 | 30 | ## prints all tests 31 | 32 | > Snapshot 1 33 | 34 | `cypress/e2e/spec.js (2 tests)␊ 35 | ├─ parent suite [@main]␊ 36 | │ ├─ works well enough␊ 37 | │ └─ inner suite␊ 38 | │ └─ shows something! [@user]␊ 39 | └─ empty parent suite␊ 40 | └─ inner suite␊ 41 | └─ shows something!␊ 42 | ␊ 43 | cypress/e2e/featureA/user.js (2 tests, 1 pending)␊ 44 | ├─ works [@user]␊ 45 | └⊙ needs to be written [@alpha]␊ 46 | ␊ 47 | found 2 specs (4 tests, 1 pending)` 48 | 49 | ## prints markdown 50 | 51 | > Snapshot 1 52 | 53 | `| 2 Specs with 4 tests | Tags |␊ 54 | | --- | --- |␊ 55 | | **\`cypress/e2e/spec.js\`** (2 tests) ||␊ 56 | | \`parent suite / works well enough\` | |␊ 57 | | \`parent suite / inner suite / shows something!\` | @user |␊ 58 | | \`empty parent suite / inner suite / shows something!\` | |␊ 59 | | **\`cypress/e2e/featureA/user.js\`** (2 tests) ||␊ 60 | | \`works\` | @user |␊ 61 | | \`needs to be written\` | @alpha |` 62 | -------------------------------------------------------------------------------- /test/snapshots/print.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/find-cypress-specs/c76be1f986e9cdac2c7384ec8fe7745a9ad333a6/test/snapshots/print.js.snap -------------------------------------------------------------------------------- /test/tagged.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const input = require('./tagged.json') 3 | const { 4 | pickTaggedTests, 5 | pickTaggedTestsFrom, 6 | preprocessAndTags, 7 | doTagsMatch, 8 | } = require('../src/tagged') 9 | 10 | test('filters a single array of tests', (t) => { 11 | const tests = [ 12 | { 13 | name: 'works', 14 | type: 'test', 15 | tags: ['@user'], 16 | }, 17 | { 18 | name: 'needs to be written', 19 | type: 'test', 20 | tags: ['@alpha'], 21 | pending: true, 22 | }, 23 | ] 24 | pickTaggedTests(tests, '@user') 25 | t.deepEqual(tests, [ 26 | { 27 | name: 'works', 28 | type: 'test', 29 | tags: ['@user'], 30 | }, 31 | ]) 32 | }) 33 | 34 | test('filters a single array of tests using multiple tags', (t) => { 35 | const tests = [ 36 | { 37 | name: 'works', 38 | type: 'test', 39 | tags: ['@user'], 40 | }, 41 | { 42 | name: 'needs to be written', 43 | type: 'test', 44 | tags: ['@alpha'], 45 | pending: true, 46 | }, 47 | ] 48 | const copy = JSON.parse(JSON.stringify(tests)) 49 | pickTaggedTests(tests, ['@user', '@alpha']) 50 | // all tests were picked by one or the other tag 51 | t.deepEqual(tests, copy) 52 | }) 53 | 54 | test('finds no tests with the given tag', (t) => { 55 | const tests = [ 56 | { 57 | name: 'works', 58 | type: 'test', 59 | tags: ['@user'], 60 | }, 61 | { 62 | name: 'needs to be written', 63 | type: 'test', 64 | tags: ['@alpha'], 65 | pending: true, 66 | }, 67 | ] 68 | pickTaggedTests(tests, '@foo') 69 | t.deepEqual(tests, []) 70 | }) 71 | 72 | test('filters a single array of tests using some tags', (t) => { 73 | const tests = [ 74 | { 75 | name: 'works', 76 | type: 'test', 77 | tags: ['@user'], 78 | }, 79 | { 80 | name: 'needs to be written', 81 | type: 'test', 82 | tags: ['@alpha'], 83 | pending: true, 84 | }, 85 | ] 86 | pickTaggedTests(tests, ['@not-found', '@alpha']) 87 | t.deepEqual(tests, [ 88 | { 89 | name: 'needs to be written', 90 | type: 'test', 91 | tags: ['@alpha'], 92 | pending: true, 93 | }, 94 | ]) 95 | }) 96 | 97 | test('filters tests tagged @alpha', (t) => { 98 | t.plan(2) 99 | const json = JSON.parse(JSON.stringify(input)) 100 | const result = pickTaggedTestsFrom(json, '@alpha') 101 | // modifies the object in place 102 | t.true(result === json) 103 | 104 | // all tests but one were eliminated 105 | const expected = { 106 | 'cypress/e2e/featureA/user.js': { 107 | counts: { tests: 1, pending: 1 }, 108 | tests: [ 109 | { 110 | name: 'needs to be written', 111 | type: 'test', 112 | tags: ['@alpha'], 113 | pending: true, 114 | }, 115 | ], 116 | }, 117 | } 118 | // console.dir(result, { depth: null }) 119 | t.deepEqual(result, expected) 120 | }) 121 | 122 | test('filters deep tests tagged @user', (t) => { 123 | t.plan(1) 124 | const json = JSON.parse(JSON.stringify(input)) 125 | const result = pickTaggedTestsFrom(json, '@user') 126 | const expected = { 127 | 'cypress/e2e/spec.js': { 128 | counts: { tests: 1, pending: 0 }, 129 | tests: [ 130 | { 131 | name: 'parent suite', 132 | type: 'suite', 133 | tags: ['@main'], 134 | suites: [ 135 | { 136 | name: 'inner suite', 137 | type: 'suite', 138 | tests: [ 139 | { 140 | name: 'shows something!', 141 | type: 'test', 142 | tags: ['@user'], 143 | }, 144 | ], 145 | }, 146 | ], 147 | tests: [], 148 | }, 149 | ], 150 | }, 151 | 'cypress/e2e/featureA/user.js': { 152 | counts: { tests: 1, pending: 0 }, 153 | tests: [{ name: 'works', type: 'test', tags: ['@user'] }], 154 | }, 155 | } 156 | // console.dir(result, { depth: null }) 157 | t.deepEqual(result, expected) 158 | }) 159 | 160 | test('applies tag from the suite to the tests', (t) => { 161 | t.plan(1) 162 | const json = JSON.parse(JSON.stringify(input)) 163 | const result = pickTaggedTestsFrom(json, '@main') 164 | const expected = { 165 | 'cypress/e2e/spec.js': { 166 | counts: { tests: 2, pending: 0 }, 167 | tests: [ 168 | { 169 | name: 'parent suite', 170 | type: 'suite', 171 | tags: ['@main'], 172 | suites: [ 173 | { 174 | name: 'inner suite', 175 | type: 'suite', 176 | tests: [ 177 | { 178 | name: 'shows something!', 179 | type: 'test', 180 | tags: ['@user'], 181 | }, 182 | ], 183 | }, 184 | ], 185 | tests: [{ name: 'works well enough', type: 'test' }], 186 | }, 187 | ], 188 | }, 189 | } 190 | // console.dir(result, { depth: null }) 191 | t.deepEqual(result, expected) 192 | }) 193 | 194 | test('applies multiple tags', (t) => { 195 | t.plan(1) 196 | const json = JSON.parse(JSON.stringify(input)) 197 | const result = pickTaggedTestsFrom(json, ['@main', '@user']) 198 | const expected = { 199 | 'cypress/e2e/spec.js': { 200 | counts: { 201 | tests: 2, 202 | pending: 0, 203 | }, 204 | tests: [ 205 | { 206 | name: 'parent suite', 207 | type: 'suite', 208 | tags: ['@main'], 209 | suites: [ 210 | { 211 | name: 'inner suite', 212 | type: 'suite', 213 | tests: [ 214 | { 215 | name: 'shows something!', 216 | type: 'test', 217 | tags: ['@user'], 218 | }, 219 | ], 220 | }, 221 | ], 222 | tests: [ 223 | { 224 | name: 'works well enough', 225 | type: 'test', 226 | }, 227 | ], 228 | }, 229 | ], 230 | }, 231 | 'cypress/e2e/featureA/user.js': { 232 | counts: { 233 | tests: 1, 234 | pending: 0, 235 | }, 236 | tests: [ 237 | { 238 | name: 'works', 239 | type: 'test', 240 | tags: ['@user'], 241 | }, 242 | ], 243 | }, 244 | } 245 | 246 | // console.dir(result, { depth: null }) 247 | t.deepEqual(result, expected) 248 | }) 249 | 250 | test('includes tests required by the parent required tag', (t) => { 251 | const json = { 252 | 'cypress/integration/spec1.js': { 253 | counts: { 254 | tests: 2, 255 | pending: 0, 256 | }, 257 | tests: [ 258 | { 259 | name: 'works', 260 | type: 'test', 261 | tags: ['user'], 262 | requiredTags: ['@foo'], 263 | }, 264 | { 265 | name: 'does not work', 266 | type: 'test', 267 | requiredTags: ['@foo', '@bar'], 268 | }, 269 | ], 270 | }, 271 | 'cypress/integration/spec2.js': { 272 | counts: { 273 | tests: 1, 274 | pending: 0, 275 | }, 276 | tests: [ 277 | { 278 | name: 'spec 2 works', 279 | type: 'test', 280 | tags: ['user'], 281 | requiredTags: ['@foo'], 282 | }, 283 | ], 284 | }, 285 | 'cypress/integration/spec3.js': { 286 | counts: { 287 | tests: 2, 288 | pending: 0, 289 | }, 290 | tests: [ 291 | { 292 | name: 'parent1', 293 | type: 'suite', 294 | requiredTags: ['@parent1'], 295 | tests: [ 296 | { 297 | name: 'child1', 298 | type: 'test', 299 | }, 300 | ], 301 | }, 302 | { 303 | name: 'parent2', 304 | type: 'suite', 305 | tags: ['@parent2'], 306 | tests: [ 307 | { 308 | name: 'child2', 309 | type: 'test', 310 | }, 311 | ], 312 | }, 313 | ], 314 | }, 315 | } 316 | const picked = pickTaggedTestsFrom(json, '@parent1') 317 | // console.log(JSON.stringify(picked, null, 2)) 318 | // only spec 3 has a test whose parent has the required tag "@parent1" 319 | t.deepEqual(picked, { 320 | 'cypress/integration/spec3.js': { 321 | counts: { 322 | tests: 1, 323 | pending: 0, 324 | }, 325 | tests: [ 326 | { 327 | name: 'parent1', 328 | type: 'suite', 329 | requiredTags: ['@parent1'], 330 | tests: [ 331 | { 332 | name: 'child1', 333 | type: 'test', 334 | }, 335 | ], 336 | }, 337 | ], 338 | }, 339 | }) 340 | }) 341 | 342 | test('preprocessAndTags', (t) => { 343 | const and = preprocessAndTags(['@user+@sanity', '@foo+@bar', '@user']) 344 | t.deepEqual(and, [['@user', '@sanity'], ['@foo', '@bar'], '@user']) 345 | }) 346 | 347 | test('filters tests using AND tags', (t) => { 348 | const tests = [ 349 | { 350 | name: 'works', 351 | type: 'test', 352 | tags: ['@user', '@sanity'], 353 | }, 354 | { 355 | name: 'needs to be written', 356 | type: 'test', 357 | tags: ['@user'], 358 | pending: true, 359 | }, 360 | ] 361 | pickTaggedTests(tests, '@user+@sanity') 362 | // only the first test has BOTH tags 363 | t.deepEqual(tests, [ 364 | { 365 | name: 'works', 366 | type: 'test', 367 | tags: ['@user', '@sanity'], 368 | }, 369 | ]) 370 | }) 371 | 372 | test('tags match with OR and AND', (t) => { 373 | const tags = ['@user', '@sanity', '@foo'] 374 | t.true( 375 | doTagsMatch(tags, ['@user+@sanity', '@bar']), 376 | 'one AND tag OR one other tag', 377 | ) 378 | t.true(doTagsMatch(tags, ['@user+@sanity']), 'one AND tag') 379 | t.false( 380 | doTagsMatch(tags, ['@user+@smoke']), 381 | 'second tag in AND does not apply', 382 | ) 383 | t.true(doTagsMatch(tags, ['@foo+@sanity']), 'AND tag order') 384 | }) 385 | 386 | test('filters tests using OR combination with AND tags', (t) => { 387 | const allTests = [ 388 | { 389 | name: 'test 1', 390 | type: 'test', 391 | tags: ['@user'], 392 | }, 393 | { 394 | name: 'test 2', 395 | type: 'test', 396 | tags: ['@user', '@sanity'], 397 | }, 398 | { 399 | name: 'test 3', 400 | type: 'test', 401 | tags: ['@sanity'], 402 | }, 403 | { 404 | name: 'test 4', 405 | type: 'test', 406 | tags: ['@regression'], 407 | }, 408 | ] 409 | const tests = structuredClone(allTests) 410 | pickTaggedTests(tests, ['@user+@sanity', '@regression']) 411 | t.deepEqual(tests, [allTests[1], allTests[3]]) 412 | }) 413 | -------------------------------------------------------------------------------- /test/tagged.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress/e2e/spec.js": { 3 | "counts": { 4 | "tests": 2, 5 | "pending": 0 6 | }, 7 | "tests": [ 8 | { 9 | "name": "parent suite", 10 | "type": "suite", 11 | "tags": ["@main"], 12 | "suites": [ 13 | { 14 | "name": "inner suite", 15 | "type": "suite", 16 | "tests": [ 17 | { 18 | "name": "shows something!", 19 | "type": "test", 20 | "tags": ["@user"] 21 | } 22 | ] 23 | } 24 | ], 25 | "tests": [ 26 | { 27 | "name": "works well enough", 28 | "type": "test" 29 | } 30 | ] 31 | }, 32 | { 33 | "name": "empty parent suite", 34 | "type": "suite", 35 | "suites": [ 36 | { 37 | "name": "inner suite", 38 | "type": "suite", 39 | "tests": [ 40 | { 41 | "name": "shows something!", 42 | "type": "test" 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | "cypress/e2e/featureA/user.js": { 51 | "counts": { 52 | "tests": 2, 53 | "pending": 1 54 | }, 55 | "tests": [ 56 | { 57 | "name": "works", 58 | "type": "test", 59 | "tags": ["@user"] 60 | }, 61 | { 62 | "name": "needs to be written", 63 | "type": "test", 64 | "tags": ["@alpha"], 65 | "pending": true 66 | } 67 | ] 68 | } 69 | } 70 | --------------------------------------------------------------------------------