├── .github └── workflows │ ├── ci.yml │ ├── commit-if-modified.sh │ ├── copyright-year.sh │ ├── isaacs-makework.yml │ └── package-json-repo.js ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── index.test.js ├── lib ├── cardinal-theme.js ├── cardinal-theme.test.js ├── color.js ├── index.js ├── index.test.js ├── pretty-diff.js ├── pretty-diff.test.js ├── pretty-source.js ├── pretty-source.test.js ├── reports │ ├── base │ │ ├── assert-counts.js │ │ ├── assert-counts.test.jsx │ │ ├── assert-name.js │ │ ├── assert-name.test.jsx │ │ ├── counts.js │ │ ├── counts.test.js │ │ ├── footer.js │ │ ├── footer.test.jsx │ │ ├── index.js │ │ ├── index.test.jsx │ │ ├── log.js │ │ ├── log.test.jsx │ │ ├── pass-fail.js │ │ ├── pass-fail.test.jsx │ │ ├── result.js │ │ ├── result.test.jsx │ │ ├── runs.js │ │ ├── runs.test.jsx │ │ ├── status-mark.js │ │ ├── status-mark.test.jsx │ │ ├── suite-counts.js │ │ ├── suite-counts.test.jsx │ │ ├── summary.js │ │ ├── summary.test.jsx │ │ ├── test-point.js │ │ ├── test-point.test.jsx │ │ ├── test.js │ │ └── test.test.jsx │ ├── specy │ │ ├── assert-name.js │ │ ├── assert-name.test.jsx │ │ ├── footer.js │ │ ├── footer.test.jsx │ │ ├── index.js │ │ ├── index.test.jsx │ │ ├── log.js │ │ ├── log.test.jsx │ │ ├── summary.js │ │ ├── summary.test.jsx │ │ ├── test-point.js │ │ └── test-point.test.jsx │ └── terse │ │ ├── footer.js │ │ ├── footer.test.jsx │ │ ├── index.js │ │ ├── index.test.jsx │ │ ├── log.js │ │ ├── log.test.jsx │ │ ├── summary.js │ │ └── summary.test.jsx ├── reset.js └── string-length.js ├── map.js ├── map.test.js ├── package-lock.json ├── package.json ├── tap-snapshots ├── lib │ ├── pretty-diff.test.js.test.cjs │ ├── pretty-source.test.js.test.cjs │ └── reports │ │ ├── base │ │ ├── assert-counts.test.jsx.test.cjs │ │ ├── assert-name.test.jsx.test.cjs │ │ ├── counts.test.js.test.cjs │ │ ├── footer.test.jsx.test.cjs │ │ ├── index.test.jsx.test.cjs │ │ ├── log.test.jsx.test.cjs │ │ ├── pass-fail.test.jsx.test.cjs │ │ ├── result.test.jsx.test.cjs │ │ ├── runs.test.jsx.test.cjs │ │ ├── status-mark.test.jsx.test.cjs │ │ ├── suite-counts.test.jsx.test.cjs │ │ ├── summary.test.jsx.test.cjs │ │ ├── test-point.test.jsx.test.cjs │ │ └── test.test.jsx.test.cjs │ │ ├── specy │ │ ├── assert-name.test.jsx.test.cjs │ │ ├── footer.test.jsx.test.cjs │ │ ├── index.test.jsx.test.cjs │ │ ├── log.test.jsx.test.cjs │ │ ├── summary.test.jsx.test.cjs │ │ └── test-point.test.jsx.test.cjs │ │ └── terse │ │ ├── footer.test.jsx.test.cjs │ │ ├── index.test.jsx.test.cjs │ │ ├── log.test.jsx.test.cjs │ │ └── summary.test.jsx.test.cjs └── types.test.js.test.cjs ├── types.js └── types.test.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | node-version: [12.x, 14.x, 16.x, 17.x] 10 | platform: 11 | - os: ubuntu-latest 12 | shell: bash 13 | - os: macos-latest 14 | shell: bash 15 | - os: windows-latest 16 | shell: bash 17 | - os: windows-latest 18 | shell: powershell 19 | fail-fast: false 20 | 21 | runs-on: ${{ matrix.platform.os }} 22 | defaults: 23 | run: 24 | shell: ${{ matrix.platform.shell }} 25 | 26 | steps: 27 | - name: Checkout Repository 28 | uses: actions/checkout@v1.1.0 29 | 30 | - name: Use Nodejs ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | 35 | - name: Install dependencies 36 | run: npm install 37 | 38 | - name: Run Tests 39 | run: npm test -- -c -t0 40 | -------------------------------------------------------------------------------- /.github/workflows/commit-if-modified.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git config --global user.email "$1" 3 | shift 4 | git config --global user.name "$1" 5 | shift 6 | message="$1" 7 | shift 8 | if [ $(git status --porcelain "$@" | egrep '^ M' | wc -l) -gt 0 ]; then 9 | git add "$@" 10 | git commit -m "$message" 11 | git push || git pull --rebase 12 | git push 13 | fi 14 | -------------------------------------------------------------------------------- /.github/workflows/copyright-year.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dir=${1:-$PWD} 3 | dates=($(git log --date=format:%Y --pretty=format:'%ad' --reverse | sort | uniq)) 4 | if [ "${#dates[@]}" -eq 1 ]; then 5 | datestr="${dates}" 6 | else 7 | datestr="${dates}-${dates[${#dates[@]}-1]}" 8 | fi 9 | 10 | stripDate='s/^((.*)Copyright\b(.*?))((?:,\s*)?(([0-9]{4}\s*-\s*[0-9]{4})|(([0-9]{4},\s*)*[0-9]{4})))(?:,)?\s*(.*)\n$/$1$9\n/g' 11 | addDate='s/^.*Copyright(?:\s*\(c\))? /Copyright \(c\) '$datestr' /g' 12 | for l in $dir/LICENSE*; do 13 | perl -pi -e "$stripDate" $l 14 | perl -pi -e "$addDate" $l 15 | done 16 | -------------------------------------------------------------------------------- /.github/workflows/isaacs-makework.yml: -------------------------------------------------------------------------------- 1 | name: "various tidying up tasks to silence nagging" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | makework: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Use Node.js 17 | uses: actions/setup-node@v2.1.4 18 | with: 19 | node-version: 16.x 20 | - name: put repo in package.json 21 | run: node .github/workflows/package-json-repo.js 22 | - name: check in package.json if modified 23 | run: | 24 | bash -x .github/workflows/commit-if-modified.sh \ 25 | "package-json-repo-bot@example.com" \ 26 | "package.json Repo Bot" \ 27 | "chore: add repo to package.json" \ 28 | package.json package-lock.json 29 | - name: put all dates in license copyright line 30 | run: bash .github/workflows/copyright-year.sh 31 | - name: check in licenses if modified 32 | run: | 33 | bash .github/workflows/commit-if-modified.sh \ 34 | "license-year-bot@example.com" \ 35 | "License Year Bot" \ 36 | "chore: add copyright year to license" \ 37 | LICENSE* 38 | -------------------------------------------------------------------------------- /.github/workflows/package-json-repo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pf = require.resolve(`${process.cwd()}/package.json`) 4 | const pj = require(pf) 5 | 6 | if (!pj.repository && process.env.GITHUB_REPOSITORY) { 7 | const fs = require('fs') 8 | const server = process.env.GITHUB_SERVER_URL || 'https://github.com' 9 | const repo = `${server}/${process.env.GITHUB_REPOSITORY}` 10 | pj.repository = repo 11 | const json = fs.readFileSync(pf, 'utf8') 12 | const match = json.match(/^\s*\{[\r\n]+([ \t]*)"/) 13 | const indent = match[1] 14 | const output = JSON.stringify(pj, null, indent || 2) + '\n' 15 | fs.writeFileSync(pf, output) 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /coverage/ 3 | /.nyc_output/ 4 | /nyc_output/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | treport is released under the following license: 2 | 3 | ``` 4 | The ISC License 5 | 6 | Copyright (c) 2019-2023 Isaac Z. Schlueter and Contributors 7 | 8 | Permission to use, copy, modify, and/or distribute this software for any 9 | purpose with or without fee is hereby granted, provided that the above 10 | copyright notice and this permission notice appear in all copies. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 13 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 14 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 15 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 16 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 17 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 18 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 19 | ``` 20 | 21 | Includes code from string-length@5.0.1, char-regex@2.0.0, strip-ansi@7.0.1, 22 | ansi-regex@6.0.1, used under the terms of the following MIT license: 23 | 24 | ``` 25 | MIT License 26 | 27 | Copyright (c) 2019-2023 Sindre Sorhus (https://sindresorhus.com) 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy of 30 | this software and associated documentation files (the "Software"), to deal in 31 | the Software without restriction, including without limitation the rights to 32 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 33 | of the Software, and to permit persons to whom the Software is furnished to do 34 | so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in all 37 | copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 45 | SOFTWARE. 46 | ``` 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # treporter 2 | 3 | Reporters for node-tap 4 | 5 | An [ink](http://npm.im/ink)-based reporter for use with 6 | [tap](http://npm.im/tap) version 13 and higher. 7 | 8 | ## Built-in Report Types 9 | 10 | ### Base 11 | 12 | The default, and the class to extend to create new reporters. Does all the 13 | things, handles all the edge cases, and ends with a pleasant surprise. 14 | 15 | ### Terse 16 | 17 | A lot like Base, but says a lot less. No timer, no list of tests concurrently 18 | running, nothing printed on test passing. Just the failures and the terse 19 | summary. 20 | 21 | ### Specy 22 | 23 | A `spec` style reporter with the current running jobs and Terse summary and 24 | footer. 25 | 26 | ## Extending 27 | 28 | You can extend this by creating a module whose main `module.exports` is a 29 | child class that extends the `treport.Base` class (which in turn extends 30 | `React.Component`). 31 | 32 | ```js 33 | const React = require('react') 34 | const {Base} = require('treport') 35 | class MyTreportBasedTapReporter extends Base { 36 | // my stuff... 37 | } 38 | module.exports = MyTreportBasedTapReporter 39 | ``` 40 | 41 | To use your module as the test reporter, you'd do this: 42 | 43 | ``` 44 | npm install -D my-treport-based-tap-reporter 45 | tap --reporter=my-treport-based-tap-reporter 46 | ``` 47 | 48 | Tap will `require()` that module, see it's a `React.Component`, and use it 49 | with ink. 50 | 51 | Your child class will get its `constructor()` called with `{tap:tap}`, 52 | which is the root tap object for the test runner. 53 | 54 | It can override the `render()` method, or anything else. In most cases, you 55 | will likely want to override just part of the class, or one of the tags used 56 | for the layout, but the sky is the limit. A child class could also modify the 57 | data being tracked, but leave the tags untouched. 58 | 59 | The following methods describe each of the class methods that can be 60 | overridden. 61 | 62 | ### render() 63 | 64 | The main rendering entry point, as is the React custom. The Base class 65 | returns: 66 | 67 | ```jsx 68 | 69 | 70 | 71 | { this.state.results ? ( 72 | 75 | ) 76 | : '' } 77 | 81 | 82 | ``` 83 | 84 | One of the easiest ways to change the look and feel of the test reporter is to 85 | swap out these components. 86 | 87 | ### get Log() 88 | 89 | A getter function that returns the React component for the "log" section. This 90 | section gets failure/todo/skip results pushed into it, as well as the final 91 | pass/fail/todo/skip result for tests when they complete. Typically, it should 92 | use a `` tag, since this will often get much longer than the height of 93 | the terminal window, and you want to be able to see the results. 94 | 95 | See `state.log` below for more info. 96 | 97 | ### get Runs() 98 | 99 | - `runs` Array of Test objects 100 | 101 | A getter function that returns the React component for the "runs" section. 102 | `this.state.runs` is a list of the tests currently in progress. 103 | 104 | ### get Summary() 105 | 106 | This is a section that shows when the test run is fully completed. It shows a 107 | pretty banner with rainbows, along with any tests that failed, or are marked as 108 | skip or todo. 109 | 110 | ### get Footer() 111 | 112 | This is a section that shows the count of test suites (ie, processes) queued 113 | and completed, a count of assertions completed, and a timer so you can see how 114 | long the test is running for. 115 | 116 | ## State Properties 117 | 118 | The reporter keeps the following state properties up to date as the test 119 | proceeds: 120 | 121 | ### this.state.log 122 | 123 | Array of log objects. Each is one of the following types: 124 | 125 | - `{ raw: }` just a plain old string. 126 | - `{ res: {ok, name, diag, todo, skip, testName} }` A test point 127 | - `{ test: [Test Object] }` A test that has completed 128 | 129 | ### this.state.tests 130 | 131 | All tests are added to this array. In the event of a bailout, everything other 132 | than the bailing-out test is removed, so that the Summary output isn't 133 | cluttered up with a bunch of spurious failures. 134 | 135 | ### this.state.runs 136 | 137 | Array of tests currently running. (When not running in parallel mode, this is 138 | always a single item.) 139 | 140 | ### this.state.results 141 | 142 | The `tap.results` object at the end of the test run. 143 | 144 | ### this.state.assertCounts 145 | 146 | Counts of all assertions run in all tests. `{total, pass, fail, skip, todo}` 147 | 148 | In order to avoid overwhelming the display, updates to assertion counts are 149 | debounced so that they are not updated more than once every 50ms. 150 | 151 | ### this.state.suiteCounts 152 | 153 | Just like `assertCounts`, but for test suites, and not debounced. 154 | 155 | ### this.state.time 156 | 157 | Total elapsed time in ms since the test run started. 158 | 159 | ### this.state.bailout 160 | 161 | When a bailout occurs, this is set to the bailout reason, or `true` if no 162 | reason is given. 163 | 164 | ### this.start 165 | 166 | The `Date.now()` when the test run started. 167 | 168 | ### this.assertCounts 169 | 170 | Updated on each test assertion. Matched to `this.state.assertCounts` at most 171 | once every 50ms. 172 | 173 | ### constructor() 174 | 175 | The constructor receives `tap` as an argument, initializes state, and assigns 176 | appropriate event handlers to keep state up to date. Then, the 177 | `this.tapResume(tap)` method is called to resume and discard the TAP output 178 | from the test harness, since the reporter gets everything it needs from the 179 | events and child test objects. 180 | 181 | ### tapResume(tap) 182 | 183 | Override to prevent the `tap` object from being resumed when calling the Base 184 | constructor. 185 | 186 | ### get time() 187 | 188 | Called to get the current running time. 189 | 190 | ### bailout(bailout, test = null) 191 | 192 | Called when a test bails out. If `test` is set to null, then that means that 193 | the root Tap test is bailing out independently of any child test. (This is 194 | unusual.) 195 | 196 | ### inc(type) 197 | 198 | Called on each assertion to increment `this.assertCounts` and 199 | `this.state.assertCounts`. 200 | 201 | ### addTest(test) 202 | 203 | Called whenever a new test is added to the queue (but before it has started 204 | running). 205 | 206 | ### startTest(test) 207 | 208 | Called when a test starts. 209 | 210 | ### endTest(test) 211 | 212 | Called when a test ends. 213 | 214 | ### endAll(tap) 215 | 216 | Called when the main all tests are done and the tap test runner completes. 217 | 218 | ### logRes(test, res) 219 | 220 | Called when a fail/todo/skip result is emitted from a test, and pushes it onto 221 | the log. 222 | 223 | ### onRaw(test, fd) 224 | 225 | Returns a handler to take all the non-TAP data from a child test, so that it 226 | can be printed to the log with a helpful prefix. In the Base test class, this 227 | prefixes with the name of the test and the file descriptor printed to. 228 | 229 | So, for example, a test named `test/foo.js` would have its stderr output 230 | prefixed with `test/foo.js 2> ...` and its stdout output prefixed with 231 | `test/foo.js 1> ...`. 232 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@isaacs/import-jsx')('./lib/index.js') 2 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | t.ok(require('./index.js')) 3 | -------------------------------------------------------------------------------- /lib/cardinal-theme.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const { 3 | redBright, 4 | red, 5 | green, 6 | cyan, 7 | yellow, 8 | yellowBright, 9 | greenBright, 10 | magenta, 11 | } = chalk 12 | const hex = chalk.hex.bind(chalk) 13 | const bgHex = chalk.bgHex.bind(chalk) 14 | 15 | // this bit of line noise composes all the functions passed into it 16 | // into a single function that calls them all. So, it allows things 17 | // like _(dim, magenta, bgBlue) 18 | // Only need this if using multiple chalk functions together. 19 | // const _ = (h, ...t) => x => h ? _(...t)(h(x)) : x 20 | 21 | // squash chalk functions into single-arg, they get confused about the 22 | // second argument sometimes 23 | const _ = h => x => h(x) 24 | 25 | 26 | // Change the below definitions in order to tweak the color theme. 27 | module.exports = { 28 | 'Boolean': { 29 | 'true' : undefined 30 | , 'false' : undefined 31 | , _default : _(redBright) 32 | } 33 | 34 | , 'Identifier': { 35 | 'undefined' : undefined 36 | , 'self' : _(redBright) 37 | , 'console' : _(cyan) 38 | , 'log' : _(cyan) 39 | , 'warn' : _(red) 40 | , 'error' : _(redBright) 41 | , _default : _(hex('#eee')) 42 | } 43 | 44 | , 'Null': { 45 | _default: _(hex('#999')) 46 | } 47 | 48 | , 'Numeric': { 49 | _default: _(cyan) 50 | } 51 | 52 | , 'String': { 53 | _default: function(s, info) { 54 | var nextToken = info.tokens[info.tokenIndex + 1] 55 | 56 | // show keys of object literals and json in different color 57 | return (nextToken && nextToken.type === 'Punctuator' && nextToken.value === ':') 58 | ? green(s) 59 | : greenBright(s) 60 | } 61 | } 62 | 63 | , 'Keyword': { 64 | 'break' : undefined 65 | 66 | , 'case' : undefined 67 | , 'catch' : _(cyan) 68 | , 'class' : undefined 69 | , 'const' : undefined 70 | , 'continue' : undefined 71 | 72 | , 'debugger' : undefined 73 | , 'default' : undefined 74 | , 'delete' : _(red) 75 | , 'do' : undefined 76 | 77 | , 'else' : undefined 78 | , 'enum' : undefined 79 | , 'export' : undefined 80 | , 'extends' : undefined 81 | 82 | , 'finally' : _(cyan) 83 | , 'for' : undefined 84 | , 'function' : undefined 85 | 86 | , 'if' : undefined 87 | , 'implements' : undefined 88 | , 'import' : undefined 89 | , 'in' : undefined 90 | , 'instanceof' : undefined 91 | , 'let' : undefined 92 | , 'new' : _(red) 93 | , 'package' : undefined 94 | , 'private' : undefined 95 | , 'protected' : undefined 96 | , 'public' : undefined 97 | , 'return' : _(red) 98 | , 'static' : undefined 99 | , 'super' : undefined 100 | , 'switch' : undefined 101 | 102 | , 'this' : _(redBright) 103 | , 'throw' : undefined 104 | , 'try' : _(cyan) 105 | , 'typeof' : undefined 106 | 107 | , 'var' : _(green) 108 | , 'void' : undefined 109 | 110 | , 'while' : undefined 111 | , 'with' : undefined 112 | , 'yield' : undefined 113 | , _default : _(cyan) 114 | } 115 | , 'Punctuator': { 116 | ';': undefined 117 | , '.': _(green) 118 | , ',': _(green) 119 | 120 | , '{': _(yellow) 121 | , '}': _(yellow) 122 | , '(': undefined 123 | , ')': undefined 124 | , '[': _(yellow) 125 | , ']': _(yellow) 126 | 127 | , '<': undefined 128 | , '>': undefined 129 | , '+': undefined 130 | , '-': undefined 131 | , '*': undefined 132 | , '%': undefined 133 | , '&': undefined 134 | , '|': undefined 135 | , '^': undefined 136 | , '!': undefined 137 | , '~': undefined 138 | , '?': undefined 139 | , ':': undefined 140 | , '=': undefined 141 | 142 | , '<=': undefined 143 | , '>=': undefined 144 | , '==': undefined 145 | , '!=': undefined 146 | , '++': undefined 147 | , '--': undefined 148 | , '<<': undefined 149 | , '>>': undefined 150 | , '&&': undefined 151 | , '||': undefined 152 | , '+=': undefined 153 | , '-=': undefined 154 | , '*=': undefined 155 | , '%=': undefined 156 | , '&=': undefined 157 | , '|=': undefined 158 | , '^=': undefined 159 | , '/=': undefined 160 | , '=>': undefined 161 | , '**': undefined 162 | 163 | , '===': undefined 164 | , '!==': undefined 165 | , '>>>': undefined 166 | , '<<=': undefined 167 | , '>>=': undefined 168 | , '...': undefined 169 | , '**=': undefined 170 | 171 | , '>>>=': undefined 172 | 173 | , _default: _(yellowBright) 174 | } 175 | 176 | // line comment 177 | , Line: { 178 | _default: _(green) 179 | } 180 | 181 | /* block comment */ 182 | , Block: { 183 | _default: _(green) 184 | } 185 | 186 | // JSX 187 | , JSXAttribute: { 188 | _default: _(magenta) 189 | } 190 | , JSXClosingElement: { 191 | _default: _(magenta) 192 | } 193 | , JSXElement: { 194 | _default: _(magenta) 195 | } 196 | , JSXEmptyExpression: { 197 | _default: _(magenta) 198 | } 199 | , JSXExpressionContainer: { 200 | _default: _(magenta) 201 | } 202 | , JSXIdentifier: { 203 | className: _(cyan) 204 | , _default: _(magenta) 205 | } 206 | , JSXMemberExpression: { 207 | _default: _(magenta) 208 | } 209 | , JSXNamespacedName: { 210 | _default: _(magenta) 211 | } 212 | , JSXOpeningElement: { 213 | _default: _(magenta) 214 | } 215 | , JSXSpreadAttribute: { 216 | _default: _(magenta) 217 | } 218 | , JSXText: { 219 | _default: _(greenBright) 220 | } 221 | 222 | , _default: _(hex('#ff0000')) 223 | } 224 | -------------------------------------------------------------------------------- /lib/cardinal-theme.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const theme = require('./cardinal-theme.js') 3 | t.match(theme, { Boolean: { _default: Function }}) 4 | 5 | const c = require('chalk') 6 | const key = theme.String._default('key', { 7 | tokens: ['key', { 8 | type: 'Punctuator', 9 | value: ':', 10 | }], 11 | tokenIndex: 0, 12 | }) 13 | t.equal(key, c.green('key'), 'keys are green') 14 | const str = theme.String._default('str', { 15 | tokens: ['str'], 16 | tokenIndex: 0, 17 | }) 18 | t.equal(str, c.greenBright('str'), 'strings are bright') 19 | 20 | t.equal(theme.Boolean._default('true'), c.redBright('true'), 'red as boold') 21 | -------------------------------------------------------------------------------- /lib/color.js: -------------------------------------------------------------------------------- 1 | // bringing back the Color tag removed in ink 3.0 2 | const c = require('chalk') 3 | const React = require('react') 4 | const {memo} = React 5 | const {Transform} = require('ink') 6 | const arrify = obj => Array.isArray(obj) ? obj 7 | : obj === '' || obj === null || obj === undefined ? [] 8 | : [obj] 9 | 10 | const methods = [ 11 | 'hex', 12 | 'hsl', 13 | 'hsv', 14 | 'hwb', 15 | 'rgb', 16 | 'keyword', 17 | 'bgHex', 18 | 'bgHsl', 19 | 'bgHsv', 20 | 'bgHwb', 21 | 'bgRgb', 22 | 'bgKeyword', 23 | 'ansi', 24 | 'ansi256', 25 | 'bgAnsi', 26 | 'bgAnsi256', 27 | ] 28 | 29 | const Color = ({children, ...colorProps}) => { 30 | if (children === '') 31 | return null 32 | 33 | const transform = children => { 34 | for (const [method, value] of Object.entries(colorProps)) { 35 | if (methods.includes(method)) 36 | children = c[method](...arrify(value))(children) 37 | else if (typeof c[method] === 'function') 38 | children = c[method](children) 39 | } 40 | return children 41 | } 42 | 43 | return ({children}) 44 | } 45 | Color.displayName = 'Color' 46 | 47 | Color.defaultProps = { children: '' } 48 | 49 | module.exports = memo(Color) 50 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const {render} = require('ink') 3 | const importJSX = require('@isaacs/import-jsx') 4 | /* istanbul ignore next */ 5 | const reporter = { 6 | report (tap, Type = 'base') { 7 | // NB: React will only render as a tag if it's capitalized 8 | if (typeof Type === 'function' && Type.prototype.isReactComponent) 9 | return render() 10 | 11 | if (typeof Type !== 'string' || !types.includes(Type)) 12 | throw new Error('unsupported report type: ' + Type) 13 | 14 | const Report = importJSX('./reports/' + Type) 15 | render() 16 | } 17 | } 18 | 19 | /* istanbul ignore next */ 20 | module.exports = (...args) => reporter.report(...args) 21 | 22 | const types = module.exports.types = require('../types.js') 23 | const cap = s => s.replace(/^./, $0 => $0.toUpperCase()) 24 | types.forEach(type => 25 | Object.defineProperty(module.exports, cap(type), { 26 | get: () => importJSX(`./reports/${type}`), 27 | enumerable: true, 28 | })) 29 | -------------------------------------------------------------------------------- /lib/index.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const index = require('@isaacs/import-jsx')('./index.js') 3 | t.ok(index) 4 | t.ok(index.types.includes('base')) 5 | t.type(index.Base, 'function') 6 | -------------------------------------------------------------------------------- /lib/pretty-diff.js: -------------------------------------------------------------------------------- 1 | const slen = require('./string-length.js') 2 | const c = require('chalk') 3 | 4 | const green = s => 5 | c.supportsColor.level >= 2 ? c.ansi256(22).bgAnsi256(193)(s) : c.green(s) 6 | 7 | const red = s => 8 | c.supportsColor.level >= 2 ? c.ansi256(52).bgAnsi256(218)(s) : c.red(s) 9 | 10 | const ctx = s => { 11 | const f = s.match(/^(\@\@.*?\@\@)( .*)$/) 12 | return f ? frag(f[1]) + ctxExtra(f[2]) 13 | : frag(s) 14 | } 15 | 16 | const frag = s => 17 | c.supportsColor.level >= 2 ? c.bold.bgHex('#fff').ansi256(165)(s) 18 | : c.bold.magenta(s) 19 | 20 | const ctxExtra = s => 21 | c.supportsColor.level >= 2 ? c.bgHex('#fff').ansi256(68).bold(s) 22 | : c.reset.bold.dim(s) 23 | 24 | const white = s => 25 | c.supportsColor.level >= 2 ? c.bgHex('#fff').hex('#111')(s) : c.reset(s) 26 | 27 | const repeat = (n, c) => new Array(Math.max(n + 1, 0)).join(c) 28 | 29 | module.exports = patch => { 30 | if (!patch) 31 | return null 32 | 33 | const columns = process.stdout.columns || 80 34 | let width = 0 35 | const maxLen = Math.max(columns - 5, 0) 36 | return patch.trimRight().split('\n').filter((line, index) => { 37 | if (slen(line) > width) 38 | width = Math.min(maxLen, slen(line)) 39 | return !( 40 | line.match(/^\=+$/) || 41 | line === '\\ No newline at end of file' 42 | ) 43 | }).map(line => 44 | slen(line) <= width 45 | ? line + repeat(width - slen(line) + 1, ' ') 46 | : line 47 | ).map(line => 48 | line.charAt(0) === '+' ? green(line) 49 | : line.charAt(0) === '-' ? red(line) 50 | : line.charAt(0) === '@' ? ctx(line) 51 | : white(line) 52 | ).map(l => ` ${l}`).join('\n').trimRight() 53 | } 54 | -------------------------------------------------------------------------------- /lib/pretty-diff.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const pretty = require('./pretty-diff.js') 3 | 4 | t.equal(pretty(), null) 5 | 6 | const diff = `--- expected 7 | +++ actual 8 | @@ fragment without context @@ 9 | @@ -1500,10 +1500,7 @@ Object { 10 | 499, 11 | ], 12 | "500x": Array [ 13 | - 1000, 14 | + 500, 15 | - 501, 16 | - 502, 17 | - 503, 18 | ], 19 | "501x": Array [ 20 | 501, 21 | @@ -3002,8 +2999,4 @@ Object { 22 | "999x": Array [ 23 | 999, 24 | ], 25 | - "500x_": Array [ 26 | - 501, 27 | - ], 28 | - "500y": 500, 29 | } 30 | ` 31 | 32 | process.stdout.columns = 40 33 | t.matchSnapshot(pretty(diff), 'a pretty diff at 40 columns') 34 | 35 | process.stdout.columns = 2 36 | t.matchSnapshot(pretty(diff), 'a pretty diff at 2 columns') 37 | 38 | process.stdout.columns = 0 39 | t.matchSnapshot(pretty(diff), 'a pretty diff at 0 columns') 40 | 41 | process.stdout.columns = 40 42 | require('chalk').supportsColor.level = 1 43 | t.matchSnapshot(pretty(diff), 'a pretty diff without ansi support') 44 | -------------------------------------------------------------------------------- /lib/pretty-source.js: -------------------------------------------------------------------------------- 1 | const {highlightFileSync} = require('cardinal') 2 | const theme = require('./cardinal-theme.js') 3 | const c = require('chalk') 4 | const { red, bold } = c 5 | const slen = require('./string-length.js') 6 | 7 | // lol 8 | const lp = (s,n) => `${ 9 | new Array(n - slen(''+s) + 1).join(' ') 10 | }${s}` 11 | 12 | const gray = s => 13 | c.supportsColor.level >= 2 ? c.hex('#aaa')(s) : c.gray(s) 14 | 15 | const dim = s => 16 | c.supportsColor.level >= 2 ? c.hex('#777')(s) : c.dim(s) 17 | 18 | module.exports = (diag) => { 19 | if (!diag || typeof diag.source !== 'string') 20 | return null 21 | 22 | if (!diag.source || !diag.at || !diag.at.line || !diag.at.file) 23 | return diag.source 24 | 25 | const source = diag.source 26 | 27 | const at = diag.at 28 | try { 29 | const lines = highlightFileSync(at.file, { 30 | jsx: true, 31 | theme, 32 | }).replace(/\n$/, '').split('\n') 33 | 34 | const ctx = 3 35 | const startLine = Math.max(at.line - ctx, 0) 36 | const endLine = Math.min(at.line + ctx, lines.length) 37 | const numLen = endLine.toString().length + 1 38 | 39 | const line = (lines[at.line - 1]) 40 | const caret = at.column && at.column <= slen(line) 41 | ? [ 42 | ` ${new Array(numLen).join(' ') 43 | } ` + dim('| ') + 44 | c.red(`${new Array(at.column).join('-')}${c.bold('^')}`) 45 | ] : [] 46 | 47 | const title = gray(' ' + at.file) 48 | const before = lines.slice(startLine, at.line - 1) 49 | const after = lines.slice(at.line, endLine) 50 | 51 | const context = [title].concat(before.map((l, i) => 52 | ` ${dim(lp(i + startLine + 1, numLen)+ ' | ')}${l}`)) 53 | .concat( 54 | red(bold(`>`)) + 55 | lp(at.line, numLen) + 56 | dim(' | ') + 57 | line 58 | ) 59 | .concat(caret) 60 | .concat(after.map((l, i) => 61 | ` ${dim(lp(i + at.line + 1, numLen) + ' | ')}${l}`)) 62 | 63 | const cols = process.stdout.columns || 80 64 | const lineLength = Math.min(cols - 2, 65 | Math.max(...(context.map(l => slen(l) + 3)))) 66 | 67 | const csplit = context.map(line => { 68 | const len = Math.max(1, lineLength - slen(line)) 69 | return c.bgHex('#222')(line + new Array(len).join(' ')) 70 | }).join('\n') 71 | 72 | delete diag.at 73 | return csplit + '\n' 74 | } catch (er) { 75 | return source 76 | } 77 | // should be impossible, but just in case 78 | /* istanbul ignore next */ 79 | return source 80 | } 81 | -------------------------------------------------------------------------------- /lib/pretty-source.test.js: -------------------------------------------------------------------------------- 1 | const pretty = require('./pretty-source.js') 2 | const t = require('tap') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const c = require('chalk') 6 | c.supportsColor.level = 3 7 | 8 | t.cleanSnapshot = s => s.split(process.cwd()).join('{CWD}') 9 | 10 | t.test('basic null responses', t => { 11 | t.equal(pretty(), null, 'no diag') 12 | t.equal(pretty({}), null, 'no source') 13 | t.equal(pretty({source:{}}), null, 'source not string') 14 | t.equal(pretty({source:'foo'}), 'foo', 'no at') 15 | t.equal(pretty({source:'foo',at:{}}), 'foo', 'no line') 16 | t.equal(pretty({source:'foo',at:{line:1}}), 'foo', 'no file') 17 | t.equal(pretty({source:'foo',at:{file:'does not exist',line:99,column:18}}), 18 | 'foo', 'nonexistent file') 19 | t.end() 20 | }) 21 | 22 | t.test('highlight a file', t => { 23 | const file = path.resolve(__dirname, '../delete-me.js') 24 | t.teardown(() => fs.unlinkSync(file)) 25 | fs.writeFileSync(file, `const line0 = 'line 0' 26 | const line1 = 'line 1' 27 | const line2 = 'line 2' 28 | const line3 = 'line 3' 29 | const line4 = 'line 4' 30 | const line5 = 'line 5' 31 | const line6 = 'line 6' 32 | const line7 = 'line 7' 33 | const line8 = 'line 8' 34 | const line9 = 'line 9' 35 | `) 36 | t.matchSnapshot(pretty({ 37 | source:'this does not matter', 38 | at: { 39 | file, 40 | line: 4, 41 | column: 5, 42 | } 43 | }), 'with the caret') 44 | t.matchSnapshot(pretty({ 45 | source:'this does not matter', 46 | at: { 47 | file, 48 | line: 4, 49 | } 50 | }), 'no caret') 51 | t.matchSnapshot(pretty({ 52 | source:'this does not matter', 53 | at: { 54 | file, 55 | line: 10, 56 | } 57 | }), 'last line, no caret') 58 | t.matchSnapshot(pretty({ 59 | source:'this does not matter', 60 | at: { 61 | file, 62 | column: 5, 63 | line: 10, 64 | } 65 | }), 'last line, with caret') 66 | t.matchSnapshot(pretty({ 67 | source:'this does not matter', 68 | at: { 69 | file, 70 | line: 2, 71 | column: 420, 72 | } 73 | }), 'caret way too far off the line') 74 | c.supportsColor.level = 1 75 | t.matchSnapshot(pretty({ 76 | source:'this does not matter', 77 | at: { 78 | file, 79 | line: 4, 80 | column: 5, 81 | } 82 | }), 'slightly less colorful') 83 | t.end() 84 | }) 85 | -------------------------------------------------------------------------------- /lib/reports/base/assert-counts.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const {Box, Text} = require('ink') 3 | const importJSX = require('@isaacs/import-jsx') 4 | const Color = importJSX('../../color.js') 5 | const Reset = importJSX('../../reset.js') 6 | 7 | module.exports = ({fail, pass, todo, skip}) => ( 8 | 9 | 10 | Asserts: 11 | 12 | { !fail && !pass && !todo && !skip ? '0 ' : '' } 13 | { fail ? ( 14 | 15 | {fail} failed 16 | {', '} 17 | 18 | ) : } 19 | { pass ? ( 20 | 21 | {pass} passed 22 | {', '} 23 | 24 | ) : } 25 | { todo ? ( 26 | 27 | {todo} todo 28 | {', '} 29 | 30 | ) : } 31 | { skip ? ( 32 | 33 | {skip} skip 34 | {', '} 35 | 36 | ) : } 37 | 38 | of {pass + fail + todo + skip} 39 | 40 | 41 | ) 42 | -------------------------------------------------------------------------------- /lib/reports/base/assert-counts.test.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const importJSX = require('@isaacs/import-jsx') 3 | const {render} = require('ink-testing-library') 4 | const t = require('tap') 5 | const AssertCounts = importJSX('./assert-counts.js') 6 | 7 | const cases = [ 8 | [0, 0, 0, 0], 9 | [0, 0, 0, 1], 10 | [0, 0, 1, 0], 11 | [0, 0, 1, 1], 12 | [0, 1, 0, 0], 13 | [0, 1, 0, 1], 14 | [0, 1, 1, 0], 15 | [0, 1, 1, 1], 16 | [1, 0, 0, 0], 17 | [1, 0, 0, 1], 18 | [1, 0, 1, 0], 19 | [1, 0, 1, 1], 20 | [1, 1, 0, 0], 21 | [1, 1, 0, 1], 22 | [1, 1, 1, 0], 23 | [1, 1, 1, 1], 24 | ] 25 | 26 | let r 27 | cases.forEach(c => { 28 | const [fail, pass, todo, skip] = c 29 | const tag = 30 | if (!r) 31 | r = render(tag) 32 | else 33 | r.rerender(tag) 34 | t.matchSnapshot(r.lastFrame(), JSON.stringify({fail, pass, todo, skip})) 35 | }) 36 | -------------------------------------------------------------------------------- /lib/reports/base/assert-name.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const {Box, Text} = require('ink') 3 | const importJSX = require('@isaacs/import-jsx') 4 | const Color = importJSX('../../color.js') 5 | 6 | const glyphColor = ({ ok, skip, todo }) => ({ 7 | [ skip ? 'cyan' 8 | : todo ? 'magenta' 9 | : !ok ? 'red' 10 | : 'green']: true, 11 | }) 12 | 13 | const glyphText = ({ ok, skip, todo }) => 14 | skip ? ' ~ ' 15 | : todo ? ' ☐ ' 16 | : !ok ? ' ✖ ' 17 | : ' ✓ ' 18 | 19 | const Glyph = ({ ok, skip, todo }) => ( 20 | 21 | 22 | {glyphText({ok, skip, todo})} 23 | 24 | 25 | ) 26 | 27 | const Reason = ({skip, todo}) => 28 | skip && skip !== true ? ( 29 | 30 | {' > '} 31 | {skip} 32 | 33 | ) 34 | : todo && todo !== true ? ( 35 | 36 | {' > '} 37 | {todo} 38 | 39 | ) 40 | : 41 | 42 | const AssertName = ({ ok, name, skip, todo }) => ( 43 | 44 | 45 | {name || '(unnamed test)'} 46 | 47 | 48 | ) 49 | 50 | module.exports = AssertName 51 | -------------------------------------------------------------------------------- /lib/reports/base/assert-name.test.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const importJSX = require('@isaacs/import-jsx') 3 | const t = require('tap') 4 | const AssertName = importJSX('./assert-name.js') 5 | const {render} = require('ink-testing-library') 6 | 7 | const ok = [ true, false ] 8 | const skip = [false, true, 'skip reason'] 9 | const todo = [false, true, 'todo reason'] 10 | const name = ['name', undefined] 11 | const cases = [] 12 | 13 | ok.forEach(ok => 14 | skip.forEach(skip => 15 | todo.forEach(todo => 16 | name.forEach(name => cases.push({ok, skip, todo, name}))))) 17 | 18 | const r = render() 19 | t.matchSnapshot(r.lastFrame(), JSON.stringify(cases[0])) 20 | for (let i = 1; i < cases.length; i++) { 21 | r.rerender() 22 | t.matchSnapshot(r.lastFrame(), JSON.stringify(cases[i])) 23 | } 24 | 25 | r.unmount() 26 | -------------------------------------------------------------------------------- /lib/reports/base/counts.js: -------------------------------------------------------------------------------- 1 | const c = require('chalk') 2 | module.exports = ({pass, fail, todo, skip, total}) => 3 | c.white( 4 | (fail ? c.red(' '+fail+' failed') : '') + 5 | (todo ? c.magenta(' '+todo+' todo') : '') + 6 | (skip ? c.cyan(' '+skip+' skip') : '') + 7 | (skip || todo || fail ? ' of' : '') + 8 | (total ? c.bold(' '+total) : '') + 9 | (total && pass === total ? c.green(' OK ') : ' ') 10 | ) 11 | -------------------------------------------------------------------------------- /lib/reports/base/counts.test.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const counts = require('./counts.js') 3 | 4 | const cases = [ 5 | [0,0,0,0], 6 | [0,0,0,1], 7 | [0,0,1,0], 8 | [0,0,1,1], 9 | [0,1,0,0], 10 | [0,1,0,1], 11 | [0,1,1,0], 12 | [0,1,1,1], 13 | [1,0,0,0], 14 | [1,0,0,1], 15 | [1,0,1,0], 16 | [1,0,1,1], 17 | [1,1,0,0], 18 | [1,1,0,1], 19 | [1,1,1,0], 20 | [1,1,1,1], 21 | [0,0,0,2], 22 | [0,0,2,0], 23 | [0,0,2,2], 24 | [0,2,0,0], 25 | [0,2,0,2], 26 | [0,2,2,0], 27 | [0,2,2,2], 28 | [2,0,0,0], 29 | [2,0,0,2], 30 | [2,0,2,0], 31 | [2,0,2,2], 32 | [2,2,0,0], 33 | [2,2,0,2], 34 | [2,2,2,0], 35 | [2,2,2,2], 36 | ] 37 | 38 | cases.forEach(set => { 39 | const [pass, fail, todo, skip] = set 40 | const total = pass + fail + todo + skip 41 | t.matchSnapshot(counts({pass, fail, todo, skip, total}), 42 | JSON.stringify({pass, fail, todo, skip, total})) 43 | }) 44 | -------------------------------------------------------------------------------- /lib/reports/base/footer.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const ms = require('ms') 3 | const {Box, Text} = require('ink') 4 | const importJSX = require('@isaacs/import-jsx') 5 | const Color = importJSX('../../color.js') 6 | const Reset = importJSX('../../reset.js') 7 | const AssertCounts = importJSX('./assert-counts.js') 8 | const SuiteCounts = importJSX('./suite-counts.js') 9 | 10 | module.exports = ({suiteCounts, assertCounts, time}) => ( 11 | 12 | 13 | 14 | 15 | 16 | Time: 17 | 18 | 19 | {ms(time)} 20 | 21 | 22 | 23 | ) 24 | -------------------------------------------------------------------------------- /lib/reports/base/footer.test.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const importJSX = require('@isaacs/import-jsx') 3 | const t = require('tap') 4 | const {render} = require('ink-testing-library') 5 | const Footer = importJSX('./footer.js') 6 | 7 | // not too terribly much to do here! 8 | const [pass, fail, skip, todo, total] = [1,1,1,1,4] 9 | const r = render(