├── .eslintignore ├── .eslintrc.js ├── .github ├── renovate.json └── workflows │ ├── lint.yml │ └── nodejs.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .yarn └── releases │ └── yarn-4.6.0.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── PostMessage.tsx ├── Reporter.tsx ├── SnapshotStatus.tsx ├── SnapshotSummary.tsx ├── Summary.tsx ├── VerboseTests.tsx ├── __tests__ │ └── index.ts ├── hooks.ts ├── index.ts ├── shared.tsx └── utils.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | parser: require.resolve('@typescript-eslint/parser'), 5 | extends: [ 6 | 'plugin:n/recommended', 7 | 'plugin:@typescript-eslint/eslint-recommended', 8 | 'plugin:react/recommended', 9 | 'prettier', 10 | ], 11 | plugins: ['prettier', 'import', '@typescript-eslint', 'react', 'react-hooks'], 12 | parserOptions: { 13 | ecmaVersion: 2018, 14 | }, 15 | env: { 16 | node: true, 17 | es6: true, 18 | }, 19 | rules: { 20 | '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], 21 | '@typescript-eslint/no-require-imports': 'off', 22 | '@typescript-eslint/prefer-ts-expect-error': 'error', 23 | '@typescript-eslint/ban-types': 'error', 24 | '@typescript-eslint/no-unused-vars': [ 25 | 'error', 26 | { argsIgnorePattern: '^_', caughtErrors: 'all' }, 27 | ], 28 | '@typescript-eslint/consistent-type-imports': [ 29 | 'error', 30 | { fixStyle: 'inline-type-imports', disallowTypeAnnotations: false }, 31 | ], 32 | '@typescript-eslint/no-import-type-side-effects': 'error', 33 | 'no-else-return': 'error', 34 | 'no-negated-condition': 'error', 35 | eqeqeq: ['error', 'smart'], 36 | strict: 'error', 37 | 'prefer-template': 'warn', 38 | 'object-shorthand': ['warn', 'always', { avoidExplicitReturnArrows: true }], 39 | 'prefer-destructuring': [ 40 | 'error', 41 | { VariableDeclarator: { array: true, object: true } }, 42 | ], 43 | 'sort-imports': ['error', { ignoreDeclarationSort: true }], 44 | 'prettier/prettier': 'error', 45 | // TS covers this 46 | 'n/no-missing-import': 'off', 47 | 'n/no-unsupported-features/es-syntax': 'off', 48 | 'n/no-unsupported-features/es-builtins': 'error', 49 | 'import/no-commonjs': 'error', 50 | 'import/no-duplicates': 'error', 51 | 'import/no-extraneous-dependencies': 'error', 52 | 'import/no-unused-modules': 'error', 53 | 'react-hooks/rules-of-hooks': 'error', 54 | 'react-hooks/exhaustive-deps': 'error', 55 | // handled by TS 56 | 'react/prop-types': 'off', 57 | }, 58 | settings: { 59 | react: { 60 | version: 'detect', 61 | }, 62 | }, 63 | overrides: [ 64 | { 65 | files: 'src/**/*', 66 | parserOptions: { 67 | sourceType: 'module', 68 | }, 69 | }, 70 | { 71 | files: ['.eslintrc.js', 'babel.config.js'], 72 | rules: { 73 | 'import/no-commonjs': 'off', 74 | }, 75 | }, 76 | ], 77 | }; 78 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:recommended"], 3 | "lockFileMaintenance": { 4 | "enabled": true, 5 | "automerge": true 6 | }, 7 | "rangeStrategy": "replace", 8 | "postUpdateOptions": ["yarnDedupeHighest"] 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | pull_request: 4 | merge_group: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | commitlint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - uses: wagoid/commitlint-github-action@v6.2.1 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | merge_group: 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test-node: 17 | name: Test on Node.js v${{ matrix.node-version }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | node-version: [18.x, 20.x, 22.x, 23.x] 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: yarn 31 | - name: install 32 | run: yarn 33 | - name: run eslint 34 | run: yarn lint 35 | - name: run prettylint 36 | run: yarn prettylint 37 | - name: run typecheck 38 | run: yarn typecheck 39 | - name: run build 40 | run: yarn build 41 | - name: run tests 42 | run: yarn test --coverage 43 | env: 44 | CI: true 45 | test-os: 46 | name: Test on ${{ matrix.os }} using Node.js LTS 47 | strategy: 48 | fail-fast: false 49 | matrix: 50 | os: [ubuntu-latest, windows-latest, macOS-latest] 51 | runs-on: ${{ matrix.os }} 52 | 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: actions/setup-node@v4 56 | with: 57 | node-version: 'lts/*' 58 | cache: yarn 59 | - name: install 60 | run: yarn 61 | - name: run prettylint 62 | run: yarn prettylint 63 | - name: run typecheck 64 | run: yarn typecheck 65 | - name: run build 66 | run: yarn build 67 | - name: run tests 68 | run: yarn test --coverage 69 | env: 70 | CI: true 71 | release: 72 | if: 73 | # prettier-ignore 74 | ${{ github.event_name == 'push' && (github.event.ref == 'refs/heads/main' || github.event.ref == 'refs/heads/next') }} 75 | needs: 76 | - test-node 77 | - test-os 78 | name: release 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@v4 82 | - uses: actions/setup-node@v4 83 | with: 84 | node-version: 'lts/*' 85 | cache: yarn 86 | - name: install 87 | run: yarn 88 | - name: run build 89 | run: yarn build 90 | - run: yarn semantic-release 91 | env: 92 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 93 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | 5 | .pnp.* 6 | .yarn/* 7 | !.yarn/patches 8 | !.yarn/plugins 9 | !.yarn/releases 10 | !.yarn/sdks 11 | !.yarn/versions 12 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | yarn commitlint -e $HUSKY_GIT_PARAMS 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: true 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.6.0.cjs 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [5.0.0](https://github.com/jest-community/jest-react-reporter/compare/v4.0.1...v5.0.0) (2025-02-15) 2 | 3 | 4 | ### chore 5 | 6 | * upgrade typescript ([#219](https://github.com/jest-community/jest-react-reporter/issues/219)) ([341c2f9](https://github.com/jest-community/jest-react-reporter/commit/341c2f9bf2e78c1d7fb22e2dfac79ce3daedee8d)) 7 | 8 | 9 | ### BREAKING CHANGES 10 | 11 | * Node versions 14, 16 and 21 are no longer supported 12 | 13 | ## [4.0.1](https://github.com/jest-community/jest-react-reporter/compare/v4.0.0...v4.0.1) (2024-05-12) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * update to React 18 ([#223](https://github.com/jest-community/jest-react-reporter/issues/223)) ([47b27fa](https://github.com/jest-community/jest-react-reporter/commit/47b27fa7d08da150c7c0ad450e3871c25d4aeba4)) 19 | 20 | # [4.0.0](https://github.com/jest-community/jest-react-reporter/compare/v3.0.0...v4.0.0) (2022-09-06) 21 | 22 | 23 | ### Features 24 | 25 | * drop node 12 ([49089b5](https://github.com/jest-community/jest-react-reporter/commit/49089b566843aadd80096cf8c6f5d84e6c1d4a7d)) 26 | * update to Jest 29 and support 27-29 in peer dependencies ([#100](https://github.com/jest-community/jest-react-reporter/issues/100)) ([a0aaa14](https://github.com/jest-community/jest-react-reporter/commit/a0aaa14b9ab18690a1d8d29009a37dbf55278c24)) 27 | 28 | 29 | ### BREAKING CHANGES 30 | 31 | * No longer support Node 12 32 | 33 | # [3.0.0](https://github.com/jest-community/jest-react-reporter/compare/v2.0.1...v3.0.0) (2022-02-26) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * **deps:** update jest monorepo to v27 (major) ([#31](https://github.com/jest-community/jest-react-reporter/issues/31)) ([bc9d09f](https://github.com/jest-community/jest-react-reporter/commit/bc9d09f3bfa2672b33498d3607e1c0f91b32ad97)) 39 | * put string inside Text ([3481e50](https://github.com/jest-community/jest-react-reporter/commit/3481e5016349ce32bce10d1ff75bdc3ee171cd7b)) 40 | 41 | 42 | ### Features 43 | 44 | * drop node 10 and 13 ([abccf17](https://github.com/jest-community/jest-react-reporter/commit/abccf17cb29292f8f7ddddd4bce72202456e6fd9)) 45 | * update ink to v3 ([#41](https://github.com/jest-community/jest-react-reporter/issues/41)) ([15cb8b9](https://github.com/jest-community/jest-react-reporter/commit/15cb8b96c1db49128df8f7030a1702993ed82dba)) 46 | 47 | 48 | ### BREAKING CHANGES 49 | 50 | * Remove support for node 10 and 13 51 | 52 | ## [2.0.1](https://github.com/jest-community/jest-react-reporter/compare/v2.0.0...v2.0.1) (2020-05-23) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * disable `esModuleInterop` ([074ff80](https://github.com/jest-community/jest-react-reporter/commit/074ff80f790e0dc8d741bb9893d59d31825ffe92)) 58 | 59 | # [2.0.0](https://github.com/jest-community/jest-react-reporter/compare/v1.1.10...v2.0.0) (2020-05-23) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * build with (patched) ncc ([146370d](https://github.com/jest-community/jest-react-reporter/commit/146370dd6a3a1908766c98b33434261b185ef4c3)) 65 | * update dependencies ([f2e12da](https://github.com/jest-community/jest-react-reporter/commit/f2e12da36d9203af4763e8c805548160e8bd92e6)) 66 | * use `import type` syntax ([8ff117c](https://github.com/jest-community/jest-react-reporter/commit/8ff117c93c7a9a05b7d7883bac74489bf9d06ee1)) 67 | 68 | 69 | ### BREAKING CHANGES 70 | 71 | * do not emit type information 72 | * drop support for Node 8 73 | 74 | ## [1.1.10](https://github.com/jest-community/jest-react-reporter/compare/v1.1.9...v1.1.10) (2019-11-14) 75 | 76 | ### Bug Fixes 77 | 78 | - respect `verbose` config 79 | ([14f9ab7](https://github.com/jest-community/jest-react-reporter/commit/14f9ab7a28be4a19b202a123ea21f2c0c50e8588)), 80 | closes [#6](https://github.com/jest-community/jest-react-reporter/issues/6) 81 | 82 | ## [1.1.9](https://github.com/jest-community/jest-react-reporter/compare/v1.1.8...v1.1.9) (2019-11-13) 83 | 84 | ### Bug Fixes 85 | 86 | - care less about width of terminal 87 | ([aa29f45](https://github.com/jest-community/jest-react-reporter/commit/aa29f4513d113306f840d46877003e38fe3d9fe5)) 88 | - handle duplicate log messages without barfing 89 | ([d48891a](https://github.com/jest-community/jest-react-reporter/commit/d48891ad69f2b50463561cf1bd62bb036e57677c)) 90 | - use normal spaces as padding instead of nbsp 91 | ([6a33c81](https://github.com/jest-community/jest-react-reporter/commit/6a33c81482a3341ffc7ff13919f38bde3fcf9448)) 92 | 93 | ## [1.1.8](https://github.com/jest-community/jest-react-reporter/compare/v1.1.7...v1.1.8) (2019-11-13) 94 | 95 | ### Bug Fixes 96 | 97 | - print to stderr 98 | ([de507dc](https://github.com/jest-community/jest-react-reporter/commit/de507dccce388a56271a640fed128c82dab93192)) 99 | 100 | ## [1.1.7](https://github.com/jest-community/jest-react-reporter/compare/v1.1.6...v1.1.7) (2019-11-13) 101 | 102 | ### Bug Fixes 103 | 104 | - always pass column width 105 | ([636edb8](https://github.com/jest-community/jest-react-reporter/commit/636edb827457ef2385f8e5a564ba910462ce3f64)) 106 | 107 | ## [1.1.6](https://github.com/jest-community/jest-react-reporter/compare/v1.1.5...v1.1.6) (2019-11-13) 108 | 109 | ### Bug Fixes 110 | 111 | - include TS types in bundle 112 | ([541a8d0](https://github.com/jest-community/jest-react-reporter/commit/541a8d0e3e733e35075521b864e2d1df6223a846)) 113 | 114 | ## [1.1.5](https://github.com/jest-community/jest-react-reporter/compare/v1.1.4...v1.1.5) (2019-11-13) 115 | 116 | ### Bug Fixes 117 | 118 | - correct dimming of text in summary 119 | ([2299ec7](https://github.com/jest-community/jest-react-reporter/commit/2299ec7e6d15af27f08b5c83ee4094de915e3eda)) 120 | 121 | ## [1.1.4](https://github.com/jest-community/jest-react-reporter/compare/v1.1.3...v1.1.4) (2019-11-13) 122 | 123 | ### Bug Fixes 124 | 125 | - use padding for spacing rather than spaces 126 | ([c98de61](https://github.com/jest-community/jest-react-reporter/commit/c98de619f5d23f80a6d77fa53d80b5addef600a0)) 127 | 128 | ## [1.1.3](https://github.com/jest-community/jest-react-reporter/compare/v1.1.2...v1.1.3) (2019-11-13) 129 | 130 | ### Bug Fixes 131 | 132 | - only print pattern if not the default 133 | ([e5e271e](https://github.com/jest-community/jest-react-reporter/commit/e5e271e17ac589edf4a22a53ee268d9ffab2e09d)) 134 | 135 | ## [1.1.2](https://github.com/jest-community/jest-react-reporter/compare/v1.1.1...v1.1.2) (2019-11-13) 136 | 137 | ### Bug Fixes 138 | 139 | - always show estimation if higher than runtime 140 | ([bd653b1](https://github.com/jest-community/jest-react-reporter/commit/bd653b1d5dd1c81f504dd832166c413f996dd6ba)) 141 | 142 | ## [1.1.1](https://github.com/jest-community/jest-react-reporter/compare/v1.1.0...v1.1.1) (2019-11-12) 143 | 144 | ### Bug Fixes 145 | 146 | - do not print post summary message if silent 147 | ([888fced](https://github.com/jest-community/jest-react-reporter/commit/888fced1c9ff1c01c90a4a5bccd528c73fd06962)) 148 | 149 | # [1.1.0](https://github.com/jest-community/jest-react-reporter/compare/v1.0.2...v1.1.0) (2019-11-12) 150 | 151 | ### Features 152 | 153 | - add message after summary 154 | ([ca67328](https://github.com/jest-community/jest-react-reporter/commit/ca67328ef616fb1d549a5c5d2aa0a408cbf10b0f)) 155 | 156 | ## [1.0.2](https://github.com/jest-community/jest-react-reporter/compare/v1.0.1...v1.0.2) (2019-11-12) 157 | 158 | ### Bug Fixes 159 | 160 | - always render a summary, remove progress and estimation when done 161 | ([4bc5cb3](https://github.com/jest-community/jest-react-reporter/commit/4bc5cb33d8b250c361e9812cd991ca0cb714536d)) 162 | 163 | ## [1.0.1](https://github.com/jest-community/jest-react-reporter/compare/v1.0.0...v1.0.1) (2019-11-12) 164 | 165 | ### Bug Fixes 166 | 167 | - unmount from within app rather than from teh outside 168 | ([eb2dc86](https://github.com/jest-community/jest-react-reporter/commit/eb2dc8676089c75142b72275a764cac3d9f72091)) 169 | 170 | # 1.0.0 (2019-11-10) 171 | 172 | ### Features 173 | 174 | - initial commit 175 | ([4a83048](https://github.com/jest-community/jest-react-reporter/commit/4a8304892e28b1a12d97bca8ad1e61db878d09b8)) 176 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Simen Bekkhus 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/jest-community/jest-react-reporter/workflows/Unit%20tests/badge.svg)](https://github.com/jest-community/jest-react-reporter/actions) 2 | 3 | ## Installation 4 | 5 | ```bash 6 | $ yarn add --dev jest-react-reporter 7 | ``` 8 | 9 | ## Usage 10 | 11 | Register the reporter with Jest following the docs: 12 | https://jestjs.io/docs/en/configuration#reporters-array-modulename-modulename-options 13 | 14 | Note that this is still early days and this reporter is quite experimental. 15 | We're aiming for feature parity with Jest's builtin reporters as a start, but 16 | we're hoping to add more features in the future. Please open issues (preferably 17 | PRs!) for any bugs. 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-react-reporter", 3 | "version": "5.0.0", 4 | "description": "Reporter for Jest written in React", 5 | "repository": "jest-community/jest-react-reporter", 6 | "license": "MIT", 7 | "author": "Simen Bekkhus ", 8 | "main": "./dist/index.js", 9 | "exports": "./dist/index.js", 10 | "files": [ 11 | "dist/" 12 | ], 13 | "scripts": { 14 | "ncc-build": "ncc build src/index.ts --external @jest/reporters --external react-devtools-core --external yoga-layout-prebuilt", 15 | "build": "yarn ncc-build --minify --no-source-map-register", 16 | "build:watch": "yarn ncc-build --watch", 17 | "lint": "eslint . --ignore-pattern '!.eslintrc.js'", 18 | "prepare": "husky && yarn build", 19 | "prettylint": "prettier README.md package.json --check", 20 | "test": "jest", 21 | "typecheck": "tsc -p . --noEmit" 22 | }, 23 | "commitlint": { 24 | "extends": [ 25 | "@commitlint/config-conventional" 26 | ] 27 | }, 28 | "lint-staged": { 29 | "*.{js,ts,tsx}": "eslint --fix", 30 | "*.{md,json}": "prettier --write" 31 | }, 32 | "prettier": { 33 | "arrowParens": "avoid", 34 | "endOfLine": "auto", 35 | "proseWrap": "always", 36 | "singleQuote": true, 37 | "trailingComma": "all" 38 | }, 39 | "jest": { 40 | "preset": "ts-jest", 41 | "reporters": [ 42 | "/" 43 | ], 44 | "testEnvironment": "node" 45 | }, 46 | "dependencies": { 47 | "@jest/reporters": "^29.0.2", 48 | "yoga-layout-prebuilt": "^1.10.0" 49 | }, 50 | "devDependencies": { 51 | "@commitlint/cli": "^19.3.0", 52 | "@commitlint/config-conventional": "^19.2.2", 53 | "@jest/test-result": "^29.0.2", 54 | "@jest/types": "^29.0.2", 55 | "@semantic-release/changelog": "^6.0.0", 56 | "@semantic-release/git": "^10.0.0", 57 | "@tsconfig/node14": "^14.1.2", 58 | "@types/jest": "^29.0.0", 59 | "@types/node": "^16.0.0", 60 | "@types/react": "^18.0.0", 61 | "@typescript-eslint/eslint-plugin": "^6.0.0", 62 | "@typescript-eslint/parser": "^6.0.0", 63 | "@vercel/ncc": "^0.38.0", 64 | "chalk": "^4.0.0", 65 | "eslint": "^8.0.0", 66 | "eslint-config-prettier": "^10.0.0", 67 | "eslint-plugin-import": "^2.18.2", 68 | "eslint-plugin-jest": "^28.0.0", 69 | "eslint-plugin-n": "^17.0.0", 70 | "eslint-plugin-prettier": "^5.0.0", 71 | "eslint-plugin-react": "^7.16.0", 72 | "eslint-plugin-react-hooks": "^4.0.2", 73 | "husky": "^9.0.0", 74 | "ink": "^3.2.0", 75 | "jest": "^29.0.2", 76 | "jest-util": "^29.0.2", 77 | "lint-staged": "^15.0.0", 78 | "prettier": "^3.0.0", 79 | "react": "^18.0.0", 80 | "semantic-release": "^24.0.0", 81 | "slash": "^3.0.0", 82 | "ts-jest": "^29.0.0", 83 | "typescript": "^5.0.0" 84 | }, 85 | "peerDependencies": { 86 | "jest": "^27.0.0 || ^28.0.0 || ^29.0.0" 87 | }, 88 | "resolutions": { 89 | "eslint-plugin-jest/@typescript-eslint/utils": "^6.0.0" 90 | }, 91 | "engines": { 92 | "node": ">=14" 93 | }, 94 | "release": { 95 | "plugins": [ 96 | "@semantic-release/commit-analyzer", 97 | "@semantic-release/release-notes-generator", 98 | "@semantic-release/changelog", 99 | "@semantic-release/npm", 100 | "@semantic-release/git", 101 | "@semantic-release/github" 102 | ] 103 | }, 104 | "packageManager": "yarn@4.6.0" 105 | } 106 | -------------------------------------------------------------------------------- /src/PostMessage.tsx: -------------------------------------------------------------------------------- 1 | import type { AggregatedResult } from '@jest/test-result'; 2 | import type { Config } from '@jest/types'; 3 | import type { TestContext } from '@jest/reporters'; 4 | import { testPathPatternToRegExp } from 'jest-util'; 5 | import * as React from 'react'; 6 | import { Box, Text } from 'ink'; 7 | 8 | const LeftPadded: React.FC<{ children: React.ReactNode }> = ({ children }) => ( 9 | {children} 10 | ); 11 | 12 | const HorizontallyPadded: React.FC<{ children: React.ReactNode }> = ({ 13 | children, 14 | }) => {children}; 15 | 16 | const TestInfo: React.FC<{ config: Config.GlobalConfig }> = ({ config }) => { 17 | if (config.runTestsByPath) { 18 | return ( 19 | 20 | within paths 21 | 22 | ); 23 | } 24 | 25 | if (config.onlyChanged) { 26 | return ( 27 | 28 | related to changed files 29 | 30 | ); 31 | } 32 | 33 | if (config.testPathPattern) { 34 | return ( 35 | 36 | 37 | 38 | {config.findRelatedTests ? 'related to files matching' : 'matching'} 39 | 40 | 41 | 42 | {testPathPatternToRegExp(config.testPathPattern).toString()} 43 | 44 | 45 | ); 46 | } 47 | 48 | return null; 49 | }; 50 | 51 | const NameInfo: React.FC<{ config: Config.GlobalConfig }> = ({ config }) => { 52 | if (config.runTestsByPath) { 53 | return {config.nonFlagArgs.map(p => `"${p}"`).join(', ')}; 54 | } 55 | 56 | if (config.testNamePattern) { 57 | return ( 58 | <> 59 | 60 | with tests matching 61 | 62 | "{config.testNamePattern}" 63 | 64 | ); 65 | } 66 | 67 | return null; 68 | }; 69 | 70 | const ContextInfo: React.FC<{ numberOfContexts: number }> = ({ 71 | numberOfContexts, 72 | }) => { 73 | if (numberOfContexts > 1) { 74 | return ( 75 | <> 76 | 77 | in 78 | 79 | {numberOfContexts} 80 | projects 81 | 82 | ); 83 | } 84 | 85 | return null; 86 | }; 87 | 88 | export const PostMessage: React.FC<{ 89 | aggregatedResults: AggregatedResult; 90 | globalConfig: Config.GlobalConfig; 91 | contexts: Set; 92 | }> = ({ aggregatedResults, globalConfig, contexts }) => { 93 | if (globalConfig.silent) { 94 | return null; 95 | } 96 | 97 | if (aggregatedResults.wasInterrupted) { 98 | return ( 99 | 100 | Test run was interrupted. 101 | 102 | ); 103 | } 104 | 105 | return ( 106 | 107 | Ran all test suites 108 | 109 | 110 | 111 | . 112 | 113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /src/Reporter.tsx: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as React from 'react'; 3 | import { 4 | Box, 5 | Static, 6 | Text, 7 | type TextProps, 8 | render, 9 | useApp, 10 | useStdout, 11 | } from 'ink'; 12 | import slash = require('slash'); 13 | import type { Config } from '@jest/types'; 14 | import type { AggregatedResult, TestResult } from '@jest/test-result'; 15 | import { 16 | BaseReporter, 17 | type ReporterOnStartOptions, 18 | type Test, 19 | type TestContext, 20 | } from '@jest/reporters'; 21 | import { SnapshotStatus } from './SnapshotStatus'; 22 | import { Summary } from './Summary'; 23 | import { DisplayName, FormattedPath, ResultHeader, Runs } from './shared'; 24 | import { PostMessage } from './PostMessage'; 25 | import { VerboseTestList } from './VerboseTests'; 26 | 27 | type ConsoleBuffer = NonNullable; 28 | type LogType = ConsoleBuffer[0]['type']; 29 | 30 | const TitleBullet = () => ; 31 | 32 | const ColoredConsole: React.FC< 33 | Omit & { type: LogType } 34 | > = ({ type, ...props }: { type: LogType }) => ( 35 | 39 | ); 40 | 41 | const TestConsoleOutput = ({ 42 | console, 43 | verbose, 44 | cwd, 45 | }: { console?: ConsoleBuffer } & Pick & 46 | Pick) => { 47 | if (!console || console.length === 0) { 48 | return null; 49 | } 50 | 51 | const TITLE_INDENT = verbose ? '\xa0'.repeat(2) : '\xa0'.repeat(4); 52 | const CONSOLE_INDENT = TITLE_INDENT + '\xa0'.repeat(2); 53 | 54 | const content = console.map(({ type, message, origin }, index) => { 55 | origin = slash(path.relative(cwd, origin)); 56 | message = message 57 | .split(/\n/) 58 | .map(line => CONSOLE_INDENT + line) 59 | .join('\n'); 60 | 61 | return ( 62 | 63 | 64 | {TITLE_INDENT}{' '} 65 | 66 | console. 67 | {type} 68 | {' '} 69 | {origin} 70 | 71 | {message} 72 | 73 | ); 74 | }); 75 | 76 | return ( 77 | 78 | 79 |    80 | Console: 81 | 82 | {content} 83 | 84 | ); 85 | }; 86 | 87 | const FailureMessage: React.FC<{ 88 | failureMessage: string | null | undefined; 89 | }> = ({ failureMessage }) => { 90 | if (failureMessage) { 91 | return <>{failureMessage.replace(/ /g, '\xa0')}; 92 | } 93 | 94 | return null; 95 | }; 96 | 97 | const CompletedTests: React.FC<{ 98 | completedTests: State['completedTests']; 99 | globalConfig: Config.GlobalConfig; 100 | }> = ({ completedTests, globalConfig }) => { 101 | if (completedTests.length === 0) { 102 | return null; 103 | } 104 | const didUpdate = globalConfig.updateSnapshot === 'all'; 105 | 106 | return ( 107 | 108 | 109 | {({ testResult, config }) => ( 110 | 111 | 112 | 116 | 121 | 122 | 126 | 127 | )} 128 | 129 | 130 | ); 131 | }; 132 | 133 | type DateEvents = 134 | | { type: 'TestStart'; payload: { test: Test } } 135 | | { 136 | type: 'TestResult'; 137 | payload: { 138 | aggregatedResults: AggregatedResult; 139 | test: Test; 140 | testResult: TestResult; 141 | }; 142 | } 143 | | { type: 'TestComplete'; payload: { contexts: Set } }; 144 | 145 | type Props = { 146 | register: (cb: (events: DateEvents) => void) => void; 147 | startingAggregatedResults: AggregatedResult; 148 | globalConfig: Config.GlobalConfig; 149 | options: ReporterOnStartOptions; 150 | }; 151 | 152 | type State = { 153 | aggregatedResults: AggregatedResult; 154 | completedTests: Array<{ 155 | testResult: TestResult; 156 | config: Config.ProjectConfig; 157 | }>; 158 | currentTests: Array<[string, Config.ProjectConfig]>; 159 | done: boolean; 160 | contexts: Set; 161 | }; 162 | 163 | const reporterReducer: React.Reducer = ( 164 | prevState, 165 | action, 166 | ) => { 167 | switch (action.type) { 168 | case 'TestStart': 169 | return { 170 | ...prevState, 171 | currentTests: [ 172 | ...prevState.currentTests, 173 | [action.payload.test.path, action.payload.test.context.config], 174 | ], 175 | }; 176 | case 'TestResult': { 177 | const { aggregatedResults, test, testResult } = action.payload; 178 | const currentTests = prevState.currentTests.filter( 179 | ([testPath]) => test.path !== testPath, 180 | ); 181 | return { 182 | ...prevState, 183 | aggregatedResults, 184 | completedTests: testResult.skipped 185 | ? prevState.completedTests 186 | : prevState.completedTests.concat({ 187 | config: test.context.config, 188 | testResult, 189 | }), 190 | currentTests, 191 | }; 192 | } 193 | case 'TestComplete': { 194 | return { ...prevState, done: true, contexts: action.payload.contexts }; 195 | } 196 | } 197 | }; 198 | 199 | const RunningTests: React.FC<{ 200 | tests: State['currentTests']; 201 | width: number; 202 | }> = ({ tests, width }) => { 203 | if (tests.length === 0) { 204 | return null; 205 | } 206 | 207 | return ( 208 | 209 | {tests.map(([path, config]) => ( 210 | 211 | 212 | 213 | 219 | 220 | ))} 221 | 222 | ); 223 | }; 224 | 225 | const Reporter: React.FC = ({ 226 | register, 227 | globalConfig, 228 | options, 229 | startingAggregatedResults, 230 | }) => { 231 | const [state, dispatch] = React.useReducer(reporterReducer, { 232 | aggregatedResults: startingAggregatedResults, 233 | completedTests: [], 234 | currentTests: [], 235 | done: false, 236 | contexts: new Set(), 237 | }); 238 | 239 | React.useLayoutEffect(() => { 240 | register(dispatch); 241 | }, [register]); 242 | 243 | const { stdout } = useStdout(); 244 | const width = stdout?.columns ?? 80; 245 | 246 | const { currentTests, completedTests, aggregatedResults, done, contexts } = 247 | state; 248 | const { estimatedTime = 0 } = options; 249 | 250 | const { exit } = useApp(); 251 | React.useEffect(() => { 252 | if (done) { 253 | exit(); 254 | } 255 | }, [done, exit]); 256 | 257 | return ( 258 | 259 | 263 | 264 | 269 | {done ? ( 270 | 275 | ) : null} 276 | 277 | ); 278 | }; 279 | 280 | export default class ReactReporter extends BaseReporter { 281 | private _globalConfig: Config.GlobalConfig; 282 | private _components: Array<(events: DateEvents) => void>; 283 | private _waitUntilExit?: () => Promise; 284 | 285 | constructor(globalConfig: Config.GlobalConfig) { 286 | super(); 287 | this._globalConfig = globalConfig; 288 | this._components = []; 289 | } 290 | 291 | onRunStart( 292 | aggregatedResults: AggregatedResult, 293 | options: ReporterOnStartOptions, 294 | ) { 295 | // TODO: remove args after Jest 25 is published 296 | super.onRunStart(aggregatedResults, options); 297 | const { waitUntilExit } = render( 298 | this._components.push(cb)} 300 | startingAggregatedResults={aggregatedResults} 301 | options={options} 302 | globalConfig={this._globalConfig} 303 | />, 304 | // TODO: should respect `GlobalConfig.useStderr`? Jest itself does not: https://github.com/facebook/jest/issues/5064 305 | { experimental: true, stdout: process.stderr }, 306 | ); 307 | 308 | this._waitUntilExit = waitUntilExit; 309 | } 310 | 311 | onTestStart(test: Test) { 312 | this._components.forEach(cb => 313 | cb({ payload: { test }, type: 'TestStart' }), 314 | ); 315 | } 316 | 317 | onTestResult( 318 | test: Test, 319 | testResult: TestResult, 320 | aggregatedResults: AggregatedResult, 321 | ) { 322 | this._components.forEach(cb => 323 | cb({ 324 | payload: { aggregatedResults, test, testResult }, 325 | type: 'TestResult', 326 | }), 327 | ); 328 | } 329 | 330 | async onRunComplete( 331 | contexts: Set, 332 | _aggregatedResults?: AggregatedResult, 333 | ) { 334 | this._components.forEach(cb => 335 | cb({ type: 'TestComplete', payload: { contexts } }), 336 | ); 337 | if (this._waitUntilExit) { 338 | await this._waitUntilExit(); 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/SnapshotStatus.tsx: -------------------------------------------------------------------------------- 1 | import type { TestResult } from '@jest/test-result'; 2 | 3 | import * as React from 'react'; 4 | import { Box, Text } from 'ink'; 5 | 6 | import { pluralize } from 'jest-util'; 7 | import { Arrow, Dot } from './shared'; 8 | 9 | const FailText: React.FC<{ children: React.ReactNode }> = ({ children }) => ( 10 | 11 | {children} 12 | 13 | ); 14 | const SnapshotAdded: React.FC<{ children: React.ReactNode }> = ({ 15 | children, 16 | }) => ( 17 | 18 | {children} 19 | 20 | ); 21 | const SnapshotUpdated: React.FC<{ children: React.ReactNode }> = ({ 22 | children, 23 | }) => ( 24 | 25 | {children} 26 | 27 | ); 28 | const SnapshotOutdated: React.FC<{ children: React.ReactNode }> = ({ 29 | children, 30 | }) => ( 31 | 32 | {children} 33 | 34 | ); 35 | 36 | export const SnapshotStatus: React.FC<{ 37 | snapshot: TestResult['snapshot']; 38 | afterUpdate: boolean; 39 | }> = ({ snapshot, afterUpdate }) => ( 40 | <> 41 | {snapshot.added > 0 && ( 42 | 43 | {pluralize('snapshot', snapshot.added)} written. 44 | 45 | )} 46 | {snapshot.updated > 0 && ( 47 | 48 | {pluralize('snapshot', snapshot.updated)} updated. 49 | 50 | )} 51 | {snapshot.unmatched > 0 && ( 52 | 53 | {pluralize('snapshot', snapshot.unmatched)} failed. 54 | 55 | )} 56 | {snapshot.unchecked > 0 ? ( 57 | afterUpdate ? ( 58 | 59 | {pluralize('snapshot', snapshot.unchecked)} removed. 60 | 61 | ) : ( 62 | 63 | {pluralize('snapshot', snapshot.unchecked)} obsolete. 64 | 65 | ) 66 | ) : null} 67 | {snapshot.unchecked > 0 && 68 | snapshot.uncheckedKeys.map(key => ( 69 | 70 |    71 | 72 | {key} 73 | 74 | ))} 75 | {snapshot.fileDeleted && ( 76 | 77 | snapshot file removed. 78 | 79 | )} 80 | 81 | ); 82 | -------------------------------------------------------------------------------- /src/SnapshotSummary.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import type { Config } from '@jest/types'; 4 | import { pluralize } from 'jest-util'; 5 | import type { SnapshotSummary as SnapshotSummaryType } from '@jest/test-result'; 6 | import { Arrow, Dot, DownArrow, FormatFullTestPath } from './shared'; 7 | 8 | const SnapshotSummary: React.FC<{ 9 | snapshots: SnapshotSummaryType; 10 | globalConfig: Config.GlobalConfig; 11 | updateCommand: string; 12 | }> = ({ snapshots, globalConfig, updateCommand }) => ( 13 | 14 | Snapshot Summary 15 | {snapshots.added && ( 16 | 17 | 18 | 19 | {pluralize('snapshot', snapshots.added)} written 20 | {' '} 21 | from {pluralize('test suite', snapshots.filesAdded)}. 22 | 23 | )} 24 | {snapshots.unmatched && ( 25 | 26 | 27 | 28 | {pluralize('snapshot', snapshots.unmatched)} failed 29 | {' '} 30 | from {pluralize('test suite', snapshots.filesUnmatched)}.{' '} 31 | 32 | Inspect your code changes or {updateCommand} to update them. 33 | 34 | 35 | )} 36 | {snapshots.updated && ( 37 | 38 | 39 | 40 | {pluralize('snapshot', snapshots.updated)} updated 41 | {' '} 42 | from {pluralize('test suite', snapshots.filesUpdated)}. 43 | 44 | )} 45 | {snapshots.filesRemoved && 46 | (snapshots.didUpdate ? ( 47 | 48 | 49 | 50 | {pluralize('snapshot file', snapshots.filesRemoved)} removed 51 | {' '} 52 | from {pluralize('test suite', snapshots.filesRemoved)}. 53 | 54 | ) : ( 55 | 56 | 57 | 58 | {pluralize('snapshot file', snapshots.filesRemoved)} obsolete 59 | {' '} 60 | from {pluralize('test suite', snapshots.filesRemoved)}.{' '} 61 | 62 | To remove ${snapshots.filesRemoved === 1 ? 'it' : 'them all'},{' '} 63 | {updateCommand}. 64 | 65 | 66 | ))} 67 | {snapshots.unchecked && 68 | (snapshots.didUpdate ? ( 69 | 70 | 71 | 72 | {pluralize('snapshot', snapshots.unchecked)} removed 73 | {' '} 74 | from {pluralize('test suite', snapshots.uncheckedKeysByFile.length)}. 75 | 76 | ) : ( 77 | 78 | 79 | 80 | {pluralize('snapshot', snapshots.unchecked)} obsolete 81 | {' '} 82 | from {pluralize('test suite', snapshots.uncheckedKeysByFile.length)}.{' '} 83 | 84 | To remove ${snapshots.unchecked === 1 ? 'it' : 'them all'},{' '} 85 | {updateCommand}. 86 | 87 | 88 | ))} 89 | {snapshots.unchecked && 90 | snapshots.uncheckedKeysByFile.map(uncheckedFile => ( 91 | <> 92 | 93 |    94 | 95 | 99 | 100 | 101 | {uncheckedFile.keys.map(key => ( 102 | <> 103 |        104 | 105 | {key} 106 | 107 | ))} 108 | 109 | 110 | ))} 111 | 112 | ); 113 | 114 | export default SnapshotSummary; 115 | -------------------------------------------------------------------------------- /src/Summary.tsx: -------------------------------------------------------------------------------- 1 | import type { SummaryOptions } from '@jest/reporters'; 2 | import type { AggregatedResult } from '@jest/test-result'; 3 | import { pluralize } from 'jest-util'; 4 | import * as React from 'react'; 5 | import { Box, Text } from 'ink'; 6 | import { useCounter } from './hooks'; 7 | 8 | const PROGRESS_BAR_WIDTH = 40; 9 | 10 | const SummaryHeading: React.FC<{ children: string }> = ({ children }) => ( 11 | 12 | {children}: 13 | 14 | ); 15 | 16 | const RightPaddedWithComma: React.FC<{ children: React.ReactNode }> = ({ 17 | children, 18 | }) => ( 19 | 20 | {children}, 21 | 22 | ); 23 | 24 | export const Summary: React.FC<{ 25 | aggregatedResults: AggregatedResult; 26 | done: boolean; 27 | options?: SummaryOptions; 28 | }> = ({ aggregatedResults, done, options }) => { 29 | const { startTime } = aggregatedResults; 30 | const [runTime, setRunTime] = React.useState(0); 31 | const time = useCounter(); 32 | React.useEffect(() => { 33 | let newRunTime = (Date.now() - startTime) / 1000; 34 | 35 | if (options && options.roundTime) { 36 | newRunTime = Math.floor(newRunTime); 37 | } 38 | 39 | setRunTime(newRunTime); 40 | }, [options, startTime, time]); 41 | 42 | const estimatedTime = (options && options.estimatedTime) || 0; 43 | const snapshotResults = aggregatedResults.snapshot; 44 | const snapshotsAdded = snapshotResults.added; 45 | const snapshotsFailed = snapshotResults.unmatched; 46 | const snapshotsOutdated = snapshotResults.unchecked; 47 | const snapshotsFilesRemoved = snapshotResults.filesRemoved; 48 | const snapshotsDidUpdate = snapshotResults.didUpdate; 49 | const snapshotsPassed = snapshotResults.matched; 50 | const snapshotsTotal = snapshotResults.total; 51 | const snapshotsUpdated = snapshotResults.updated; 52 | const suitesFailed = aggregatedResults.numFailedTestSuites; 53 | const suitesPassed = aggregatedResults.numPassedTestSuites; 54 | const suitesPending = aggregatedResults.numPendingTestSuites; 55 | const suitesRun = suitesFailed + suitesPassed; 56 | const suitesTotal = aggregatedResults.numTotalTestSuites; 57 | const testsFailed = aggregatedResults.numFailedTests; 58 | const testsPassed = aggregatedResults.numPassedTests; 59 | const testsPending = aggregatedResults.numPendingTests; 60 | const testsTodo = aggregatedResults.numTodoTests; 61 | const testsTotal = aggregatedResults.numTotalTests; 62 | const width = (options && options.width) || 0; 63 | 64 | return ( 65 | 66 | 67 | Test Suites 68 | 69 | {suitesFailed > 0 && ( 70 | 71 | 72 | {suitesFailed} failed 73 | 74 | ,{' '} 75 | 76 | )} 77 | {suitesPending > 0 && ( 78 | 79 | 80 | {suitesPending} skipped 81 | 82 | ,{' '} 83 | 84 | )} 85 | {suitesPassed > 0 && ( 86 | 87 | 88 | {suitesPassed} passed 89 | 90 | ,{' '} 91 | 92 | )} 93 | {suitesRun !== suitesTotal && {suitesRun} of } 94 | {suitesTotal} total 95 | 96 | 97 | 98 | Tests 99 | 100 | {testsFailed > 0 && ( 101 | 102 | 103 | {testsFailed} failed 104 | 105 | 106 | )} 107 | {testsPending > 0 && ( 108 | <> 109 | 110 | {testsPending} skipped 111 | 112 | ,{' '} 113 | 114 | )} 115 | {testsTodo > 0 && ( 116 | 117 | 118 | {testsTodo} todo 119 | 120 | 121 | )} 122 | {testsPassed > 0 && ( 123 | 124 | 125 | {testsPassed} passed 126 | 127 | 128 | )} 129 | {testsTotal} total 130 | 131 | 132 | 133 | Snapshots 134 | 135 | {snapshotsFailed > 0 && ( 136 | 137 | 138 | {snapshotsFailed} failed 139 | 140 | 141 | )} 142 | {snapshotsOutdated > 0 && !snapshotsDidUpdate && ( 143 | 144 | 145 | {snapshotsOutdated} obsolete 146 | 147 | 148 | )} 149 | {snapshotsOutdated > 0 && snapshotsDidUpdate && ( 150 | <> 151 | 152 | {snapshotsOutdated} removed 153 | 154 | ,{' '} 155 | 156 | )} 157 | {snapshotsFilesRemoved > 0 && !snapshotsDidUpdate && ( 158 | 159 | 160 | {pluralize('file', snapshotsFilesRemoved)} obsolete 161 | 162 | 163 | )} 164 | {snapshotsFilesRemoved > 0 && snapshotsDidUpdate && ( 165 | 166 | 167 | {pluralize('file', snapshotsFilesRemoved)} removed 168 | 169 | 170 | )} 171 | {snapshotsUpdated > 0 && ( 172 | 173 | 174 | {snapshotsUpdated} updated 175 | 176 | 177 | )} 178 | {snapshotsAdded > 0 && ( 179 | 180 | 181 | {snapshotsAdded} written 182 | 183 | 184 | )} 185 | {snapshotsPassed > 0 && ( 186 | 187 | 188 | {snapshotsPassed} passed 189 | 190 | 191 | )} 192 | {snapshotsTotal} total 193 | 194 | 195 | 196 | Time 197 | 199 | 200 | 206 | 207 | ); 208 | }; 209 | 210 | const ProgressBar: React.FC<{ 211 | runTime: number; 212 | estimatedTime: number; 213 | done: boolean; 214 | width?: number; 215 | }> = ({ estimatedTime, runTime, width, done }) => { 216 | if (done) { 217 | return null; 218 | } 219 | // Only show a progress bar if the test run is actually going to take 220 | // some time. 221 | if (estimatedTime <= 2 || runTime >= estimatedTime || !width) { 222 | return null; 223 | } 224 | const availableWidth = Math.min(PROGRESS_BAR_WIDTH, width); 225 | 226 | if (availableWidth < 2) { 227 | return null; 228 | } 229 | 230 | const length = Math.min( 231 | Math.floor((runTime / estimatedTime) * availableWidth), 232 | availableWidth, 233 | ); 234 | 235 | return ( 236 | 237 | {'█'.repeat(length)} 238 | {'█'.repeat(availableWidth - length)} 239 | 240 | ); 241 | }; 242 | 243 | const Time: React.FC<{ 244 | runTime: number; 245 | estimatedTime: number; 246 | }> = ({ runTime, estimatedTime }) => { 247 | // If we are more than one second over the estimated time, highlight it. 248 | const renderedTime = 249 | estimatedTime && runTime >= estimatedTime + 1 ? ( 250 | 251 | {runTime}s 252 | 253 | ) : ( 254 | {runTime}s 255 | ); 256 | 257 | return ( 258 | 259 | {renderedTime} 260 | {runTime < estimatedTime ? ( 261 | , estimated {estimatedTime}s 262 | ) : null} 263 | 264 | ); 265 | }; 266 | -------------------------------------------------------------------------------- /src/VerboseTests.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import type { Config } from '@jest/types'; 4 | import type { AssertionResult, Suite, TestResult } from '@jest/test-result'; 5 | import { VerboseReporter } from '@jest/reporters'; 6 | import { specialChars } from 'jest-util'; 7 | 8 | const { ICONS } = specialChars; 9 | 10 | const Status: React.FC<{ status: AssertionResult['status'] }> = ({ 11 | status, 12 | }) => { 13 | if (status === 'failed') { 14 | return {ICONS.failed}; 15 | } 16 | if (status === 'pending') { 17 | return {ICONS.pending}; 18 | } 19 | if (status === 'todo') { 20 | return {ICONS.todo}; 21 | } 22 | return {ICONS.success}; 23 | }; 24 | 25 | const TestLine: React.FC<{ test: AssertionResult; indentation: number }> = ({ 26 | test, 27 | indentation, 28 | }) => ( 29 | 30 | 31 | 32 | 33 | {test.title} 34 | {test.duration ? ( 35 | 36 | ({test.duration.toFixed(0)}ms) 37 | 38 | ) : null} 39 | 40 | ); 41 | const NotExpandedTestLine: React.FC<{ 42 | test: AssertionResult; 43 | indentation: number; 44 | }> = ({ test, indentation }) => ( 45 | 46 | 47 | 48 | 49 | 50 | {test.status === 'pending' ? 'skipped' : test.status} {test.title} 51 | 52 | 53 | ); 54 | 55 | const TestsLog: React.FC<{ 56 | tests: Suite['tests']; 57 | indendation: number; 58 | expand: boolean; 59 | }> = ({ tests, expand, indendation }) => { 60 | if (expand) { 61 | return ( 62 | <> 63 | {tests.map((test, i) => ( 64 | 65 | ))} 66 | 67 | ); 68 | } 69 | 70 | const summedTests = tests.reduce<{ 71 | pending: Suite['tests']; 72 | todo: Suite['tests']; 73 | render: Suite['tests']; 74 | }>( 75 | (result, test) => { 76 | if (test.status === 'pending') { 77 | result.pending.push(test); 78 | } else if (test.status === 'todo') { 79 | result.todo.push(test); 80 | } else { 81 | result.render.push(test); 82 | } 83 | 84 | return result; 85 | }, 86 | { pending: [], todo: [], render: [] }, 87 | ); 88 | 89 | return ( 90 | 91 | {summedTests.render.map((test, i) => ( 92 | 93 | ))} 94 | {summedTests.pending.map((test, i) => ( 95 | 100 | ))} 101 | {summedTests.todo.map((test, i) => ( 102 | 107 | ))} 108 | 109 | ); 110 | }; 111 | 112 | const SuiteLog: React.FC<{ 113 | indendation: number; 114 | suite: Suite; 115 | globalConfig: Config.GlobalConfig; 116 | }> = ({ globalConfig, indendation, suite }) => { 117 | const newIndentation = indendation + 1; 118 | 119 | return ( 120 | 121 | {suite.title ? {suite.title} : null} 122 | 123 | 128 | {suite.suites.map((inner, i) => ( 129 | 135 | ))} 136 | 137 | ); 138 | }; 139 | 140 | export const VerboseTestList: React.FC<{ 141 | testResult: TestResult; 142 | globalConfig: Config.GlobalConfig; 143 | }> = ({ testResult, globalConfig }) => { 144 | if (!globalConfig.verbose) { 145 | return null; 146 | } 147 | 148 | const groupedTests = VerboseReporter.groupTestsBySuites( 149 | testResult.testResults, 150 | ); 151 | 152 | return ( 153 | 158 | ); 159 | }; 160 | -------------------------------------------------------------------------------- /src/__tests__/index.ts: -------------------------------------------------------------------------------- 1 | import '../'; 2 | 3 | test('placeholder', () => { 4 | expect(1).toBe(1); 5 | }); 6 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // from: https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 4 | function useInterval(callback: () => void, delay: number) { 5 | const savedCallback = React.useRef(callback); 6 | 7 | // Remember the latest callback. 8 | React.useEffect(() => { 9 | savedCallback.current = callback; 10 | }, [callback]); 11 | 12 | // Set up the interval. 13 | React.useEffect(() => { 14 | function tick() { 15 | savedCallback.current(); 16 | } 17 | if (delay !== null) { 18 | const id = setInterval(tick, delay); 19 | return () => clearInterval(id); 20 | } 21 | 22 | return undefined; 23 | }, [delay]); 24 | } 25 | 26 | export function useCounter() { 27 | const [count, setCount] = React.useState(0); 28 | 29 | useInterval(() => { 30 | setCount(count => count + 1); 31 | }, 1000); 32 | 33 | return count; 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import ReactReporter from './Reporter'; 2 | 3 | export { default } from './Reporter'; 4 | 5 | // Until https://github.com/facebook/jest/pull/9161 is released 6 | // eslint-disable-next-line import/no-commonjs 7 | module.exports = ReactReporter; 8 | -------------------------------------------------------------------------------- /src/shared.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Text, type TextProps } from 'ink'; 3 | import type { TestResult } from '@jest/test-result'; 4 | import type { Config } from '@jest/types'; 5 | import chalk = require('chalk'); 6 | import slash = require('slash'); 7 | import { relativePath } from './utils'; 8 | 9 | export const Arrow: React.FC = () => <>{' \u203A '}; 10 | export const Dot: React.FC = () => <>{' \u2022 '}; 11 | export const DownArrow: React.FC = () => <>{' \u21B3 '}; 12 | 13 | const pad = (string: string) => (chalk.supportsColor ? ` ${string} ` : string); 14 | 15 | const PaddedText: React.FC = ({ children, ...props }) => ( 16 | 17 | {children} 18 | 19 | ); 20 | 21 | const Status: React.FC = ({ text, ...props }) => ( 22 | 23 | {pad(text)} 24 | 25 | ); 26 | 27 | const Fails: React.FC = () => ; 28 | 29 | const Pass: React.FC = () => ; 30 | 31 | export const Runs: React.FC = () => ; 32 | 33 | export const DisplayName: React.FC<{ 34 | config: Config.ProjectConfig; 35 | }> = ({ config }) => { 36 | const { displayName } = config; 37 | if (!displayName) { 38 | return null; 39 | } 40 | 41 | if (typeof displayName === 'string') { 42 | return ( 43 | 44 | {displayName} 45 | 46 | ); 47 | } 48 | 49 | const { name, color } = displayName; 50 | 51 | return ( 52 | 53 | {name} 54 | 55 | ); 56 | }; 57 | 58 | const TestStatus: React.FC<{ testResult: TestResult }> = ({ testResult }) => { 59 | if (testResult.skipped) { 60 | return null; 61 | } 62 | 63 | if (testResult.numFailingTests > 0 || testResult.testExecError) { 64 | return ; 65 | } 66 | 67 | return ; 68 | }; 69 | 70 | export const ResultHeader: React.FC<{ 71 | testResult: TestResult; 72 | config: Config.ProjectConfig; 73 | }> = ({ testResult, config }) => ( 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | 81 | export const FormattedPath: React.FC<{ 82 | pad: number; 83 | config: Config.ProjectConfig | Config.GlobalConfig; 84 | testPath: string; 85 | columns: number | undefined; 86 | }> = ({ pad, config, testPath, columns }) => { 87 | const maxLength = (columns || Number.NaN) - pad; 88 | const relative = relativePath(config, testPath); 89 | const { basename } = relative; 90 | let { dirname } = relative; 91 | dirname = slash(dirname); 92 | 93 | // length is ok 94 | if (`${dirname}/${basename}`.length <= maxLength) { 95 | return ( 96 | <> 97 | {dirname}/ 98 | {basename} 99 | 100 | ); 101 | } 102 | 103 | // we can fit trimmed dirname and full basename 104 | const basenameLength = basename.length; 105 | if (basenameLength + 4 < maxLength) { 106 | const dirnameLength = maxLength - 4 - basenameLength; 107 | dirname = `…${dirname.slice( 108 | dirname.length - dirnameLength, 109 | dirname.length, 110 | )}`; 111 | return ( 112 | <> 113 | {dirname}/ 114 | {basename} 115 | 116 | ); 117 | } 118 | 119 | if (basenameLength + 4 === maxLength) { 120 | return ( 121 | <> 122 | …/ 123 | {basename} 124 | 125 | ); 126 | } 127 | 128 | // can't fit dirname, but can fit trimmed basename 129 | return ( 130 | 131 | …{basename.slice(basename.length - maxLength - 4, basename.length)} 132 | 133 | ); 134 | }; 135 | 136 | export const FormatFullTestPath: React.FC<{ 137 | config: Config.GlobalConfig | Config.ProjectConfig; 138 | testPath: string; 139 | }> = ({ config, testPath }) => ( 140 | // TODO: maybe not 9000? We just don't want to trim it 141 | 142 | ); 143 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import type { Config } from '@jest/types'; 3 | 4 | export const relativePath = ( 5 | config: Config.GlobalConfig | Config.ProjectConfig, 6 | testPath: string, 7 | ) => { 8 | // this function can be called with ProjectConfigs or GlobalConfigs. GlobalConfigs 9 | // do not have config.cwd, only config.rootDir. Try using config.cwd, fallback 10 | // to config.rootDir. (Also, some unit just use config.rootDir, which is ok) 11 | testPath = path.relative( 12 | (config as Config.ProjectConfig).cwd || config.rootDir, 13 | testPath, 14 | ); 15 | const dirname = path.dirname(testPath); 16 | const basename = path.basename(testPath); 17 | return { basename, dirname }; 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node14/tsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck": false, 5 | "jsx": "react", 6 | "declaration": false, 7 | "isolatedModules": true, 8 | "noImplicitReturns": true, 9 | // ink requires this 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "outDir": "dist/" 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["**/__tests__/**"] 16 | } 17 | --------------------------------------------------------------------------------