@small-tech/tap-monkey 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.4.0] - 2022-05-24
8
+
9
+ Testy monkey.
10
+
11
+ ## Added
12
+
13
+ - 100% code coverage. Now using tape and *drumroll* Tap Monkey itself for the tests.
14
+
7
15
  ## [1.3.0] - 2021-03-05
8
16
 
9
17
  Debug monkey.
package/README.md CHANGED
@@ -107,6 +107,22 @@ export default function log (...args) {
107
107
 
108
108
  If your test runner (e.g., [ESM Tape Runner](https://github.com/small-tech/esm-tape-runner)) encounters an error in your app’s source code (e.g., a syntax error in one of your classes), it should bail out (and hopefully also log the error so you know what it is). Tap Monkey will respond to bail out events by displaying them and exiting (your test runner should have exit also so you should not get any pipe errors).
109
109
 
110
+ ## Testing Tap Monkey
111
+
112
+ Tap Monkey itself, of course, comes with unit tests displayed by none other than _\*drumroll\*_ Tap Monkey!
113
+
114
+ ### Run tests
115
+
116
+ ```shell
117
+ npm run -s test
118
+ ```
119
+
120
+ ### Run coverage
121
+
122
+ ```shell
123
+ npm run -s coverage
124
+ ```
125
+
110
126
  ## Like this? Fund us!
111
127
 
112
128
  [Small Technology Foundation](https://small-tech.org) is a tiny, independent not-for-profit.
package/index.js CHANGED
@@ -16,14 +16,15 @@ import Ora from 'ora'
16
16
  import chalk from 'chalk'
17
17
  import { performance } from 'perf_hooks'
18
18
  import tapOut from '@small-tech/tap-out'
19
-
20
- let hasFailures = false
19
+ import os from 'os'
21
20
 
22
21
  // The formatter has a --quiet option that stops status updates being
23
22
  // printed until there is a failure or until the aggregate statistics is
24
23
  // being shown. People using screen readers and other assistive technologies
25
24
  // might want to use this if the number of status updates becomes overwhelming.
26
- let quiet = (process.argv.length === 3 && process.argv[2] === '--quiet')
25
+ export const context = {
26
+ quiet: (process.argv.length === 3 && process.argv[2] === '--quiet')
27
+ }
27
28
 
28
29
  // Due to the 300ms frame duration of the monkey animation, not every
29
30
  // status update we receive about new test suites and test passes will be
@@ -54,23 +55,22 @@ let printingCoverage = false
54
55
  let coverageBorderCount = 0
55
56
  let currentTest = ''
56
57
 
57
- parser.on('test', test => {
58
- spinner.start()
59
- if (!quiet) {
60
- currentTest = test.name
61
- spinner.text = `Running ${chalk.underline(currentTest)} tests`
58
+ const passHandler = (assert => {
59
+ if (!context.quiet) {
60
+ spinner.text = `${chalk.green('✔')} ${assert.name}`
62
61
  }
63
62
  })
64
63
 
65
- parser.on('pass', assert => {
66
- if (!quiet) {
67
- spinner.text = `${chalk.green('✔')} ${assert.name}`
64
+ const testHandler = (test => {
65
+ spinner.start()
66
+ if (!context.quiet) {
67
+ currentTest = test.name
68
+ spinner.text = `Running ${chalk.underline(currentTest)} tests`
68
69
  }
69
70
  })
70
71
 
71
- parser.on('fail', assert => {
72
+ const failHandler = (assert => {
72
73
  // Stop the spinner and output failures in full.
73
- hasFailures = true
74
74
  spinner.stop()
75
75
 
76
76
  const e = assert.error
@@ -80,18 +80,18 @@ parser.on('fail', assert => {
80
80
  if (e.operator !== undefined) console.log(` operator:`, e.operator)
81
81
  if (e.expected !== undefined) console.log(` ${chalk.green(`expected: ${e.expected}`)}`)
82
82
  if (e.actual !== undefined) console.log(` ${chalk.red(`actual : ${e.actual}`)}`)
83
- if (e.at !== undefined) console.log(` ${chalk.yellow(`at : ${e.at}`)}`)
83
+ if (e.at !== undefined) console.log(` ${chalk.yellow(`at : ${e.at.file.replace(os.homedir(), '~')}:${e.at.line}:${e.at.character}`)}`)
84
84
 
85
85
  console.log()
86
86
 
87
87
  e.stack.split('\n').forEach(line => {
88
- console.log(' ', chalk.gray(line))
88
+ console.log(' ', chalk.red(line))
89
89
  })
90
90
 
91
91
  spinner.start()
92
92
  })
93
93
 
94
- parser.on('bailOut', event => {
94
+ const bailOutHandler = (event => {
95
95
  // If the test runner has emitted a bail out event, it has signaled
96
96
  // that it cannot continue. So we notify the person and exit.
97
97
  spinner.stop()
@@ -100,10 +100,10 @@ parser.on('bailOut', event => {
100
100
  process.exit(1)
101
101
  })
102
102
 
103
- parser.on('comment', comment => {
103
+ const commentHandler = (comment => {
104
104
  spinner.stop()
105
105
  let commentText = comment.raw
106
-
106
+
107
107
  const isCoverageBorder = commentText.startsWith('----')
108
108
  if (isCoverageBorder) { printingCoverage = true }
109
109
 
@@ -125,12 +125,12 @@ parser.on('comment', comment => {
125
125
  } else {
126
126
  // We aren’t printing coverage yet so this must be a regular TAP comment.
127
127
  // Display it fully.
128
- console.log(chalk.yellow(' 🢂 '),commentText.trim())
128
+ console.log(chalk.yellow(' 🢂 '), commentText.trim())
129
129
  spinner.start()
130
130
  }
131
131
  })
132
132
 
133
- parser.on('output', results => {
133
+ const outputHandler = (results => {
134
134
  const duration = ((performance.now() - startTime)/1000).toFixed(2)
135
135
  spinner.stop()
136
136
 
@@ -138,7 +138,9 @@ parser.on('output', results => {
138
138
  const passing = results.pass.length
139
139
  const failing = results.fail.length
140
140
 
141
- if (hasFailures) {
141
+ // TODO: Handle edge case of zero total tests.
142
+
143
+ if (failing > 0) {
142
144
  console.log(` 🙊️ ${chalk.magenta('There are failed tests.')}`)
143
145
  } else {
144
146
  console.log(` 🍌️ ${chalk.green('All tests passing!')}`)
@@ -150,3 +152,13 @@ parser.on('output', results => {
150
152
  console.log(chalk.red( ` Failing ${failing}`))
151
153
  console.log(chalk.gray( ` Duration ${duration} secs`))
152
154
  })
155
+
156
+ parser.on('test', testHandler)
157
+ parser.on('pass', passHandler)
158
+ parser.on('fail', failHandler)
159
+ parser.on('bailOut', bailOutHandler)
160
+ parser.on('comment', commentHandler)
161
+ parser.on('output', outputHandler)
162
+
163
+ export default { testHandler, passHandler, failHandler, bailOutHandler, commentHandler, outputHandler, parser, spinner }
164
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@small-tech/tap-monkey",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "A tap formatter that’s also a monkey.",
5
5
  "keywords": [
6
6
  "tap",
@@ -19,7 +19,8 @@
19
19
  "tap-monkey": "./index.js"
20
20
  },
21
21
  "scripts": {
22
- "test": "echo \"Error: no test specified\" && exit 1"
22
+ "test": "tape tests/index.js | node index.js",
23
+ "coverage": "c8 tape tests/index.js | node index.js"
23
24
  },
24
25
  "author": {
25
26
  "name": "Aral Balkan",
@@ -36,5 +37,10 @@
36
37
  "@small-tech/tap-out": "^3.2.0",
37
38
  "chalk": "^4.1.0",
38
39
  "ora": "^5.3.0"
40
+ },
41
+ "devDependencies": {
42
+ "c8": "^7.11.3",
43
+ "strip-ansi": "^7.0.1",
44
+ "tape": "^5.5.3"
39
45
  }
40
46
  }
package/tests/index.js ADDED
@@ -0,0 +1,295 @@
1
+ import test from 'tape'
2
+ import tapMonkey from '../index.js'
3
+ import { context } from '../index.js'
4
+ import strip from 'strip-ansi'
5
+
6
+ // Since Tap Monkey pipes stdin, this will leave a handle open.
7
+ // We have to destroy stdin when all tests are done for the
8
+ // runner to exit properly.
9
+ // See https://github.com/nodejs/node/issues/32291
10
+
11
+ test.onFinish(() => {
12
+ process.stdin.destroy()
13
+ })
14
+
15
+ //
16
+ // Test handler and general tests.
17
+ //
18
+
19
+ test('test handler', t => {
20
+ context.quiet = false
21
+ tapMonkey.testHandler({name: 'mock'})
22
+ tapMonkey.spinner.stop()
23
+
24
+ t.strictEquals(tapMonkey.spinner._spinner.interval, 300, 'spinner interval is as expected')
25
+ t.strictEquals(tapMonkey.spinner._spinner.frames[0], ' 🙈 ', 'animation frame 1 is correct')
26
+ t.strictEquals(tapMonkey.spinner._spinner.frames[1], ' 🙈 ', 'animation frame 2 is correct')
27
+ t.strictEquals(tapMonkey.spinner._spinner.frames[2], ' 🙉 ', 'animation frame 3 is correct')
28
+ t.strictEquals(tapMonkey.spinner._spinner.frames[3], ' 🙊 ', 'animation frame 4 is correct')
29
+
30
+ t.true(strip(tapMonkey.spinner.text).includes('Running mock tests'), 'test name is displayed correctly')
31
+
32
+ context.quiet = true
33
+ tapMonkey.testHandler({name: 'quiet mock'})
34
+ tapMonkey.spinner.stop()
35
+
36
+ t.false(strip(tapMonkey.spinner.text).includes('Running quiet mock tests'), 'test name not displayed in quiet mode')
37
+
38
+ t.end()
39
+ })
40
+
41
+ //
42
+ // Pass handler tests.
43
+ //
44
+
45
+ test('pass handler', t => {
46
+ // Quiet passes (the default)
47
+ context.quiet = true
48
+ const quietPass = 'a quiet pass'
49
+ tapMonkey.passHandler({ name: quietPass })
50
+ t.false(tapMonkey.spinner.text.includes(quietPass), 'quiet mode should not display passed tests')
51
+
52
+ // Loud passes.
53
+ context.quiet = false
54
+ const loudPass = 'a loud pass'
55
+ tapMonkey.passHandler({ name: loudPass })
56
+ t.true(tapMonkey.spinner.text.includes(loudPass), 'passed tests should display when quiet mode is off')
57
+
58
+ t.end()
59
+ })
60
+
61
+ //
62
+ // Fail handler tests.
63
+ //
64
+
65
+ test('fail handler', t => {
66
+ // Capture console log temporarily.
67
+ const originalConsoleLog = console.log
68
+ let output = ''
69
+ const capturedConsoleLog = (...args) => output += args.join(' ')
70
+ console.log = capturedConsoleLog
71
+
72
+ const mockAssertionWithTypeError = {
73
+ type: 'assert',
74
+ raw: 'not ok 2 TypeError: Cannot convert undefined or null to object',
75
+ ok: false,
76
+ number: 2,
77
+ name: 'TypeError: Cannot convert undefined or null to object',
78
+ error: {
79
+ operator: 'error',
80
+ expected: undefined,
81
+ actual: undefined,
82
+ at: {
83
+ file: '/var/home/aral/Projects/nodekit/node_modules/tape-promise/node_modules/onetime/index.js',
84
+ line: '30',
85
+ character: '12'
86
+ },
87
+ stack: 'TypeError: Cannot convert undefined or null to object\n' +
88
+ 'at Function.keys (<anonymous>)\n' +
89
+ 'at sortResults (file:///var/home/aral/Projects/nodekit/tests/files.js:17:10)\n' +
90
+ 'at file:///var/home/aral/Projects/nodekit/tests/files.js:101:58\n' +
91
+ 'at processTicksAndRejections (node:internal/process/task_queues:96:5)\n',
92
+ raw: ' operator: error\n' +
93
+ ' at: bound (/var/home/aral/Projects/nodekit/node_modules/tape-promise/node_modules/onetime/index.js:30:12)\n' +
94
+ ' stack: |-\n' +
95
+ 'TypeError: Cannot convert undefined or null to object\n' +
96
+ 'at Function.keys (<anonymous>)\n' +
97
+ 'at sortResults (file:///var/home/aral/Projects/nodekit/tests/files.js:17:10)\n' +
98
+ 'at file:///var/home/aral/Projects/nodekit/tests/files.js:101:58\n' +
99
+ 'at processTicksAndRejections (node:internal/process/task_queues:96:5)'
100
+ },
101
+ test: 10
102
+ }
103
+
104
+ tapMonkey.failHandler(mockAssertionWithTypeError)
105
+ tapMonkey.spinner.stop()
106
+ console.log = originalConsoleLog
107
+
108
+ t.true(output.includes('TypeError: Cannot convert undefined or null to object'), 'output includes main error message')
109
+ t.true(output.includes('~/Projects/nodekit/node_modules/tape-promise/node_modules/onetime/index.js:30:12'), 'error location shown')
110
+
111
+ // Test a regular assertion failure.
112
+
113
+ output = ''
114
+ console.log = capturedConsoleLog
115
+
116
+ const regularFailedAssertion = {
117
+ type: 'assert',
118
+ raw: 'not ok 8 quiet mode should not display passed tests',
119
+ ok: false,
120
+ number: 8,
121
+ name: 'quiet mode should not display passed tests',
122
+ error: {
123
+ operator: 'ok',
124
+ expected: 'true',
125
+ actual: 'false',
126
+ at: undefined,
127
+ stack: 'Error: quiet mode should not display passed tests\n' +
128
+ 'at Test.assert [as _assert] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:314:54)\n' +
129
+ 'at Test.bound [as _assert] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:99:32)\n' +
130
+ 'at Test.assert (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:433:10)\n' +
131
+ 'at Test.bound [as true] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:99:32)\n' +
132
+ 'at Test.<anonymous> (file:///var/home/aral/Projects/tap-monkey/tests/index.js:50:9)\n' +
133
+ 'at Test.bound [as _cb] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:99:32)\n' +
134
+ 'at Test.run (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:117:31)\n' +
135
+ 'at Test.bound [as run] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:99:32)\n' +
136
+ 'at Immediate.next [as _onImmediate] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/results.js:88:19)\n' +
137
+ 'at processImmediate (node:internal/timers:466:21)\n',
138
+ raw: ' operator: ok\n' +
139
+ ' expected: true\n' +
140
+ ' actual: false\n' +
141
+ ' stack: |-\n' +
142
+ 'Error: quiet mode should not display passed tests\n' +
143
+ 'at Test.assert [as _assert] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:314:54)\n' +
144
+ 'at Test.bound [as _assert] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:99:32)\n' +
145
+ 'at Test.assert (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:433:10)\n' +
146
+ 'at Test.bound [as true] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:99:32)\n' +
147
+ 'at Test.<anonymous> (file:///var/home/aral/Projects/tap-monkey/tests/index.js:50:9)\n' +
148
+ 'at Test.bound [as _cb] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:99:32)\n' +
149
+ 'at Test.run (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:117:31)\n' +
150
+ 'at Test.bound [as run] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/test.js:99:32)\n' +
151
+ 'at Immediate.next [as _onImmediate] (/var/home/aral/Projects/tap-monkey/node_modules/tape/lib/results.js:88:19)\n' +
152
+ 'at processImmediate (node:internal/timers:466:21)'
153
+ },
154
+ test: 2
155
+ }
156
+
157
+ tapMonkey.failHandler(regularFailedAssertion)
158
+ tapMonkey.spinner.stop()
159
+ console.log = originalConsoleLog
160
+
161
+ t.true(output.includes('operator: ok'), 'output should include operator')
162
+ t.true(output.includes('expected: true'), 'output should include expected field')
163
+ t.true(output.includes('actual : false'), 'output should include actual field')
164
+
165
+ t.end()
166
+ })
167
+
168
+ //
169
+ // Bail out handler tests.
170
+ //
171
+
172
+ test('bailout handler', t => {
173
+ // Setup.
174
+ const _error = console.error
175
+ let output = ''
176
+ console.error = string => output += string
177
+
178
+ const _exit = process.exit
179
+ let exitCalledWithCorrectCode = false
180
+ process.exit = code => exitCalledWithCorrectCode = code === 1
181
+
182
+ // Test.
183
+ tapMonkey.spinner.start()
184
+ const mockRaw = 'mock raw event contents'
185
+ tapMonkey.bailOutHandler({ raw: mockRaw })
186
+
187
+ t.strictEquals(tapMonkey.spinner.isSpinning, false, 'spinner has stopped')
188
+ t.true(output.includes(mockRaw), 'output includes mock event raw string')
189
+ t.true(exitCalledWithCorrectCode, 'exit is called')
190
+
191
+ // Tear down.
192
+ console.error = _error
193
+ process.exit = _exit
194
+
195
+ t.end()
196
+ })
197
+
198
+ //
199
+ // Comment handler tests.
200
+ //
201
+
202
+ test('comment handler', t => {
203
+ // Setup.
204
+ const originalConsoleLog = console.log
205
+ const capturedConsoleLog = (...args) => output += args.join(' ')
206
+
207
+ console.log = capturedConsoleLog
208
+ let output = ''
209
+
210
+ // Test regular comment.
211
+ const regularCommentPrefix = ' 🢂 '
212
+ const regularComment = 'a regular comment'
213
+
214
+ tapMonkey.commentHandler({ raw: regularComment })
215
+ tapMonkey.spinner.stop()
216
+ console.log = originalConsoleLog
217
+
218
+ t.equals(output, `${regularCommentPrefix} ${regularComment}`, 'regular comment is formatted correctly')
219
+
220
+ // Test coverage comments.
221
+ output = ''
222
+ console.log = capturedConsoleLog
223
+
224
+ const coverageTap = [
225
+ '----------|---------|----------|---------|---------|-----------------------',
226
+ ' File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ',
227
+ '----------|---------|----------|---------|---------|-----------------------',
228
+ 'All files | 70.37 | 71.42 | 50 | 70.37 | ',
229
+ ' index.js | 70.37 | 71.42 | 50 | 70.37 | 59-61,105-131,135-152 ',
230
+ '----------|---------|----------|---------|---------|-----------------------'
231
+ ]
232
+
233
+ const expectedCoverageOutput = '╭───────────┬─────────┬──────────┬─────────┬─────────┬────────────────────────╮│ File │ % Stmts │ % Branch │ % Funcs │ % Lines │ Uncovered Line #s │├───────────┼─────────┼──────────┼─────────┼─────────┼────────────────────────┤│ All files │ 70.37 │ 71.42 │ 50 │ 70.37 │ ││ index.js │ 70.37 │ 71.42 │ 50 │ 70.37 │ 59─61,105─131,135─152 │╰───────────┴─────────┴──────────┴─────────┴─────────┴────────────────────────╯'
234
+
235
+ coverageTap.forEach(line => {
236
+ tapMonkey.commentHandler({ raw: line })
237
+ })
238
+ tapMonkey.spinner.stop()
239
+ console.log = originalConsoleLog
240
+
241
+ // Assertions.
242
+ t.equals(output.trim(), expectedCoverageOutput, 'formatted coverage output is correct')
243
+
244
+ // Test too many borders in coverage error.
245
+ t.throws(() => {
246
+ tapMonkey.commentHandler({ raw: coverageTap[0] })
247
+ }, 'too many borders in coverage output error should throw')
248
+
249
+ t.end()
250
+ })
251
+
252
+ //
253
+ // Output handler tests.
254
+ //
255
+
256
+ test ('output handler', t => {
257
+ const vacuumPack = string => string.replace(/\s/g, '')
258
+ const originalConsoleLog = console.log
259
+ const capturedConsoleLog = (...args) => output += args.join(' ')
260
+
261
+ console.log = capturedConsoleLog
262
+ let output = ''
263
+
264
+ // Test: all tests passing.
265
+
266
+ const mockAllTestsPassingResults = {
267
+ asserts: [1, 2, 3],
268
+ pass: [1, 2, 3],
269
+ fail: []
270
+ }
271
+
272
+ tapMonkey.outputHandler(mockAllTestsPassingResults)
273
+ console.log = originalConsoleLog
274
+
275
+ t.strictEquals(tapMonkey.spinner.isSpinning, false, 'spinner has stopped')
276
+ t.true(vacuumPack(output).includes('Alltestspassing!Total3Passing3Failing0'))
277
+
278
+ // Test: failing tests.
279
+
280
+ output = ''
281
+ console.log = capturedConsoleLog
282
+
283
+ const mockFailingTests = {
284
+ asserts: [1, 2, 3],
285
+ pass: [1],
286
+ fail: [2, 3]
287
+ }
288
+
289
+ tapMonkey.outputHandler(mockFailingTests)
290
+ console.log = originalConsoleLog
291
+
292
+ t.true(vacuumPack(output).includes('Therearefailedtests.Total3Passing1Failing2'))
293
+ t.end()
294
+ })
295
+