├── .editorconfig ├── .eslintrc.js ├── .github ├── settings.yml └── workflows │ ├── build.yml │ ├── release-github.yaml │ └── release-npm.yaml ├── .gitignore ├── .mocharc.js ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASING.md ├── docs ├── basic.png └── homebrew.png ├── package-lock.json ├── package.json ├── renovate.json ├── scripts ├── remove-empty-sections-changelog.awk └── update-changelog.sh ├── src ├── indentStyleText.spec.ts ├── indentStyleText.ts ├── index.ts ├── styleText.spec.ts ├── styleText.ts ├── theme.spec.ts └── theme.ts ├── test ├── background.spec.ts ├── data-table.spec.ts ├── description.spec.ts ├── doc-string.spec.ts ├── exec.ts ├── feature.spec.ts ├── features │ ├── background.feature │ ├── data-table.feature │ ├── description.feature │ ├── doc-string.feature │ ├── feature.feature │ ├── feature2.feature │ ├── fr.feature │ ├── hook.feature │ ├── ru.feature │ ├── rule-background.feature │ ├── rule.feature │ ├── scenario-outline.feature │ ├── scenario.feature │ ├── step.feature │ ├── support │ │ ├── World.ts │ │ ├── hooks.ts │ │ └── steps.ts │ ├── tag.feature │ └── world.feature ├── hook.spec.ts ├── i18n.spec.ts ├── rule.spec.ts ├── scenario-outline.spec.ts ├── scenario.spec.ts ├── step.spec.ts ├── summary.spec.ts ├── tag.spec.ts └── text-styling.spec.ts ├── tsconfig.json └── tsconfig.node.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | 6 | [*.js] 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*.ts] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.feature] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | 'prettier', 6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module', // Allows for the use of imports 11 | }, 12 | rules: { 13 | '@typescript-eslint/explicit-function-return-type': 0, 14 | '@typescript-eslint/no-empty-function': 0, 15 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | 3 | repository: 4 | name: cucumber-js-pretty-formatter 5 | description: Cucumber.js pretty formatter 6 | 7 | teams: 8 | # See https://docs.github.com/en/rest/reference/teams#add-or-update-team-repository-permissions for available options 9 | 10 | - name: committers 11 | permission: push 12 | 13 | - name: cucumber-js 14 | permission: admin 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | workflow_call: 8 | push: 9 | branches: 10 | - main 11 | - renovate/** 12 | 13 | jobs: 14 | tests: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node: [14.x, 16.x, 18.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Node.js ${{ matrix.node }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node }} 27 | - name: Cache node modules 28 | uses: actions/cache@v4 29 | env: 30 | cache-name: cache-node-modules 31 | with: 32 | # npm cache files are stored in `~/.npm` on Linux/macOS 33 | path: ~/.npm 34 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 35 | restore-keys: | 36 | ${{ runner.os }}-build-${{ env.cache-name }}- 37 | ${{ runner.os }}-build- 38 | ${{ runner.os }}- 39 | - run: npm install 40 | - run: npm test 41 | 42 | lint: 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | - name: Set up Node.js 48 | uses: actions/setup-node@v4 49 | - run: npm install 50 | - name: Lint code 51 | run: npm run lint 52 | -------------------------------------------------------------------------------- /.github/workflows/release-github.yaml: -------------------------------------------------------------------------------- 1 | name: Release GitHub 2 | 3 | on: 4 | push: 5 | branches: [release/*] 6 | 7 | jobs: 8 | create-github-release: 9 | name: Create GitHub Release and Git tag 10 | runs-on: ubuntu-latest 11 | environment: Release 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: cucumber/action-create-github-release@v1.1.1 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/release-npm.yaml: -------------------------------------------------------------------------------- 1 | name: Release NPM (latest) 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'release/*' 7 | 8 | jobs: 9 | publish-npm: 10 | name: Publish NPM module 11 | runs-on: ubuntu-latest 12 | environment: Release 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: '16' 18 | cache: 'npm' 19 | cache-dependency-path: package-lock.json 20 | - run: npm install 21 | - run: npm run build:release 22 | - uses: cucumber/action-publish-npm@v1.1.1 23 | with: 24 | npm-token: ${{ secrets.NPM_TOKEN }} 25 | npm-tag: 'latest' 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | spec: ['lib/**/*.spec.js'], 3 | 'watch-files': ['lib/**/*'] 4 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "semi": false, 5 | "printWidth": 80 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CONTRIBUTING.md) on how to contribute to Cucumber. 9 | 10 | ## Unreleased 11 | 12 | ## 1.0.1 - 2024-03-26 13 | ### Fixed 14 | - Correct repo URL in `package.json` 15 | 16 | ## [1.0.0] - 2022-06-30 17 | ### Added 18 | - Export default theme to make configuration easier ([#16](https://github.com/cucumber/cucumber-js-pretty-formatter/pull/16)) 19 | 20 | ## [1.0.0-alpha.2] 21 | ### Fixed 22 | - Replace `@cucumber/cucumber` deep imports with equivalents from main entry point 23 | 24 | ## [1.0.0-alpha.1] 25 | ### Fixed 26 | - Fix compatibility issues with cucumber-js 7.3.0 [#5](https://github.com/cucumber/cucumber-pretty-formatter/pull/5) 27 | 28 | ## [v1.0.0-alpha.0] 29 | ### Changed 30 | - Thank you Ilya for the work you've done! The objective of this fork is to provide Cucumber users with an officially supported pretty formatter for Cucumber.js. 31 | - This is the first version of the pretty formatter that supports Cucumber.js 7.0.0 and above, now based on [cucumber-messages](https://github.com/cucumber/cucumber/tree/master/messages) (instead of the now deceased event protocol). 32 | - All the codebase has been migrated to TypeScript, yey! 33 | - The pretty formatter can now be customised! Ansi styles can be applied to almost all elements of the output. 34 | - The latest addition to the Gherkin syntax, the `Rule` keyword, is supported. 35 | 36 | [1.0.0]: https://github.com/cucumber/cucumber-pretty-formatter/compare/v1.0.0-alpha.2...v1.0.0 37 | [1.0.0-alpha.2]: https://github.com/cucumber/cucumber-pretty-formatter/compare/v1.0.0-alpha.1...v1.0.0-alpha.2 38 | [1.0.0-alpha.1]: https://github.com/cucumber/cucumber-pretty-formatter/compare/v1.0.0-alpha.0...v1.0.0-alpha.1 39 | [v1.0.0-alpha.0]: https://github.com/cucumber/cucumber-pretty-formatter/compare/03f000d68098f854b9596f812a474857df675491...v1.0.0-alpha.0 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to cucumber-pretty 2 | 3 | ## Making changes 4 | 5 | * Use TDD 6 | * Update `CHANGELOG.md` when you make a significant change 7 | 8 | ## Tests 9 | 10 | To build the project and run all the tests in one go, simply run: 11 | 12 | npm test 13 | 14 | As a BDD practitioner, you'll probably need to run the tests often. They are run using [Mocha](https://mochajs.org/). The source of cucumber-pretty is written in [TypeScript](https://typescriptlang.org/) and needs to be compiled to JavaScript to run. To do so, run the build watch task in one terminal: 15 | 16 | npm run build:watch 17 | 18 | All TypeScript sources found in `src/` and `test/` are now being built into `lib/`. That directory contains both the production code we release (`lib/src/`) as an NPM package and also all the tests (`lib/test/`). Run those tests with the following command: 19 | 20 | npm run test:mocha 21 | 22 | When focussing on a single test, you can add the string `wip` to its description and run `npm run test:mocha:wip`. Keep in mind that the `build:watch` daemon needs to run in the background for your changes to be picked up after you save your changes. 23 | 24 | You can also automatically rerun the tests on file changes: 25 | 26 | npm run test:mocha:wip:watch 27 | 28 | ## Release process 29 | 30 | _The following is a checklist for maintainers when preparing a new release_ 31 | 32 | ### Major releases 33 | 34 | We will always make a release candidate before issuing a major release. The release candidate will be available for at least a month to give users time to validate that there are no unexpected breaking changes. 35 | 36 | ### Process 37 | 38 | The release is done from the [cucumber-build](https://github.com/cucumber/cucumber-build/) docker container. This makes 39 | sure we use the same environment for all releases. 40 | 41 | **Every command should be run from within the Docker container**. 42 | 43 | Start the container: 44 | 45 | make docker-run 46 | 47 | Inside the container, update dependencies: 48 | 49 | npm run update-dependencies 50 | npm install 51 | npm build:release 52 | 53 | That last command will build, run the tests and remove test files from the package to be published. 54 | 55 | If the tests fail, update your code to be compatible with the new libraries, or revert the library upgrades that break the build. 56 | 57 | * Add missing entries to `CHANGELOG.md` 58 | * Ideally the CHANGELOG should be up-to-date, but sometimes there will be accidental omissions when merging PRs. Missing PRs should be added. 59 | * Describe the major changes introduced. API changes must be documented. In particular, backward-incompatible changes must be well explained, with examples when possible. 60 | * `git log --format=format:"* %s (%an)" --reverse ..HEAD` might be handy. 61 | * Update the contributors list in `package.json` 62 | * `git log --format=format:"%an <%ae>" --reverse ..HEAD | grep -vEi "(renovate|dependabot|Snyk)" | sort| uniq -i` 63 | * Manually add contributors (in alphabetical order) 64 | 65 | [Decide what the next version should be](https://github.com/cucumber/cucumber/blob/master/RELEASE_PROCESS.md#decide-what-the-next-version-should-be). 66 | 67 | Update CHANGELOG links: 68 | 69 | NEW_VERSION= make update-changelog 70 | 71 | Verify changes to the CHANGELOG are correct. Stage uncommitted changes: 72 | 73 | git add . 74 | git commit -am "Release " 75 | 76 | Then bump the version number and create a git tag. Run *one* of the following: 77 | 78 | # Major prelease 79 | npm version premajor --preid=rc 80 | 81 | # Major release 82 | npm version major 83 | 84 | # Minor release 85 | npm version minor 86 | 87 | # Patch release 88 | npm version patch 89 | 90 | Publish to npm: 91 | 92 | npm publish --access public 93 | 94 | Push to git: 95 | 96 | git push 97 | git push --tags -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ilya Kozhevnikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Official Cucumber.js Pretty Formatter 2 | 3 | [![build][build-badge]][build] 4 | [![npm][version]][npm] 5 | [![npm][downloads]][npm] 6 | 7 | [build]: https://github.com/jbpros/cucumber-pretty-formatter/actions?query=workflow%3Abuild 8 | [build-badge]: https://github.com/jbpros/cucumber-pretty-formatter/workflows/build/badge.svg 9 | [npm]: https://www.npmjs.com/package/@cucumber/pretty-formatter 10 | [version]: https://img.shields.io/npm/v/@cucumber/pretty-formatter.svg 11 | [downloads]: https://img.shields.io/npm/dm/@cucumber/pretty-formatter.svg 12 | 13 | The Cucumber.js pretty formatter logs your feature suite in its original Gherkin form. It offers custom style themes. 14 | 15 | ## Install 16 | 17 | The pretty formatter requires: 18 | 19 | - Node.js 10, 12, 14 or 15. 20 | - [Cucumber.js](https://www.npmjs.com/package/@cucumber/cucumber) 7.0 and above. 21 | 22 | npm install --save-dev @cucumber/pretty-formatter @cucumber/cucumber 23 | 24 | There are pretty formatters for [older versions of Cucumber](#older-cucumber-versions). 25 | 26 | ## Usage 27 | 28 | cucumber-js -f @cucumber/pretty-formatter 29 | 30 | We recommend using [Cucumber profiles](https://github.com/cucumber/cucumber-js/blob/master/docs/cli.md#profiles) to [specify formatters](https://github.com/cucumber/cucumber-js/blob/master/docs/cli.md#formats). 31 | 32 | ## Theme customisation 33 | 34 | You can define your own colors by passing a `theme` format option: 35 | 36 | --format-options '{"theme": }' 37 | 38 | Where `THEME_JSON` is in the following shape: 39 | 40 | ```json 41 | {"feature keyword": ["magenta", "bold"], "scenario keyword": ["red"]} 42 | ``` 43 | 44 | The customisable theme items are: 45 | 46 | * `datatable border` 47 | * `datatable content` 48 | * `datatable`: all data table elements (border and content) 49 | * `docstring content`: multiline argument content 50 | * `docstring delimiter`: multiline argument delimiter: `"""` 51 | * `feature description` 52 | * `feature keyword` 53 | * `feature name` 54 | * `location`: location comments added to the right of feature and scenario names 55 | * `rule keyword` 56 | * `rule name` 57 | * `scenario keyword` 58 | * `scenario name` 59 | * `step keyword` 60 | * `step message`: usually a failing step error message and stack trace 61 | * `step status`: additional styles added to the built-in styles applied by Cucumber to non-passing steps status. Foreground colors have no effects on this item, background and modifiers do. 62 | * `step text` 63 | * `tag` 64 | 65 | You can combine all the styles you'd like from [modifiers, foreground colors and background colors exposed by ansi-styles](https://github.com/chalk/ansi-styles#styles). 66 | 67 | ### Extending the Default Theme 68 | 69 | If you just want to tweak a few things about the default theme without redefining it entirely, you can grab the default theme in your `cucumber.js` config file and use it as the base for yours: 70 | 71 | ```js 72 | const { DEFAULT_THEME } = require('@cucumber/pretty-formatter') 73 | 74 | module.exports = { 75 | default: { 76 | formatOptions: { 77 | theme: { 78 | ...DEFAULT_THEME, 79 | 'step text': 'magenta' 80 | } 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | ### Example Themes 87 | 88 | #### _Matrix_ 89 | 90 | It could be called *eco-friendly*, cuz it's very green: 91 | 92 | --format-options '{"theme":{"datatable border":["green"],"datatable content":["green","italic"],"docstring content":["green","italic"],"docstring delimiter":["green"],"feature description":["green"],"feature keyword":["bold","green"],"rule keyword":["yellow"],"scenario keyword":["greenBright"],"scenario name":["green","underline"],"step keyword":["bgGreen","black","italic"],"step text":["greenBright","italic"],"tag":["green"]}}' 93 | 94 | #### _Legacy pretty_ 95 | 96 | This was the theme offered by [Ilya Kozhevnikov](http://kozhevnikov.com/)'s pretty formatter, pre-Cucumber.js 7.x. 97 | 98 | 99 | 100 | 101 | --format-options '{"theme":{"feature keyword":["magenta","bold"],"scenario keyword":["magenta","bold"],"step keyword":["bold"]}}' 102 | 103 | ### We need more themes 104 | 105 | Please share your creations by forking, adding the theme to this section of the README and [opening a pull request](https://github.com/jbpros/cucumber-pretty-formatter/pulls). 106 | 107 | ## Older Cucumber versions 108 | 109 | If you're using an older version of Cucumber.js, you'll need to use one of the previous pretty formatters: 110 | 111 | ### Cucumber.js 1 → 2 112 | 113 | The original pretty formatter used to ship with Cucumber. Simply specify it when invoking Cucumber: 114 | 115 | cucumber-js -f pretty 116 | 117 | ### Cucumber.js 3 → 6 118 | 119 | You can install [`cucumber-pretty`](https://www.npmjs.com/package/cucumber-pretty), created by [Ilya Kozhevnikov](http://kozhevnikov.com/). 120 | 121 | - Cucumber.js 3, 4, 5: `npm i --save-dev cucumber-pretty@1.5` 122 | - Cucumber.js 6: `npm i --save-dev cucumber-pretty@6` 123 | 124 | Tell Cucumber to use it: 125 | 126 | cucumber-js -f cucumber-pretty 127 | 128 | ## Credits 129 | 130 | This project is based on the [original work](https://github.com/kozhevnikov/cucumber-pretty) of [Ilya Kozhevnikov](http://kozhevnikov.com/). It got migrated to TypeScript, upgraded for Cucumber.js 7+ that exposes [cucumber-messages](https://github.com/cucumber/cucumber/tree/master/messages) and is currently maintained by [Julien Biezemans](https://github.com/jbpros/) and the [Cucumber team](https://github.com/cucumber). 131 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | See [.github/RELEASING](https://github.com/cucumber/.github/blob/main/RELEASING.md). 2 | -------------------------------------------------------------------------------- /docs/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-js-pretty-formatter/b73d06489c99e1505b130f75d0aaf6ac61cf84f8/docs/basic.png -------------------------------------------------------------------------------- /docs/homebrew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-js-pretty-formatter/b73d06489c99e1505b130f75d0aaf6ac61cf84f8/docs/homebrew.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cucumber/pretty-formatter", 3 | "version": "1.0.1", 4 | "description": "Official Cucumber.js Pretty Formatter", 5 | "repository": "https://github.com/cucumber/cucumber-js-pretty-formatter", 6 | "maintainers": [ 7 | "Julien Biezemans " 8 | ], 9 | "contributors": [ 10 | "Ilya Kozhevnikov ", 11 | "Julien Biezemans " 12 | ], 13 | "license": "MIT", 14 | "main": "lib/src/index.js", 15 | "types": "lib/src/index.d.ts", 16 | "files": [ 17 | "lib/src" 18 | ], 19 | "directories": { 20 | "lib": "lib" 21 | }, 22 | "scripts": { 23 | "build:clean:tests": "rm -rf lib/test", 24 | "build:clean": "rm -rf lib", 25 | "build:release": "npm run build && npm run test:nobuild && npm run build:clean:tests", 26 | "build:watch": "tsc -p tsconfig.node.json --watch", 27 | "build": "tsc -p tsconfig.node.json", 28 | "lint:fix": "eslint --fix \"src/**/*.ts\" \"test/**/*.ts\"", 29 | "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"", 30 | "test:mocha:wip:watch": "mocha -w -g wip", 31 | "test:mocha:wip": "mocha -g wip", 32 | "test:mocha": "mocha", 33 | "test:nobuild": "npm run test:mocha", 34 | "test": "npm run build && npm run test:mocha" 35 | }, 36 | "dependencies": { 37 | "ansi-styles": "^5.0.0", 38 | "cli-table3": "^0.6.0", 39 | "figures": "^3.2.0", 40 | "ts-dedent": "^2.0.0" 41 | }, 42 | "peerDependencies": { 43 | "@cucumber/cucumber": ">=7.0.0", 44 | "@cucumber/messages": "*" 45 | }, 46 | "devDependencies": { 47 | "@cucumber/cucumber": "^8.3.1", 48 | "@cucumber/messages": "^24.0.0", 49 | "@types/glob": "^8.0.0", 50 | "@types/mocha": "^10.0.0", 51 | "@types/node": "^18.0.0", 52 | "@typescript-eslint/eslint-plugin": "^8.0.0", 53 | "@typescript-eslint/parser": "^8.0.0", 54 | "colors": "^1.4.0", 55 | "eslint": "^8.0.0", 56 | "eslint-config-prettier": "^9.0.0", 57 | "eslint-plugin-import": "^2.22.1", 58 | "eslint-plugin-prettier": "^5.0.0", 59 | "glob": "^11.0.0", 60 | "mocha": "^10.0.0", 61 | "prettier": "^3.0.0", 62 | "should": "^13.2.3", 63 | "stream-to-string": "^1.2.0", 64 | "typescript": "^4.1.3" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>cucumber/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /scripts/remove-empty-sections-changelog.awk: -------------------------------------------------------------------------------- 1 | function start_buffering() { 2 | buf = $0 3 | } 4 | function store_line_in_buffer() { 5 | buf = buf ORS $0 6 | } 7 | function clear_buffer() { 8 | buf = "" 9 | } 10 | /^### (Added|Changed|Deprecated|Removed|Fixed)$/ { 11 | start_buffering() 12 | next 13 | } 14 | /^## / { 15 | clear_buffer() 16 | } 17 | /^ *$/ { 18 | if (buf != "") { 19 | store_line_in_buffer() 20 | } else { 21 | print $0 22 | } 23 | } 24 | !/^ *$/ { 25 | if (buf != "") { 26 | print buf 27 | clear_buffer() 28 | } 29 | print $0 30 | } -------------------------------------------------------------------------------- /scripts/update-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -uf -o pipefail 3 | 4 | # Reads a changelog from STDIN and writes out a new one to STDOUT where: 5 | # 6 | # * The [Unreleased] diff link is updated 7 | # * A new diff link for the new release is added 8 | # * The ## [Unreleased] header is changed to a version header with date 9 | # * The empty sections are removed 10 | # * A new, empty [Unreleased] paragraph is added at the top 11 | # 12 | 13 | changelog=$(&2 echo "No version found in link: ${unreleased_link}" 54 | exit 1 55 | fi 56 | 57 | # Insert a new release diff link 58 | 59 | insertion_line_number=$((line_number + 1)) 60 | release_link=$(echo "${changelog}" | head -n ${insertion_line_number} | tail -1) 61 | 62 | new_release_link=$(echo "${release_link}" | \ 63 | sed "s/${last_version}/${new_version}/g" | \ 64 | sed "s/v[0-9]\+.[0-9]\+.[0-9]\+/${last_version}/") 65 | 66 | changelog=$(echo "${changelog}" | sed "${insertion_line_number} i \\ 67 | ${new_release_link} 68 | ") 69 | 70 | # Remove empty sections 71 | 72 | scripts_path="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" 73 | changelog=$(echo "${changelog}" | awk -f "${scripts_path}/remove-empty-sections-changelog.awk") 74 | 75 | # Insert a new [Unreleased] header 76 | 77 | changelog=$(echo "${changelog}" | sed "s/----/----\\ 78 | ${header_escaped}\\ 79 | /g") 80 | 81 | echo "${changelog}" -------------------------------------------------------------------------------- /src/indentStyleText.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { indentStyleText } from './indentStyleText' 4 | import { styleText } from './styleText' 5 | 6 | describe('indentStyleText', () => { 7 | it('leaves text unstyled', () => { 8 | indentStyleText(0, 'some text').should.eql('some text') 9 | }) 10 | 11 | it('trims text on the right', () => { 12 | indentStyleText(0, ' \t some text ').should.eql(' \t some text') 13 | }) 14 | 15 | it('indents text', () => { 16 | indentStyleText(2, 'some text').should.eql(' some text') 17 | }) 18 | 19 | it('indents and trims text on the right', () => { 20 | indentStyleText(2, ' some text ').should.eql(' some text') 21 | }) 22 | 23 | it('trims multiline text', () => { 24 | indentStyleText(0, ' \t \n some \t \n \t more text \n ').should.eql( 25 | '\n some\n \t more text\n' 26 | ) 27 | }) 28 | 29 | it('indents multiline text', () => { 30 | indentStyleText(3, ' \n some \n \t more text \n ').should.eql( 31 | '\n some\n \t more text\n' 32 | ) 33 | }) 34 | 35 | it("doesn't indent only whitespace-lines", () => { 36 | indentStyleText(3, '\n \n \t').should.eql('\n\n') 37 | }) 38 | 39 | it('applies styles to the text', () => { 40 | indentStyleText(0, 'some text', ['red']).should.eql( 41 | styleText('some text', 'red') 42 | ) 43 | }) 44 | 45 | it('applies styles separately to lines', () => { 46 | indentStyleText(0, 'some text\nmore text', ['red']).should.eql( 47 | `${styleText('some text', 'red')}\n${styleText('more text', 'red')}` 48 | ) 49 | }) 50 | 51 | it('applies styles separately to indented lines', () => { 52 | indentStyleText(2, 'some text\nmore text', ['red']).should.eql( 53 | ` ${styleText('some text', 'red')}\n ${styleText('more text', 'red')}` 54 | ) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/indentStyleText.ts: -------------------------------------------------------------------------------- 1 | import { styleText, TextStyle } from './styleText' 2 | 3 | export const indentStyleText = ( 4 | indent: number, 5 | text: string, 6 | styles: TextStyle[] = [] 7 | ): string => 8 | text.replace(/^(.+)$/gm, (subText) => 9 | subText.trim().length === 0 10 | ? '' 11 | : `${' '.repeat(indent)}${styleText(subText.trimRight(), ...styles)}` 12 | ) 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Status, 3 | SummaryFormatter, 4 | IFormatterOptions, 5 | formatterHelpers, 6 | } from '@cucumber/cucumber' 7 | import * as messages from '@cucumber/messages' 8 | import * as CliTable3 from 'cli-table3' 9 | import { cross, tick } from 'figures' 10 | import { EOL as n } from 'os' 11 | import dedent from 'ts-dedent' 12 | 13 | import { makeTheme, ThemeItem, ThemeStyles } from './theme' 14 | 15 | const { formatLocation, GherkinDocumentParser, PickleParser } = formatterHelpers 16 | const { getGherkinExampleRuleMap, getGherkinScenarioMap, getGherkinStepMap } = 17 | GherkinDocumentParser 18 | const { getPickleStepMap } = PickleParser 19 | 20 | const marks = { 21 | [Status.AMBIGUOUS]: cross, 22 | [Status.FAILED]: cross, 23 | [Status.PASSED]: tick, 24 | [Status.PENDING]: '?', 25 | [Status.SKIPPED]: '-', 26 | [Status.UNDEFINED]: '?', 27 | [Status.UNKNOWN]: '?', 28 | } 29 | 30 | const themeItemIndentations: { [key in ThemeItem]: number } = { 31 | [ThemeItem.DataTable]: 6, 32 | [ThemeItem.DataTableBorder]: 0, 33 | [ThemeItem.DataTableContent]: 0, 34 | [ThemeItem.DocStringContent]: 6, 35 | [ThemeItem.DocStringDelimiter]: 6, 36 | [ThemeItem.FeatureDescription]: 2, 37 | [ThemeItem.FeatureKeyword]: 0, 38 | [ThemeItem.FeatureName]: 0, 39 | [ThemeItem.Location]: 0, 40 | [ThemeItem.RuleKeyword]: 2, 41 | [ThemeItem.RuleName]: 0, 42 | [ThemeItem.ScenarioKeyword]: 2, 43 | [ThemeItem.ScenarioName]: 0, 44 | [ThemeItem.StepKeyword]: 4, 45 | [ThemeItem.StepMessage]: 6, 46 | [ThemeItem.StepStatus]: 4, 47 | [ThemeItem.StepText]: 0, 48 | [ThemeItem.Tag]: 0, 49 | } 50 | 51 | export const DEFAULT_THEME: ThemeStyles = { 52 | [ThemeItem.DataTable]: [], 53 | [ThemeItem.DataTableBorder]: ['gray'], 54 | [ThemeItem.DataTableContent]: ['gray', 'italic'], 55 | [ThemeItem.DocStringContent]: ['gray', 'italic'], 56 | [ThemeItem.DocStringDelimiter]: ['gray'], 57 | [ThemeItem.FeatureDescription]: ['gray'], 58 | [ThemeItem.FeatureKeyword]: ['blueBright', 'bold'], 59 | [ThemeItem.FeatureName]: ['blueBright', 'underline'], 60 | [ThemeItem.Location]: ['dim'], 61 | [ThemeItem.RuleKeyword]: ['blueBright', 'bold'], 62 | [ThemeItem.RuleName]: ['blueBright', 'underline'], 63 | [ThemeItem.ScenarioKeyword]: ['cyan', 'bold'], 64 | [ThemeItem.ScenarioName]: ['cyan', 'underline'], 65 | [ThemeItem.StepKeyword]: ['cyan'], 66 | [ThemeItem.StepMessage]: [], 67 | [ThemeItem.StepStatus]: [], 68 | [ThemeItem.StepText]: [], 69 | [ThemeItem.Tag]: ['cyan'], 70 | } 71 | 72 | export default class PrettyFormatter extends SummaryFormatter { 73 | private uri?: string 74 | private lastRuleId?: string 75 | private indentOffset = 0 76 | private logItem: (item: ThemeItem, ...text: string[]) => void 77 | private styleItem: ( 78 | indent: number, 79 | item: ThemeItem, 80 | ...text: string[] 81 | ) => string 82 | private tableLayout: CliTable3.TableConstructorOptions 83 | 84 | constructor(options: IFormatterOptions) { 85 | super(options) 86 | const theme = makeTheme( 87 | !!options.parsedArgvOptions.colorsEnabled 88 | ? options.parsedArgvOptions.theme || DEFAULT_THEME 89 | : {} 90 | ) 91 | 92 | this.styleItem = (indent: number, item: ThemeItem, ...text: string[]) => { 93 | if (indent > 0 && this.indentOffset > 0) 94 | indent = indent + this.indentOffset 95 | return theme.indentStyleText(indent, item, ...text) 96 | } 97 | 98 | this.logItem = (item: ThemeItem, ...text: string[]) => 99 | this.log(this.styleItem(themeItemIndentations[item], item, ...text)) 100 | 101 | this.parseEnvelope = this.parseEnvelope.bind(this) 102 | 103 | const tableFrameChar = theme.indentStyleText( 104 | 0, 105 | ThemeItem.DataTableBorder, 106 | '│' 107 | ) 108 | 109 | this.tableLayout = { 110 | chars: { 111 | left: tableFrameChar, 112 | middle: tableFrameChar, 113 | right: tableFrameChar, 114 | top: '', 115 | 'top-left': '', 116 | 'top-mid': '', 117 | 'top-right': '', 118 | mid: '', 119 | 'left-mid': '', 120 | 'mid-mid': '', 121 | 'right-mid': '', 122 | bottom: '', 123 | 'bottom-left': '', 124 | 'bottom-mid': '', 125 | 'bottom-right': '', 126 | }, 127 | style: { 128 | head: [], 129 | border: [], 130 | }, 131 | } 132 | 133 | options.eventBroadcaster.on('envelope', this.parseEnvelope) 134 | } 135 | 136 | private parseEnvelope(envelope: messages.Envelope) { 137 | if (envelope.testCaseStarted) 138 | this.onTestCaseStarted(envelope.testCaseStarted) 139 | if (envelope.testStepStarted) 140 | this.onTestStepStarted(envelope.testStepStarted) 141 | if (envelope.testStepFinished) 142 | this.onTestStepFinished(envelope.testStepFinished) 143 | if (envelope.testCaseFinished) 144 | this.onTestCaseFinished(envelope.testCaseFinished) 145 | } 146 | 147 | private onTestCaseStarted(testCaseStarted: messages.TestCaseStarted) { 148 | const { gherkinDocument, pickle } = 149 | this.eventDataCollector.getTestCaseAttempt(testCaseStarted.id || '') 150 | const { feature } = gherkinDocument 151 | if (this.uri !== gherkinDocument.uri && feature) { 152 | this.indentOffset = 0 153 | this.uri = gherkinDocument.uri || '' 154 | this.lastRuleId = undefined 155 | this.renderFeatureHead(feature) 156 | } 157 | 158 | const gherkinExampleRuleMap = getGherkinExampleRuleMap(gherkinDocument) 159 | if (!pickle.astNodeIds) throw new Error('Pickle AST nodes missing') 160 | const rule = gherkinExampleRuleMap[pickle.astNodeIds[0]] 161 | if (rule && rule.id !== this.lastRuleId) { 162 | this.indentOffset = 0 163 | this.renderRule(rule) 164 | this.lastRuleId = rule.id 165 | this.indentOffset = 2 166 | } 167 | 168 | this.renderScenarioHead(gherkinDocument, pickle) 169 | } 170 | 171 | private onTestStepStarted(testStepStarted: messages.TestStepStarted) { 172 | const { gherkinDocument, pickle, testCase } = 173 | this.eventDataCollector.getTestCaseAttempt( 174 | testStepStarted.testCaseStartedId || '' 175 | ) 176 | 177 | const pickleStepMap = getPickleStepMap(pickle) 178 | const gherkinStepMap = getGherkinStepMap(gherkinDocument) 179 | const testStep = (testCase.testSteps || []).find( 180 | (item) => item.id === testStepStarted.testStepId 181 | ) 182 | 183 | if (testStep && testStep.pickleStepId) { 184 | const pickleStep = pickleStepMap[testStep.pickleStepId] 185 | const astNodeId = pickleStep.astNodeIds[0] 186 | const gherkinStep = gherkinStepMap[astNodeId] 187 | this.logItem(ThemeItem.StepKeyword, gherkinStep.keyword) 188 | this.log(' ') 189 | this.logItem(ThemeItem.StepText, pickleStep.text) 190 | this.newline() 191 | 192 | if (gherkinStep.docString) { 193 | this.logItem( 194 | ThemeItem.DocStringDelimiter, 195 | gherkinStep.docString.delimiter 196 | ) 197 | this.newline() 198 | this.logItem(ThemeItem.DocStringContent, gherkinStep.docString.content) 199 | this.newline() 200 | this.logItem( 201 | ThemeItem.DocStringDelimiter, 202 | gherkinStep.docString.delimiter 203 | ) 204 | this.newline() 205 | } 206 | 207 | if (gherkinStep.dataTable) { 208 | const datatable = new CliTable3(this.tableLayout) 209 | datatable.push( 210 | ...gherkinStep.dataTable.rows.map((row: messages.TableRow) => 211 | (row.cells || []).map((cell) => 212 | this.styleItem(0, ThemeItem.DataTableContent, cell.value || '') 213 | ) 214 | ) 215 | ) 216 | this.logItem(ThemeItem.DataTable, datatable.toString()) 217 | this.newline() 218 | } 219 | } 220 | } 221 | 222 | private onTestStepFinished(testStepFinished: messages.TestStepFinished) { 223 | const { message, status } = testStepFinished.testStepResult || {} 224 | 225 | if (status && status !== Status.PASSED) { 226 | this.logItem( 227 | ThemeItem.StepStatus, 228 | this.colorFns.forStatus(status)( 229 | `${marks[status]} ${Status[status].toLowerCase()}` 230 | ) 231 | ) 232 | this.newline() 233 | 234 | if (message) { 235 | this.logItem(ThemeItem.StepMessage, message) 236 | this.newline() 237 | } 238 | } 239 | } 240 | 241 | private onTestCaseFinished(_testCaseFinished: messages.TestCaseFinished) { 242 | this.newline() 243 | } 244 | 245 | private renderTags(indent: number, tags: readonly { name: string }[]) { 246 | const tagStrings = tags.reduce( 247 | (tags, tag) => (tag.name ? [...tags, tag.name] : tags), 248 | [] 249 | ) 250 | if (tagStrings.length > 0) { 251 | const firstTag = tagStrings.shift() as string 252 | this.log(this.styleItem(indent, ThemeItem.Tag, firstTag)) 253 | tagStrings.forEach((tag) => { 254 | this.log(' ') 255 | this.logItem(ThemeItem.Tag, tag) 256 | }) 257 | this.newline() 258 | } 259 | } 260 | 261 | private renderFeatureHead(feature: messages.Feature) { 262 | this.renderTags(0, feature.tags || []) 263 | this.logItem(ThemeItem.FeatureKeyword, feature.keyword || '[feature]', ':') 264 | this.log(' ') 265 | this.logItem(ThemeItem.FeatureName, feature.name || '') 266 | if (feature.location) { 267 | this.log(' ') 268 | this.renderLocation(feature.location?.line || -1) 269 | } 270 | this.newline() 271 | 272 | if (feature.description) { 273 | this.newline() 274 | this.logItem( 275 | ThemeItem.FeatureDescription, 276 | dedent(feature.description.trim()) 277 | ) 278 | this.newline() 279 | } 280 | this.newline() 281 | } 282 | 283 | private renderRule(rule: messages.Rule) { 284 | this.logItem(ThemeItem.RuleKeyword, rule.keyword, ':') 285 | this.log(' ') 286 | this.logItem(ThemeItem.RuleName, rule.name || '') 287 | this.newline() 288 | this.newline() 289 | } 290 | 291 | private renderScenarioHead( 292 | gherkinDocument: messages.GherkinDocument, 293 | pickle: messages.Pickle 294 | ) { 295 | this.renderTags(2, pickle.tags || []) 296 | const gherkinScenarioMap = getGherkinScenarioMap(gherkinDocument) 297 | if (!pickle.astNodeIds) throw new Error('Pickle AST nodes missing') 298 | 299 | const scenario: messages.Scenario = gherkinScenarioMap[pickle.astNodeIds[0]] 300 | this.logItem(ThemeItem.ScenarioKeyword, scenario.keyword || '???', ':') 301 | this.log(' ') 302 | this.logItem(ThemeItem.ScenarioName, pickle.name || '') 303 | if (gherkinScenarioMap[pickle.astNodeIds[0]]) { 304 | this.log(' ') 305 | this.renderLocation(scenario.location?.line || -1) 306 | } 307 | this.newline() 308 | } 309 | 310 | private renderLocation(line: number) { 311 | this.logItem( 312 | ThemeItem.Location, 313 | '# ', 314 | formatLocation({ uri: this.uri || '', line }, process.cwd()) 315 | ) 316 | } 317 | 318 | private newline() { 319 | this.log(n) 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/styleText.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { styleText } from './styleText' 4 | 5 | describe('styleText()', () => { 6 | it('applies modifiers', () => { 7 | styleText('Burpless 26', 'bold').should.containEql( 8 | '\u001b[1mBurpless 26\u001b[22m' 9 | ) 10 | styleText('Burpless 26', 'italic').should.containEql( 11 | '\u001b[3mBurpless 26\u001b[23m' 12 | ) 13 | }) 14 | 15 | it('applies foreground colors (red)', () => { 16 | styleText('Burpless 26', 'red').should.containEql( 17 | '\u001b[31mBurpless 26\u001b[39m' 18 | ) 19 | }) 20 | 21 | it('applies foreground colors (green)', () => { 22 | styleText('Burpless 26', 'green').should.containEql( 23 | '\u001b[32mBurpless 26\u001b[39m' 24 | ) 25 | }) 26 | 27 | it('applies foreground colors (blue)', () => { 28 | styleText('Burpless 26', 'blue').should.containEql( 29 | '\u001b[34mBurpless 26\u001b[39m' 30 | ) 31 | }) 32 | 33 | it('applies bright foreground colors (red)', () => { 34 | styleText('Burpless 26', 'redBright').should.containEql( 35 | '\u001b[91mBurpless 26\u001b[39m' 36 | ) 37 | }) 38 | 39 | it('applies bright foreground colors (green)', () => { 40 | styleText('Burpless 26', 'greenBright').should.containEql( 41 | '\u001b[92mBurpless 26\u001b[39m' 42 | ) 43 | }) 44 | 45 | it('applies bright foreground colors (blue)', () => { 46 | styleText('Burpless 26', 'blueBright').should.containEql( 47 | '\u001b[94mBurpless 26\u001b[39m' 48 | ) 49 | }) 50 | 51 | it('applies background colors (red)', () => { 52 | styleText('Burpless 26', 'bgRed').should.containEql( 53 | '\u001b[41mBurpless 26\u001b[49m' 54 | ) 55 | }) 56 | 57 | it('applies background colors (green)', () => { 58 | styleText('Burpless 26', 'bgGreen').should.containEql( 59 | '\u001b[42mBurpless 26\u001b[49m' 60 | ) 61 | }) 62 | 63 | it('applies background colors (blue)', () => { 64 | styleText('Burpless 26', 'bgBlue').should.containEql( 65 | '\u001b[44mBurpless 26\u001b[49m' 66 | ) 67 | }) 68 | 69 | it('applies bright background colors (red)', () => { 70 | styleText('Burpless 26', 'bgRedBright').should.containEql( 71 | '\u001b[101mBurpless 26\u001b[49m' 72 | ) 73 | }) 74 | 75 | it('applies bright background colors (green)', () => { 76 | styleText('Burpless 26', 'bgGreenBright').should.containEql( 77 | '\u001b[102mBurpless 26\u001b[49m' 78 | ) 79 | }) 80 | 81 | it('applies bright background colors (blue)', () => { 82 | styleText('Burpless 26', 'bgBlueBright').should.containEql( 83 | '\u001b[104mBurpless 26\u001b[49m' 84 | ) 85 | }) 86 | 87 | it('combines modifiers, foreground and background colors', () => { 88 | styleText( 89 | 'Burpless 26', 90 | 'redBright', 91 | 'bgBlueBright', 92 | 'bold' 93 | ).should.containEql( 94 | '\u001b[91m\u001b[104m\u001b[1mBurpless 26\u001b[22m\u001b[49m\u001b[39m' 95 | ) 96 | }) 97 | 98 | it('fails with a clear error on unknown styles', () => { 99 | ;(() => 100 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 101 | // @ts-ignore 102 | styleText('Burpless 26', 'ultraviolet')).should.throwError( 103 | 'Unknown style "ultraviolet"' 104 | ) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /src/styleText.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BackgroundColor, 3 | bgColor, 4 | color, 5 | CSPair, 6 | ForegroundColor, 7 | modifier, 8 | Modifier, 9 | } from 'ansi-styles' 10 | 11 | export type TextStyle = 12 | | keyof BackgroundColor 13 | | keyof ForegroundColor 14 | | keyof Modifier 15 | 16 | type StyleFunction = (text: string) => string 17 | const styleDefs: { [key in TextStyle]: CSPair } = { 18 | ...bgColor, 19 | ...color, 20 | ...modifier, 21 | } 22 | 23 | export const styleText = (text: string, ...styles: TextStyle[]): string => { 24 | validateStyles(styles) 25 | return applyStyles(...styles)(text) 26 | } 27 | 28 | const validateStyles = (styles: TextStyle[]) => { 29 | styles.forEach((style) => { 30 | if (!(style in styleDefs)) throw new Error(`Unknown style "${style}"`) 31 | }) 32 | } 33 | 34 | const applyStyles = (...styles: TextStyle[]): StyleFunction => 35 | styles.reduce( 36 | (fn, style) => (text) => 37 | fn(`${styleDefs[style].open}${text}${styleDefs[style].close}`), 38 | (text) => text 39 | ) 40 | -------------------------------------------------------------------------------- /src/theme.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | import { styleText } from './styleText' 3 | 4 | import { 5 | IndentStyleThemeItem, 6 | makeTheme, 7 | ThemeItem, 8 | ThemeStyles, 9 | } from './theme' 10 | 11 | describe('Theme', () => { 12 | let styleThemeItem: IndentStyleThemeItem 13 | 14 | beforeEach(() => { 15 | const styles: ThemeStyles = { 16 | [ThemeItem.DataTable]: ['bgRed'], 17 | [ThemeItem.DataTableBorder]: ['red'], 18 | [ThemeItem.DataTableContent]: ['blue'], 19 | [ThemeItem.DocStringContent]: ['green'], 20 | [ThemeItem.DocStringDelimiter]: ['red'], 21 | [ThemeItem.FeatureDescription]: ['white'], 22 | [ThemeItem.FeatureKeyword]: ['red'], 23 | [ThemeItem.FeatureName]: ['yellow'], 24 | [ThemeItem.Location]: ['bgWhite', 'black'], 25 | [ThemeItem.RuleKeyword]: ['green'], 26 | [ThemeItem.RuleName]: ['bgRed'], 27 | [ThemeItem.ScenarioKeyword]: ['blue'], 28 | [ThemeItem.ScenarioName]: ['blue', 'underline'], 29 | [ThemeItem.StepKeyword]: ['magenta'], 30 | [ThemeItem.StepMessage]: ['bgCyan'], 31 | [ThemeItem.StepStatus]: ['bgRed'], 32 | [ThemeItem.StepText]: ['bgYellow'], 33 | [ThemeItem.Tag]: ['bgRed'], 34 | } 35 | styleThemeItem = makeTheme(styles).indentStyleText 36 | }) 37 | 38 | it('applies styles to Feature keywords', () => { 39 | styleThemeItem( 40 | 0, 41 | ThemeItem.FeatureKeyword, 42 | 'Fonctionnalité:' 43 | ).should.containEql(styleText('Fonctionnalité:', 'red')) 44 | }) 45 | 46 | it('applies styles to Feature names', () => { 47 | styleThemeItem(0, ThemeItem.FeatureName, 'my feature').should.containEql( 48 | styleText('my feature', 'yellow') 49 | ) 50 | }) 51 | 52 | it('applies styles to feature descriptions', () => { 53 | styleThemeItem( 54 | 2, 55 | ThemeItem.FeatureDescription, 56 | 'This is some\ndescription...' 57 | ).should.containEql( 58 | ` ${styleText('This is some', 'white')}\n` + 59 | ` ${styleText('description...', 'white')}` 60 | ) 61 | }) 62 | 63 | it('applies styles to locations', () => { 64 | styleThemeItem( 65 | 0, 66 | ThemeItem.Location, 67 | '# path/to/file.feature:12' 68 | ).should.containEql( 69 | styleText('# path/to/file.feature:12', 'bgWhite', 'black') 70 | ) 71 | }) 72 | 73 | it('applies styles to Rule keywords', () => { 74 | styleThemeItem(0, ThemeItem.RuleKeyword, 'Règle:').should.containEql( 75 | styleText('Règle:', 'green') 76 | ) 77 | }) 78 | 79 | it('applies styles to Rule names', () => { 80 | styleThemeItem(0, ThemeItem.RuleName, 'my rules').should.containEql( 81 | styleText('my rules', 'bgRed') 82 | ) 83 | }) 84 | 85 | it('applies styles to Scenario keywords', () => { 86 | styleThemeItem(0, ThemeItem.ScenarioKeyword, 'Scénario:').should.containEql( 87 | styleText('Scénario:', 'blue') 88 | ) 89 | }) 90 | 91 | it('applies styles to Scenario names', () => { 92 | styleThemeItem(0, ThemeItem.ScenarioName, 'my scenario').should.containEql( 93 | styleText('my scenario', 'blue', 'underline') 94 | ) 95 | }) 96 | 97 | it('applies styles to Step keywords', () => { 98 | styleThemeItem(0, ThemeItem.StepKeyword, 'Etant donné').should.containEql( 99 | styleText('Etant donné', 'magenta') 100 | ) 101 | }) 102 | 103 | it('applies styles to Step text', () => { 104 | styleThemeItem(0, ThemeItem.StepText, 'some cucumbers').should.containEql( 105 | styleText('some cucumbers', 'bgYellow') 106 | ) 107 | }) 108 | 109 | it('applies styles to Step statuses', () => { 110 | styleThemeItem( 111 | 0, 112 | ThemeItem.StepStatus, 113 | '? undefined step' 114 | ).should.containEql(styleText('? undefined step', 'bgRed')) 115 | }) 116 | 117 | it('applies styles to Step messages', () => { 118 | styleThemeItem(0, ThemeItem.StepMessage, 'step message').should.containEql( 119 | styleText('step message', 'bgCyan') 120 | ) 121 | }) 122 | 123 | it('applies styles to DocString content', () => { 124 | styleThemeItem( 125 | 0, 126 | ThemeItem.DocStringContent, 127 | 'this is some docstring' 128 | ).should.containEql(styleText('this is some docstring', 'green')) 129 | }) 130 | 131 | it('applies styles to DocString delimiters', () => { 132 | styleThemeItem(0, ThemeItem.DocStringDelimiter, '"""').should.containEql( 133 | styleText('"""', 'red') 134 | ) 135 | }) 136 | 137 | it('applies styles to DataTable', () => { 138 | styleThemeItem( 139 | 0, 140 | ThemeItem.DataTable, 141 | '| foo | bar |\n| baz | oof |' 142 | ).should.containEql( 143 | `${styleText('| foo | bar |', 'bgRed')}\n${styleText( 144 | '| baz | oof |', 145 | 'bgRed' 146 | )}` 147 | ) 148 | }) 149 | 150 | it('applies styles to DataTable borders', () => { 151 | styleThemeItem(0, ThemeItem.DataTableBorder, '|').should.containEql( 152 | styleText('|', 'red') 153 | ) 154 | }) 155 | 156 | it('applies styles to DataTable content', () => { 157 | styleThemeItem(0, ThemeItem.DataTableContent, 'foo').should.containEql( 158 | styleText('foo', 'blue') 159 | ) 160 | }) 161 | 162 | it('applies styles to feature tags', () => { 163 | styleThemeItem(0, ThemeItem.Tag, '@someTag').should.containEql( 164 | styleText('@someTag', 'bgRed') 165 | ) 166 | }) 167 | 168 | it('concatenates multiple strings', () => { 169 | styleThemeItem( 170 | 0, 171 | ThemeItem.StepKeyword, 172 | 'Etant', 173 | ' donné', 174 | ' que' 175 | ).should.containEql(styleText('Etant donné que', 'magenta')) 176 | }) 177 | 178 | it('fails when applying styles to unknown theme items', () => { 179 | ;(() => 180 | styleThemeItem( 181 | 0, 182 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 183 | // @ts-ignore 184 | 'unknown theme item', 185 | 'text' 186 | )).should.throw() 187 | }) 188 | 189 | it('fails when making a theme with unknown theme items', () => { 190 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 191 | // @ts-ignore 192 | ;(() => makeTheme({ 'random item': ['red'] })).should.throwError( 193 | 'Unknown theme item "random item"' 194 | ) 195 | }) 196 | 197 | it('defaults to unstyled items', () => { 198 | styleThemeItem = makeTheme({}).indentStyleText 199 | styleThemeItem(0, ThemeItem.FeatureKeyword, 'Feature').should.eql('Feature') 200 | styleThemeItem(0, ThemeItem.RuleKeyword, 'Rule').should.eql('Rule') 201 | styleThemeItem(0, ThemeItem.ScenarioKeyword, 'Scenario').should.eql( 202 | 'Scenario' 203 | ) 204 | styleThemeItem(0, ThemeItem.StepKeyword, 'Given').should.eql('Given') 205 | }) 206 | 207 | it('allows some defined and some missing styles', () => { 208 | styleThemeItem = makeTheme({ 209 | [ThemeItem.FeatureKeyword]: ['red'], 210 | }).indentStyleText 211 | styleThemeItem(0, ThemeItem.FeatureKeyword, 'Feature').should.containEql( 212 | styleText('Feature', 'red') 213 | ) 214 | styleThemeItem(0, ThemeItem.RuleKeyword, 'Rule').should.eql('Rule') 215 | styleThemeItem(0, ThemeItem.ScenarioKeyword, 'Scenario').should.eql( 216 | 'Scenario' 217 | ) 218 | styleThemeItem(0, ThemeItem.StepKeyword, 'Given').should.eql('Given') 219 | }) 220 | }) 221 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { indentStyleText } from './indentStyleText' 2 | import { TextStyle } from './styleText' 3 | 4 | export enum ThemeItem { 5 | DataTable = 'datatable', 6 | DataTableBorder = 'datatable border', 7 | DataTableContent = 'datatable content', 8 | DocStringContent = 'docstring content', 9 | DocStringDelimiter = 'docstring delimiter', 10 | FeatureDescription = 'feature description', 11 | FeatureKeyword = 'feature keyword', 12 | FeatureName = 'feature name', 13 | Location = 'location', 14 | RuleKeyword = 'rule keyword', 15 | RuleName = 'rule name', 16 | ScenarioKeyword = 'scenario keyword', 17 | ScenarioName = 'scenario name', 18 | StepKeyword = 'step keyword', 19 | StepMessage = 'step message', 20 | StepStatus = 'step status', 21 | StepText = 'step text', 22 | Tag = 'tag', 23 | } 24 | export type ThemeStyles = { [key in ThemeItem]: TextStyle[] } 25 | 26 | const unstyledTheme: ThemeStyles = { 27 | [ThemeItem.DataTable]: [], 28 | [ThemeItem.DataTableBorder]: [], 29 | [ThemeItem.DataTableContent]: [], 30 | [ThemeItem.DocStringContent]: [], 31 | [ThemeItem.DocStringDelimiter]: [], 32 | [ThemeItem.FeatureDescription]: [], 33 | [ThemeItem.FeatureKeyword]: [], 34 | [ThemeItem.FeatureName]: [], 35 | [ThemeItem.Location]: [], 36 | [ThemeItem.RuleKeyword]: [], 37 | [ThemeItem.RuleName]: [], 38 | [ThemeItem.ScenarioKeyword]: [], 39 | [ThemeItem.ScenarioName]: [], 40 | [ThemeItem.StepKeyword]: [], 41 | [ThemeItem.StepMessage]: [], 42 | [ThemeItem.StepStatus]: [], 43 | [ThemeItem.StepText]: [], 44 | [ThemeItem.Tag]: [], 45 | } 46 | 47 | export const makeTheme = (styles: Partial): ThemeHelpers => { 48 | const validateItemExists = (item: string) => { 49 | if (!Object.values(ThemeItem).includes(item as ThemeItem)) 50 | throw new Error(`Unknown theme item "${item}"`) 51 | } 52 | 53 | Object.keys(styles).forEach(validateItemExists) 54 | 55 | return { 56 | indentStyleText: (indent: number, item: ThemeItem, ...text: string[]) => { 57 | validateItemExists(item) 58 | return indentStyleText( 59 | indent, 60 | text.join(''), 61 | { ...unstyledTheme, ...styles }[item] 62 | ) 63 | }, 64 | } 65 | } 66 | 67 | export type IndentStyleThemeItem = ( 68 | indent: number, 69 | item: ThemeItem, 70 | ...text: string[] 71 | ) => string 72 | export type ThemeHelpers = { 73 | indentStyleText: IndentStyleThemeItem 74 | } 75 | -------------------------------------------------------------------------------- /test/background.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Background', () => { 6 | it('does not log backgrounds', async () => { 7 | const result = await run('background.feature') 8 | result.should.startWith( 9 | 'Feature: Background # test/features/background.feature:1\n' + 10 | '\n' + 11 | ' Scenario: Background scenario # test/features/background.feature:6\n' + 12 | ' Given noop\n' + 13 | ' When noop\n' + 14 | ' Then noop\n' + 15 | '\n' + 16 | ' Scenario Outline: Background scenario outline # test/features/background.feature:10\n' + 17 | ' Given noop\n' + 18 | ' When noop "bar"\n' + 19 | ' Then noop\n' + 20 | '\n' + 21 | ' Scenario Outline: Background scenario outline # test/features/background.feature:10\n' + 22 | ' Given noop\n' + 23 | ' When noop "baz"\n' + 24 | ' Then noop' 25 | ) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/data-table.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Data Table', () => { 6 | it('logs data tables', async () => { 7 | const result = await run('data-table.feature') 8 | result.should.containEql( 9 | ' When data table:\n' + 10 | ' │ foo │ bar │\n' + 11 | ' │ lorem │ ipsum │\n' 12 | ) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/description.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Description', () => { 6 | it('logs feature descriptions', async () => { 7 | const result = await run('description.feature') 8 | result.should.startWith( 9 | 'Feature: Description # test/features/description.feature:1\n' + 10 | '\n' + 11 | ' **I like**\n' + 12 | ' To describe\n' + 13 | ' My _features_\n' + 14 | '\n' 15 | ) 16 | }) 17 | 18 | it('does not log scenario descriptions', async () => { 19 | const result = await run('description.feature') 20 | result.should.containEql( 21 | ' Scenario: Description scenario # test/features/description.feature:7\n' + 22 | ' When noop\n' + 23 | ' Then noop\n' + 24 | '\n' + 25 | ' Scenario Outline: Description scenario outline # test/features/description.feature:14\n' + 26 | ' When noop "bar"\n' + 27 | ' Then noop\n' + 28 | '\n' + 29 | ' Scenario Outline: Description scenario outline # test/features/description.feature:14\n' + 30 | ' When noop "baz"\n' + 31 | ' Then noop' 32 | ) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/doc-string.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Doc String', () => { 6 | it('logs doc strings', async () => { 7 | const result = await run('doc-string.feature') 8 | result.should.containEql( 9 | ' When doc string:\n' + 10 | ' """\n' + 11 | ' foo\n' + 12 | ' bar\n' + 13 | ' """\n' + 14 | ' Then noop\n' 15 | ) 16 | }) 17 | 18 | it('preserves doc string indentation', async () => { 19 | const result = await run('doc-string.feature') 20 | result.should.containEql( 21 | ' When doc string:\n' + 22 | ' """\n' + 23 | ' foo\n' + 24 | ' bar\n' + 25 | ' baz\n' + 26 | ' /foo\n' + 27 | ' """\n' 28 | ) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/exec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IConfiguration, 3 | loadConfiguration, 4 | runCucumber, 5 | } from '@cucumber/cucumber/api' 6 | import { glob } from 'glob' 7 | import { join } from 'path' 8 | import { PassThrough } from 'stream' 9 | import * as streamToString from 'stream-to-string' 10 | 11 | import { ThemeStyles } from '../src/theme' 12 | 13 | type FormatOptions = { 14 | colorsEnabled: boolean 15 | theme: Partial 16 | } 17 | 18 | export const run = async ( 19 | fileName: string, 20 | cucumberOptions: Partial = {}, 21 | formatOptions: Partial = {}, 22 | throws = false 23 | ): Promise => { 24 | // clear require cache for support code 25 | const matches = await glob('features/support/*', { 26 | absolute: true, 27 | cwd: __dirname, 28 | }) 29 | matches.forEach((match) => delete require.cache[match]) 30 | 31 | const configuration: Partial = { 32 | ...cucumberOptions, 33 | format: [join(__dirname, '..', 'src')], 34 | formatOptions: { 35 | colorsEnabled: false, 36 | theme: {}, 37 | ...formatOptions, 38 | }, 39 | paths: [join('test', 'features', fileName)], 40 | publishQuiet: true, 41 | require: [join(__dirname, 'features')], 42 | } 43 | const { runConfiguration } = await loadConfiguration({ 44 | provided: configuration, 45 | }) 46 | const stdout = new PassThrough() 47 | const stderr = new PassThrough() 48 | try { 49 | await runCucumber(runConfiguration, { 50 | cwd: join(__dirname, '..', '..'), 51 | stdout, 52 | stderr, 53 | }) 54 | } catch (ex) { 55 | if (throws) { 56 | throw ex 57 | } 58 | } 59 | stdout.end() 60 | stderr.end() 61 | const result = await streamToString(stdout) 62 | return result.replace(/\d+m\d+\.\d+s/g, '0m00.000s') 63 | } 64 | -------------------------------------------------------------------------------- /test/feature.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Feature', () => { 6 | it('logs feature names and inserts new lines between scenarios and features', async () => { 7 | const result = await run('*.feature', { name: ['Feature \\d'] }) 8 | result.should.startWith( 9 | 'Feature: The Feature # test/features/feature.feature:1\n' + 10 | '\n' + 11 | ' Scenario: Feature 1 # test/features/feature.feature:7\n' + 12 | ' When noop\n' + 13 | ' Then noop\n' + 14 | '\n' + 15 | 'Feature: Feature # test/features/feature2.feature:1\n' + 16 | '\n' + 17 | ' Scenario: Feature 2 # test/features/feature2.feature:3\n' + 18 | ' When noop\n' + 19 | ' Then noop\n' + 20 | '\n' + 21 | ' Scenario: Feature 3 # test/features/feature2.feature:7\n' + 22 | ' When noop\n' + 23 | ' Then noop\n' 24 | ) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/features/background.feature: -------------------------------------------------------------------------------- 1 | Feature: Background 2 | 3 | Background: Background name 4 | Given noop 5 | 6 | Scenario: Background scenario 7 | When noop 8 | Then noop 9 | 10 | Scenario Outline: Background scenario outline 11 | When noop "" 12 | Then noop 13 | 14 | Examples: Background examples 15 | | foo | 16 | | bar | 17 | | baz | 18 | -------------------------------------------------------------------------------- /test/features/data-table.feature: -------------------------------------------------------------------------------- 1 | Feature: Data Table 2 | 3 | Scenario: Data Table 4 | When data table: 5 | | foo | bar | 6 | | lorem | ipsum | 7 | Then noop 8 | -------------------------------------------------------------------------------- /test/features/description.feature: -------------------------------------------------------------------------------- 1 | Feature: Description 2 | 3 | **I like** 4 | To describe 5 | My _features_ 6 | 7 | Scenario: Description scenario 8 | 9 | A scenario description 10 | 11 | When noop 12 | Then noop 13 | 14 | Scenario Outline: Description scenario outline 15 | 16 | A scenario outline description 17 | 18 | When noop "" 19 | Then noop 20 | 21 | Examples: Description examples 22 | 23 | An examples description 24 | 25 | | foo | 26 | | bar | 27 | | baz | 28 | -------------------------------------------------------------------------------- /test/features/doc-string.feature: -------------------------------------------------------------------------------- 1 | Feature: Doc String 2 | 3 | Scenario: Doc String 4 | When doc string: 5 | """ 6 | foo 7 | bar 8 | """ 9 | Then noop 10 | 11 | Scenario: Doc String with indentation 12 | When doc string: 13 | """ 14 | foo 15 | bar 16 | baz 17 | /foo 18 | """ -------------------------------------------------------------------------------- /test/features/feature.feature: -------------------------------------------------------------------------------- 1 | Feature: The Feature 2 | 3 | Scenario: Feature name 4 | When noop 5 | Then noop 6 | 7 | Scenario: Feature 1 8 | When noop 9 | Then noop 10 | -------------------------------------------------------------------------------- /test/features/feature2.feature: -------------------------------------------------------------------------------- 1 | Feature: Feature 2 | 3 | Scenario: Feature 2 4 | When noop 5 | Then noop 6 | 7 | Scenario: Feature 3 8 | When noop 9 | Then noop 10 | -------------------------------------------------------------------------------- /test/features/fr.feature: -------------------------------------------------------------------------------- 1 | # language: fr 2 | Fonctionnalité: Nom de la Fonctionnalité 3 | 4 | Scénario: Nom du Scénario 5 | Quand noop 6 | Alors noop 7 | -------------------------------------------------------------------------------- /test/features/hook.feature: -------------------------------------------------------------------------------- 1 | Feature: Hook 2 | 3 | @before @after 4 | Scenario: Hook 5 | When noop 6 | Then noop 7 | -------------------------------------------------------------------------------- /test/features/ru.feature: -------------------------------------------------------------------------------- 1 | # language: ru 2 | Функция: Функция Name 3 | 4 | Сценарий: Сценарий name 5 | Когда noop 6 | Тогда noop 7 | -------------------------------------------------------------------------------- /test/features/rule-background.feature: -------------------------------------------------------------------------------- 1 | Feature: Rule background 2 | 3 | Rule: the rule 4 | 5 | Background: 6 | Given noop 7 | 8 | Scenario: Rule 1 scenario 9 | Given noop -------------------------------------------------------------------------------- /test/features/rule.feature: -------------------------------------------------------------------------------- 1 | Feature: Rule 2 | 3 | Scenario: No rule top scenario 4 | Given noop 5 | 6 | Rule: first rule 7 | 8 | Scenario: Rule 1 scenario 9 | Given noop 10 | 11 | Rule: second rule 12 | 13 | Scenario: Rule 2 scenario 14 | Given noop -------------------------------------------------------------------------------- /test/features/scenario-outline.feature: -------------------------------------------------------------------------------- 1 | Feature: Scenario Outline 2 | 3 | Scenario Outline: Scenario outline 4 | When noop "" 5 | 6 | Examples: 7 | | foo | 8 | | bar | 9 | | baz | 10 | -------------------------------------------------------------------------------- /test/features/scenario.feature: -------------------------------------------------------------------------------- 1 | Feature: Scenario 2 | 3 | Scenario: Scenario name 4 | When noop 5 | Then noop 6 | 7 | Scenario: Scenario 1 8 | When noop 9 | Then noop 10 | 11 | Scenario: Scenario 2 12 | When noop 13 | Then noop 14 | -------------------------------------------------------------------------------- /test/features/step.feature: -------------------------------------------------------------------------------- 1 | @tag 2 | Feature: Step 3 | 4 | Description 5 | 6 | @stag 7 | Scenario: Step name 8 | Given noop 9 | When noop 10 | Then noop 11 | And noop 12 | But noop 13 | 14 | Scenario: Ambiguous step 15 | When ambiguous 16 | 17 | Scenario: Failed step 18 | When failed 19 | 20 | Scenario: Passed step 21 | When passed 22 | 23 | Scenario: Pending step 24 | When pending 25 | 26 | Scenario: Skipped step 27 | When skipped 28 | 29 | Scenario: Undefined step 30 | When undefined 31 | 32 | Scenario: DocString 33 | Given doc string: 34 | """ 35 | Some multiline 36 | Text 37 | """ 38 | 39 | Scenario: DataTable 40 | Given data table: 41 | | a | b | 42 | | c | d | 43 | 44 | Rule: some rule 45 | 46 | Scenario: scenario under rule 47 | Given noop -------------------------------------------------------------------------------- /test/features/support/World.ts: -------------------------------------------------------------------------------- 1 | import { blue } from 'colors/safe' 2 | 3 | export class World { 4 | someWorldMethod(): void { 5 | console.log(blue('WORLD')) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/features/support/hooks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | After, 3 | AfterAll, 4 | Before, 5 | BeforeAll, 6 | setWorldConstructor, 7 | } from '@cucumber/cucumber' 8 | 9 | import { World } from './World' 10 | 11 | setWorldConstructor(World) 12 | 13 | if (process.argv.some((arg) => arg === 'test/features/hook.feature')) { 14 | BeforeAll(() => { 15 | console.log('[[[BeforeAll]]]') 16 | }) 17 | AfterAll(() => { 18 | console.log('[[[AfterAll]]]') 19 | }) 20 | } 21 | 22 | Before('@before', () => { 23 | console.log('[[[Before]]]') 24 | }) 25 | 26 | After('@after', () => { 27 | console.log('[[[After]]]') 28 | }) 29 | -------------------------------------------------------------------------------- /test/features/support/steps.ts: -------------------------------------------------------------------------------- 1 | import { DataTable, defineStep } from '@cucumber/cucumber' 2 | 3 | import { World } from './World' 4 | 5 | const noop = () => {} 6 | 7 | defineStep('noop', noop) 8 | defineStep('noop {string}', (_: string) => {}) 9 | defineStep('ambiguous', noop) 10 | defineStep('ambiguous', noop) 11 | defineStep('failed', () => { 12 | throw new Error('FAILED') 13 | }) 14 | defineStep('passed', noop) 15 | defineStep('pending', () => 'pending') 16 | defineStep('skipped', () => 'skipped') 17 | defineStep('doc string:', (_: string) => {}) 18 | defineStep('data table:', (_: DataTable) => {}) 19 | defineStep('world', function (this: World) { 20 | this.someWorldMethod() 21 | }) 22 | -------------------------------------------------------------------------------- /test/features/tag.feature: -------------------------------------------------------------------------------- 1 | @feature @tag 2 | Feature: Tag 3 | 4 | Scenario: Feature tag 5 | When noop 6 | Then noop 7 | 8 | @scenario 9 | Scenario: Scenario tag 10 | When noop 11 | Then noop 12 | 13 | @scenario-outline 14 | Scenario Outline: Scenario outline tag 15 | When noop "" 16 | 17 | @example 18 | Examples: 19 | | foo | 20 | | bar | 21 | | baz | 22 | -------------------------------------------------------------------------------- /test/features/world.feature: -------------------------------------------------------------------------------- 1 | Feature: World 2 | 3 | Scenario: World 4 | When world 5 | Then noop 6 | -------------------------------------------------------------------------------- /test/hook.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Hook', () => { 6 | it('does not log hooks', async () => { 7 | const result = await run('hook.feature') 8 | result.should.startWith( 9 | 'Feature: Hook # test/features/hook.feature:1\n' + 10 | '\n' + 11 | ' @before @after\n' + 12 | ' Scenario: Hook # test/features/hook.feature:4\n' + 13 | ' When noop\n' + 14 | ' Then noop\n' 15 | ) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/i18n.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Internationalization', () => { 6 | it('logs French', async () => { 7 | const result = await run('fr.feature', { 8 | name: ['Nom du Scénario'], 9 | }) 10 | result.should.startWith( 11 | 'Fonctionnalité: Nom de la Fonctionnalité # test/features/fr.feature:2\n' + 12 | '\n' + 13 | ' Scénario: Nom du Scénario # test/features/fr.feature:4\n' + 14 | ' Quand noop\n' + 15 | ' Alors noop\n' 16 | ) 17 | }) 18 | 19 | it('logs Russian', async () => { 20 | const result = await run('ru.feature', { name: ['Сценарий name'] }) 21 | result.should.startWith( 22 | 'Функция: Функция Name # test/features/ru.feature:2\n' + 23 | '\n' + 24 | ' Сценарий: Сценарий name # test/features/ru.feature:4\n' + 25 | ' Когда noop\n' + 26 | ' Тогда noop\n' 27 | ) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/rule.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Rule', () => { 6 | it('logs rules', async () => { 7 | const expectedOutput = 8 | 'Feature: Rule # test/features/rule.feature:1\n\ 9 | \n\ 10 | Scenario: No rule top scenario # test/features/rule.feature:3\n\ 11 | Given noop\n\ 12 | \n\ 13 | Rule: first rule\n\ 14 | \n\ 15 | Scenario: Rule 1 scenario # test/features/rule.feature:8\n\ 16 | Given noop\n\ 17 | \n\ 18 | Rule: second rule\n\ 19 | \n\ 20 | Scenario: Rule 2 scenario # test/features/rule.feature:13\n\ 21 | Given noop\n' 22 | const result = await run('rule.feature') 23 | result.should.startWith(expectedOutput) 24 | }) 25 | 26 | it('logs background steps in rules', async () => { 27 | const expectedOutput = 28 | 'Feature: Rule background # test/features/rule-background.feature:1\n\ 29 | \n\ 30 | Rule: the rule\n\ 31 | \n\ 32 | Scenario: Rule 1 scenario # test/features/rule-background.feature:8\n\ 33 | Given noop\n\ 34 | Given noop\n' 35 | const result = await run('rule-background.feature') 36 | result.should.startWith(expectedOutput) 37 | }) 38 | 39 | it('offsets the scenario indentation', async () => { 40 | const result = await run('rule*.feature') 41 | result.should.startWith( 42 | 'Feature: Rule background # test/features/rule-background.feature:1\n\ 43 | \n\ 44 | Rule: the rule\n\ 45 | \n\ 46 | Scenario: Rule 1 scenario # test/features/rule-background.feature:8\n\ 47 | Given noop\n\ 48 | Given noop\n\ 49 | \n\ 50 | Feature: Rule' 51 | ) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/scenario-outline.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Scenario Outline', () => { 6 | it('logs scenario outlines', async () => { 7 | const result = await run('scenario-outline.feature', { 8 | name: ['Scenario outline'], 9 | }) 10 | result.should.containEql( 11 | // TODO: use the example location when running a scenario outline 12 | 'Feature: Scenario Outline # test/features/scenario-outline.feature:1\n' + 13 | '\n' + 14 | ' Scenario Outline: Scenario outline # test/features/scenario-outline.feature:3\n' + 15 | ' When noop "bar"\n' + 16 | '\n' + 17 | ' Scenario Outline: Scenario outline # test/features/scenario-outline.feature:3\n' + 18 | ' When noop "baz"' 19 | ) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/scenario.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Scenario', () => { 6 | it('logs scenario names', async () => { 7 | const result = await run('scenario.feature', { name: ['Scenario name'] }) 8 | result.should.containEql( 9 | ' Scenario: Scenario name # test/features/scenario.feature:3\n' 10 | ) 11 | }) 12 | 13 | it('logs new lines between scenarios', async () => { 14 | const result = await run('scenario.feature', { name: ['Scenario \\d'] }) 15 | result.should.containEql( 16 | 'Feature: Scenario # test/features/scenario.feature:1\n' + 17 | '\n' + 18 | ' Scenario: Scenario 1 # test/features/scenario.feature:7\n' + 19 | ' When noop\n' + 20 | ' Then noop\n' + 21 | '\n' + 22 | ' Scenario: Scenario 2 # test/features/scenario.feature:11\n' + 23 | ' When noop\n' + 24 | ' Then noop\n' 25 | ) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/step.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Step', () => { 6 | it('logs steps', async () => { 7 | const result = await run('step.feature', { name: ['Step name'] }) 8 | result.should.containEql( 9 | ' Given noop\n' + 10 | ' When noop\n' + 11 | ' Then noop\n' + 12 | ' And noop\n' + 13 | ' But noop\n' 14 | ) 15 | }) 16 | 17 | it('warns about ambiguous steps', async () => { 18 | const result = await run('step.feature', { name: ['Ambiguous step'] }) 19 | result.should.containEql(' When ambiguous\n' + ' ✖ ambiguous\n') 20 | }) 21 | 22 | it('warns about failed steps', async () => { 23 | const result = await run('step.feature', { name: ['Failed step'] }) 24 | result.should.containEql(' When failed\n' + ' ✖ failed\n') 25 | }) 26 | 27 | it('logs passed steps', async () => { 28 | const result = await run('step.feature', { name: ['Passed step'] }) 29 | result.should.match(/ {4}When passed\n(?! {4})/) 30 | }) 31 | 32 | it('logs pending steps', async () => { 33 | const result = await run('step.feature', { name: ['Pending step'] }) 34 | result.should.containEql(' When pending\n' + ' ? pending\n') 35 | }) 36 | 37 | it('logs skipped steps', async () => { 38 | const result = await run('step.feature', { name: ['Skipped step'] }) 39 | result.should.containEql(' When skipped\n' + ' - skipped\n') 40 | }) 41 | 42 | it('logs undefined steps', async () => { 43 | const result = await run('step.feature', { name: ['Undefined step'] }) 44 | result.should.containEql(' When undefined\n' + ' ? undefined\n') 45 | }) 46 | 47 | it('logs errors', async () => { 48 | const result = await run('step.feature', { name: ['Failed step'] }) 49 | result.should.containEql( 50 | ' When failed\n' + 51 | ' ✖ failed\n' + 52 | ' Error: FAILED\n' + 53 | ' at World' 54 | ) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/summary.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Summary', () => { 6 | it('logs empty run summaries', async () => { 7 | const result = await run('feature.feature', { tags: '@empty' }) 8 | result.should.equal( 9 | '0 scenarios\n' + '0 steps\n' + '0m00.000s (executing steps: 0m00.000s)\n' 10 | ) 11 | }) 12 | 13 | it('logs summaries after a new line', async () => { 14 | const result = await run('feature.feature', { name: ['Feature name'] }) 15 | result.should.equal( 16 | 'Feature: The Feature # test/features/feature.feature:1\n' + 17 | '\n' + 18 | ' Scenario: Feature name # test/features/feature.feature:3\n' + 19 | ' When noop\n' + 20 | ' Then noop\n' + 21 | '\n' + 22 | '1 scenario (1 passed)\n' + 23 | '2 steps (2 passed)\n' + 24 | '0m00.000s (executing steps: 0m00.000s)\n' 25 | ) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/tag.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { run } from './exec' 4 | 5 | describe('Tag', () => { 6 | it('log feature tags', async () => { 7 | const result = await run('tag.feature', { name: ['Feature tag'] }) 8 | result.should.startWith('@feature @tag\n' + 'Feature: Tag') 9 | }) 10 | 11 | it('logs scenario tags', async () => { 12 | const result = await run('tag.feature', { name: ['Scenario tag'] }) 13 | result.should.containEql( 14 | ' @feature @tag @scenario\n' + ' Scenario: Scenario tag' 15 | ) 16 | }) 17 | 18 | it('logs scenario outline tags', async () => { 19 | const result = await run('tag.feature', { 20 | name: ['Scenario outline tag'], 21 | }) 22 | result.should.containEql( 23 | ' @feature @tag @scenario-outline @example\n' + 24 | ' Scenario Outline: Scenario outline tag' 25 | ) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/text-styling.spec.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | 3 | import { indentStyleText } from '../src/indentStyleText' 4 | import { styleText, TextStyle } from '../src/styleText' 5 | import { ThemeItem, ThemeStyles } from '../src/theme' 6 | import { run } from './exec' 7 | 8 | describe('Text styling', () => { 9 | const runColored = ( 10 | fileName: string, 11 | name?: string, 12 | theme?: Partial, 13 | throws = false 14 | ) => 15 | run( 16 | fileName, 17 | { name: name ? [name] : undefined }, 18 | { colorsEnabled: true, theme }, 19 | throws 20 | ) 21 | 22 | it('fails with unknown styles', async () => { 23 | const theme: Partial = { 24 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 25 | // @ts-ignore 26 | [ThemeItem.FeatureKeyword]: ['ultraviolet'], 27 | } 28 | try { 29 | await runColored('step.feature', 'Step name', theme, true) 30 | throw new Error('Should have failed') 31 | } catch (error) { 32 | error.toString().should.containEql('Error: Unknown style "ultraviolet"') 33 | } 34 | }) 35 | 36 | it('fails with unknown theme items', async () => { 37 | const theme: Partial = { 38 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 39 | // @ts-ignore 40 | ['unknown theme item']: ['red'], 41 | } 42 | try { 43 | await runColored('step.feature', 'Step name', theme, true) 44 | throw new Error('Should have failed') 45 | } catch (error) { 46 | error 47 | .toString() 48 | .should.containEql('Error: Unknown theme item "unknown theme item"') 49 | } 50 | }) 51 | 52 | describe('customizable items', () => { 53 | it('styles feature keywords', async () => { 54 | const result = await runColored('feature.feature', 'Feature name', { 55 | [ThemeItem.FeatureKeyword]: ['red', 'italic'], 56 | }) 57 | result.should.containEql(styleText('Feature:', 'red', 'italic')) 58 | }) 59 | 60 | it('styles feature names', async () => { 61 | const result = await runColored('feature.feature', 'Feature name', { 62 | [ThemeItem.FeatureName]: ['yellow', 'italic'], 63 | }) 64 | result.should.containEql(styleText('The Feature', 'yellow', 'italic')) 65 | }) 66 | 67 | it('styles feature descriptions', async () => { 68 | const result = await runColored('description.feature', undefined, { 69 | [ThemeItem.FeatureDescription]: ['bgGreen'], 70 | }) 71 | result.should.containEql( 72 | indentStyleText(2, '**I like**\nTo describe\nMy _features_', [ 73 | 'bgGreen', 74 | ]) 75 | ) 76 | }) 77 | 78 | it('styles feature tags', async () => { 79 | const s = (tag: string) => styleText(tag, 'bgYellow') 80 | const result = await runColored('tag.feature', 'Scenario tag', { 81 | [ThemeItem.Tag]: ['bgYellow'], 82 | }) 83 | result.should.containEql(`${s('@feature')} ${s('@tag')}\n`) 84 | }) 85 | 86 | it('styles feature locations', async () => { 87 | const s = (tag: string) => styleText(tag, 'bgYellow') 88 | const result = await runColored('feature.feature', undefined, { 89 | [ThemeItem.Location]: ['bgYellow'], 90 | }) 91 | result.should.containEql(`${s('# test/features/feature.feature:1')}\n`) 92 | }) 93 | 94 | it('styles rule keywords', async () => { 95 | const result = await runColored('rule.feature', undefined, { 96 | [ThemeItem.RuleKeyword]: ['yellow'], 97 | }) 98 | result.should.containEql(` ${styleText('Rule:', 'yellow')} first rule\n`) 99 | }) 100 | 101 | it('styles rule names', async () => { 102 | const result = await runColored('rule.feature', undefined, { 103 | [ThemeItem.RuleName]: ['green'], 104 | }) 105 | result.should.containEql(` Rule: ${styleText('first rule', 'green')}\n`) 106 | }) 107 | 108 | it('styles scenario keywords', async () => { 109 | const result = await runColored('scenario.feature', 'Scenario name', { 110 | [ThemeItem.ScenarioKeyword]: ['bgYellow'], 111 | }) 112 | result.should.containEql( 113 | `${styleText('Scenario:', 'bgYellow')} Scenario name` 114 | ) 115 | }) 116 | 117 | it('styles scenario names', async () => { 118 | const result = await runColored('scenario.feature', 'Scenario name', { 119 | [ThemeItem.ScenarioName]: ['bgMagenta'], 120 | }) 121 | result.should.containEql( 122 | ` Scenario: ${styleText('Scenario name', 'bgMagenta')}` 123 | ) 124 | }) 125 | 126 | it('styles scenario locations', async () => { 127 | const s = (tag: string) => styleText(tag, 'bgYellow') 128 | const result = await runColored('scenario.feature', 'Scenario name', { 129 | [ThemeItem.Location]: ['bgYellow'], 130 | }) 131 | result.should.containEql( 132 | `Scenario: Scenario name ${s('# test/features/scenario.feature:3')}\n` 133 | ) 134 | }) 135 | 136 | it('styles scenario tags', async () => { 137 | const s = (tag: string) => styleText(tag, 'bgBlue') 138 | const result = await runColored('tag.feature', 'Scenario tag', { 139 | [ThemeItem.Tag]: ['bgBlue'], 140 | }) 141 | result.should.containEql( 142 | ` ${s('@feature')} ${s('@tag')} ${s('@scenario')}\n` 143 | ) 144 | }) 145 | 146 | it('styles step keywords', async () => { 147 | const stepStyles: TextStyle[] = ['bgYellow', 'bold'] 148 | const result = await runColored('step.feature', 'Step name', { 149 | [ThemeItem.StepKeyword]: stepStyles, 150 | }) 151 | result.should.containEql( 152 | ` ${styleText('Given', ...stepStyles)} noop\n` + 153 | ` ${styleText('When', ...stepStyles)} noop\n` + 154 | ` ${styleText('Then', ...stepStyles)} noop\n` 155 | ) 156 | }) 157 | 158 | it('styles step text', async () => { 159 | const stepStyles: TextStyle[] = ['bgYellow', 'bold'] 160 | const result = await runColored('step.feature', 'Step name', { 161 | [ThemeItem.StepText]: stepStyles, 162 | }) 163 | result.should.containEql(styleText('noop', ...stepStyles)) 164 | }) 165 | 166 | it('styles step statuses', async () => { 167 | const stepStyles: TextStyle[] = ['bgWhite'] 168 | const s = (text: string) => styleText(text, ...stepStyles) 169 | const result = await runColored('step.feature', 'Failed step', { 170 | [ThemeItem.StepStatus]: stepStyles, 171 | }) 172 | result.should.containEql( 173 | ` ${s('\u001b[31m✖ failed\u001b[39m')}\n ` 174 | ) 175 | }) 176 | 177 | it('styles step messages', async () => { 178 | const stepStyles: TextStyle[] = ['bgCyan'] 179 | const s = (text: string) => styleText(text, ...stepStyles) 180 | const result = await runColored('step.feature', 'Failed step', { 181 | [ThemeItem.StepMessage]: stepStyles, 182 | }) 183 | result.should.containEql(` ${s('Error: FAILED')}\n `) 184 | }) 185 | 186 | it('styles DocString content and delimiters', async () => { 187 | const result = await runColored('doc-string.feature', undefined, { 188 | [ThemeItem.DocStringDelimiter]: ['green', 'bgYellow'], 189 | [ThemeItem.DocStringContent]: ['red', 'bold'], 190 | }) 191 | result.should.containEql( 192 | `${indentStyleText(6, '"""', ['green', 'bgYellow'])}\n` + 193 | `${indentStyleText(6, 'foo\nbar', ['red', 'bold'])}\n` + 194 | `${indentStyleText(6, '"""', ['green', 'bgYellow'])}\n` 195 | ) 196 | }) 197 | 198 | it('styles DataTables', async () => { 199 | const styles: TextStyle[] = ['green', 'bgYellow'] 200 | const result = await runColored('data-table.feature', undefined, { 201 | [ThemeItem.DataTable]: ['green', 'bgYellow'], 202 | }) 203 | result.should.containEql( 204 | ` ${styleText('│ foo │ bar │', ...styles)}\n` + 205 | ` ${styleText('│ lorem │ ipsum │', ...styles)}\n` 206 | ) 207 | }) 208 | 209 | it('styles DataTable borders', async () => { 210 | const border = styleText('│', 'green', 'bgYellow') 211 | const result = await runColored('data-table.feature', undefined, { 212 | [ThemeItem.DataTableBorder]: ['green', 'bgYellow'], 213 | }) 214 | result.should.containEql( 215 | ` ${border} foo ${border} bar ${border}\n` + 216 | ` ${border} lorem ${border} ipsum ${border}\n` 217 | ) 218 | }) 219 | 220 | it('styles DataTable content', async () => { 221 | const styles: TextStyle[] = ['green', 'bgYellow'] 222 | const result = await runColored('data-table.feature', undefined, { 223 | [ThemeItem.DataTableContent]: ['green', 'bgYellow'], 224 | }) 225 | result.should.containEql( 226 | ` │ ${styleText('foo', ...styles)} │ ${styleText( 227 | 'bar', 228 | ...styles 229 | )} │\n` + 230 | ` │ ${styleText('lorem', ...styles)} │ ${styleText( 231 | 'ipsum', 232 | ...styles 233 | )} │\n` 234 | ) 235 | }) 236 | }) 237 | 238 | describe('non-customizable items (colored by Cucumber)', () => { 239 | it('styles ambiguous steps', async () => { 240 | const result = await runColored('step.feature', 'Ambiguous step') 241 | result.should.containEql(` ${styleText('✖ ambiguous', 'red')}\n`) 242 | }) 243 | 244 | it('styles failed steps', async () => { 245 | const result = await runColored('step.feature', 'Failed step') 246 | result.should.containEql(` ${styleText('✖ failed', 'red')}\n`) 247 | }) 248 | 249 | it('styles pending steps', async () => { 250 | const result = await runColored('step.feature', 'Pending step') 251 | result.should.containEql(` ${styleText('? pending', 'yellow')}\n`) 252 | }) 253 | 254 | it('styles undefined steps', async () => { 255 | const result = await runColored('step.feature', 'Undefined step') 256 | result.should.containEql(` ${styleText('? undefined', 'yellow')}\n`) 257 | }) 258 | 259 | it('styles skipped steps', async () => { 260 | const result = await runColored('step.feature', 'Skipped step') 261 | result.should.containEql(` ${styleText('- skipped', 'cyan')}\n`) 262 | }) 263 | 264 | it('styles errors', async () => { 265 | const result = await runColored('step.feature', 'Failed step') 266 | result.should.containEql(` ${styleText('Error: FAILED', 'red')}`) 267 | }) 268 | }) 269 | 270 | describe('default theme', () => { 271 | let runResult: string 272 | before(async () => (runResult = await runColored('step.feature'))) 273 | 274 | it('styles feature keywords', () => 275 | runResult.should.containEql(styleText('Feature:', 'blueBright', 'bold'))) 276 | 277 | it('styles feature names', () => 278 | runResult.should.containEql(styleText('Step', 'blueBright', 'underline'))) 279 | 280 | it('styles feature tags', () => 281 | runResult.should.containEql(styleText('@tag', 'cyan'))) 282 | 283 | it('styles rule keywords', () => 284 | runResult.should.containEql(styleText('Rule:', 'blueBright', 'bold'))) 285 | 286 | it('styles rule names', () => 287 | runResult.should.containEql( 288 | styleText('some rule', 'blueBright', 'underline') 289 | )) 290 | 291 | it('styles scenario keywords', () => 292 | runResult.should.containEql(styleText('Scenario:', 'cyan', 'bold'))) 293 | 294 | it('styles scenario names', () => 295 | runResult.should.containEql(styleText('Step name', 'cyan', 'underline'))) 296 | 297 | it('styles scenario tags', () => 298 | runResult.should.containEql(styleText('@stag', 'cyan'))) 299 | 300 | it('styles locations', () => 301 | runResult.should.containEql( 302 | styleText('# test/features/step.feature:2', 'dim') 303 | )) 304 | 305 | it('styles descriptions', () => 306 | runResult.should.containEql(styleText('Description', 'gray'))) 307 | 308 | it('styles step keywords', () => { 309 | runResult.should.containEql(styleText('Given', 'cyan')) 310 | runResult.should.containEql(styleText('When', 'cyan')) 311 | runResult.should.containEql(styleText('Then', 'cyan')) 312 | }) 313 | 314 | it('does not style step text', () => { 315 | runResult.should.containEql(styleText('noop')) 316 | }) 317 | 318 | it('styles DocString content and delimiters', () => { 319 | runResult.should.containEql( 320 | `${indentStyleText(6, '"""', ['gray'])}\n` + 321 | `${indentStyleText(6, 'Some multiline\nText', [ 322 | 'gray', 323 | 'italic', 324 | ])}\n` + 325 | `${indentStyleText(6, '"""', ['gray'])}\n` 326 | ) 327 | }) 328 | 329 | it('styles DataTable borders and content', () => { 330 | const d = styleText('│', 'gray') 331 | const s = (text: string) => styleText(text, 'gray', 'italic') 332 | 333 | runResult.should.containEql( 334 | ` ${d} ${s('a')} ${d} ${s('b')} ${d}\n` + 335 | ` ${d} ${s('c')} ${d} ${s('d')} ${d}\n` 336 | ) 337 | }) 338 | }) 339 | }) 340 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "module": "none", 5 | "moduleResolution": "node", 6 | "noImplicitAny": true, 7 | "noImplicitReturns": true, 8 | "noImplicitThis": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "es2019" 13 | 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": [ 17 | "**/node_modules", 18 | ], 19 | "compileOnSave": true 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "outDir": "lib", 7 | }, 8 | "include": [ 9 | "src/**/*", 10 | "test/**/*" 11 | ] 12 | } --------------------------------------------------------------------------------