UNPKG

24.6 kBMarkdownView Raw
1# tape <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
2
3[TAP](https://testanything.org/)-producing test harness for node and browsers
4
5[![github actions][actions-image]][actions-url]
6[![coverage][codecov-image]][codecov-url]
7[![dependency status][deps-svg]][deps-url]
8[![dev dependency status][dev-deps-svg]][dev-deps-url]
9[![License][license-image]][license-url]
10[![Downloads][downloads-image]][downloads-url]
11
12[![npm badge][npm-badge-png]][package-url]
13
14![tape](https://web.archive.org/web/20170612184731if_/http://substack.net/images/tape_drive.png)
15
16# example
17
18``` js
19var test = require('tape');
20
21test('timing test', function (t) {
22 t.plan(2);
23
24 t.equal(typeof Date.now, 'function');
25 var start = Date.now();
26
27 setTimeout(function () {
28 t.equal(Date.now() - start, 100);
29 }, 100);
30});
31
32test('test using promises', async function (t) {
33 const result = await someAsyncThing();
34 t.ok(result);
35});
36```
37
38```
39$ node example/timing.js
40TAP version 13
41# timing test
42ok 1 should be strictly equal
43not ok 2 should be strictly equal
44 ---
45 operator: equal
46 expected: 100
47 actual: 107
48 ...
49
501..2
51# tests 2
52# pass 1
53# fail 1
54```
55
56# usage
57
58You always need to `require('tape')` in test files. You can run the tests by usual node means (`require('test-file.js')` or `node test-file.js`).
59You can also run tests using the `tape` binary to utilize globbing, on Windows for example:
60
61```sh
62$ tape tests/**/*.js
63```
64
65`tape`'s arguments are passed to the [`glob`](https://www.npmjs.com/package/glob) module.
66If you want `glob` to perform the expansion on a system where the shell performs such expansion, quote the arguments as necessary:
67
68```sh
69$ tape 'tests/**/*.js'
70$ tape "tests/**/*.js"
71```
72
73## Preloading modules
74
75Additionally, it is possible to make `tape` load one or more modules before running any tests, by using the `-r` or `--require` flag. Here's an example that loads [babel-register](http://babeljs.io/docs/usage/require/) before running any tests, to allow for JIT compilation:
76
77```sh
78$ tape -r babel-register tests/**/*.js
79```
80
81Depending on the module you're loading, you may be able to parameterize it using environment variables or auxiliary files. Babel, for instance, will load options from [`.babelrc`](http://babeljs.io/docs/usage/babelrc/) at runtime.
82
83The `-r` flag behaves exactly like node's `require`, and uses the same module resolution algorithm. This means that if you need to load local modules, you have to prepend their path with `./` or `../` accordingly.
84
85For example:
86
87```sh
88$ tape -r ./my/local/module tests/**/*.js
89```
90
91Please note that all modules loaded using the `-r` flag will run *before* any tests, regardless of when they are specified. For example, `tape -r a b -r c` will actually load `a` and `c` *before* loading `b`, since they are flagged as required modules.
92
93# things that go well with tape
94
95`tape` maintains a fairly minimal core. Additional features are usually added by using another module alongside `tape`.
96
97## pretty reporters
98
99The default TAP output is good for machines and humans that are robots.
100
101If you want a more colorful / pretty output there are lots of modules on npm that will output something pretty if you pipe TAP into them:
102
103- [tap-spec](https://github.com/scottcorgan/tap-spec)
104- [tap-dot](https://github.com/scottcorgan/tap-dot)
105- [faucet](https://github.com/ljharb/faucet)
106- [tap-bail](https://github.com/juliangruber/tap-bail)
107- [tap-browser-color](https://github.com/kirbysayshi/tap-browser-color)
108- [tap-json](https://github.com/gummesson/tap-json)
109- [tap-min](https://github.com/derhuerst/tap-min)
110- [tap-nyan](https://github.com/calvinmetcalf/tap-nyan)
111- [tap-pessimist](https://www.npmjs.org/package/tap-pessimist)
112- [tap-prettify](https://github.com/toolness/tap-prettify)
113- [colortape](https://github.com/shuhei/colortape)
114- [tap-xunit](https://github.com/aghassemi/tap-xunit)
115- [tap-difflet](https://github.com/namuol/tap-difflet)
116- [tape-dom](https://github.com/gritzko/tape-dom)
117- [tap-diff](https://github.com/axross/tap-diff)
118- [tap-notify](https://github.com/axross/tap-notify)
119- [tap-summary](https://github.com/zoubin/tap-summary)
120- [tap-markdown](https://github.com/Hypercubed/tap-markdown)
121- [tap-html](https://github.com/gabrielcsapo/tap-html)
122- [tap-react-browser](https://github.com/mcnuttandrew/tap-react-browser)
123- [tap-junit](https://github.com/dhershman1/tap-junit)
124- [tap-nyc](https://github.com/MegaArman/tap-nyc)
125- [tap-spec (emoji patch)](https://github.com/Sceat/tap-spec-emoji)
126- [tape-repeater](https://github.com/rgruesbeck/tape-repeater)
127- [tabe](https://github.com/Josenzo/tabe)
128
129To use them, try `node test/index.js | tap-spec` or pipe it into one of the modules of your choice!
130
131## uncaught exceptions
132
133By default, uncaught exceptions in your tests will not be intercepted, and will cause `tape` to crash. If you find this behavior undesirable, use [`tape-catch`](https://github.com/michaelrhodes/tape-catch) to report any exceptions as TAP errors.
134
135## other
136
137- CoffeeScript support with https://www.npmjs.com/package/coffeetape
138- ES6 support with https://www.npmjs.com/package/babel-tape-runner or https://www.npmjs.com/package/buble-tape-runner
139- Different test syntax with https://github.com/pguth/flip-tape (warning: mutates String.prototype)
140- Electron test runner with https://github.com/tundrax/electron-tap
141- Concurrency support with https://github.com/imsnif/mixed-tape
142- In-process reporting with https://github.com/DavidAnson/tape-player
143- Describe blocks with https://github.com/mattriley/tape-describe
144
145# command-line flags
146
147While running tests, top-level configurations can be passed via the command line to specify desired behavior.
148
149Available configurations are listed below:
150
151## --require
152
153**Alias**: `-r`
154
155This is used to load modules before running tests and is explained extensively in the [preloading modules](#preloading-modules) section.
156
157## --ignore
158
159**Alias**: `-i`
160
161This flag is used when tests from certain folders and/or files are not intended to be run.
162The argument is a path to a file that contains the patterns to be ignored.
163It defaults to `.gitignore` when passed with no argument.
164
165```sh
166tape -i .ignore '**/*.js'
167```
168
169An error is thrown if the specified file passed as argument does not exist.
170
171## --ignore-pattern
172
173Same functionality as `--ignore`, but passing the pattern directly instead of an ignore file.
174If both `--ignore` and `--ignore-pattern` are given, the `--ignore-pattern` argument is appended to the content of the ignore file.
175
176```sh
177tape --ignore-pattern 'integration_tests/**/*.js' '**/*.js'
178```
179
180## --no-only
181This is particularly useful in a CI environment where an [only test](#testonlyname-opts-cb) is not supposed to go unnoticed.
182
183By passing the `--no-only` flag, any existing [only test](#testonlyname-opts-cb) causes tests to fail.
184
185```sh
186tape --no-only **/*.js
187```
188
189Alternatively, the environment variable `NODE_TAPE_NO_ONLY_TEST` can be set to `true` to achieve the same behavior; the command-line flag takes precedence.
190
191# methods
192
193The assertion methods in `tape` are heavily influenced or copied from the methods in [node-tap](https://github.com/isaacs/node-tap).
194
195```js
196var test = require('tape')
197```
198
199## test([name], [opts], cb)
200
201Create a new test with an optional `name` string and optional `opts` object.
202`cb(t)` fires with the new test object `t` once all preceding tests have finished.
203Tests execute serially.
204
205Available `opts` options are:
206- opts.skip = true/false. See test.skip.
207- opts.timeout = 500. Set a timeout for the test, after which it will fail. See test.timeoutAfter.
208- opts.objectPrintDepth = 5. Configure max depth of expected / actual object printing. Environmental variable `NODE_TAPE_OBJECT_PRINT_DEPTH` can set the desired default depth for all tests; locally-set values will take precedence.
209- opts.todo = true/false. Test will be allowed to fail.
210
211If you forget to `t.plan()` out how many assertions you are going to run and you don't call `t.end()` explicitly, or return a Promise that eventually settles, your test will hang.
212
213If `cb` returns a Promise, it will be implicitly awaited. If that promise rejects, the test will be failed; if it fulfills, the test will end. Explicitly calling `t.end()` while also returning a Promise that fulfills is an error.
214
215## test.skip([name], [opts], cb)
216
217Generate a new test that will be skipped over.
218
219## test.onFinish(fn)
220
221The onFinish hook will get invoked when ALL `tape` tests have finished right before `tape` is about to print the test summary.
222
223`fn` is called with no arguments, and its return value is ignored.
224
225## test.onFailure(fn)
226
227The onFailure hook will get invoked whenever any `tape` tests has failed.
228
229`fn` is called with no arguments, and its return value is ignored.
230
231## t.plan(n)
232
233Declare that `n` assertions should be run. `t.end()` will be called automatically after the `n`th assertion.
234If there are any more assertions after the `n`th, or after `t.end()` is called, they will generate errors.
235
236## t.end(err)
237
238Declare the end of a test explicitly. If `err` is passed in `t.end` will assert that it is falsy.
239
240Do not call `t.end()` if your test callback returns a Promise.
241
242## t.teardown(cb)
243
244Register a callback to run after the individual test has completed. Multiple registered teardown callbacks will run in order. Useful for undoing side effects, closing network connections, etc.
245
246## t.fail(msg)
247
248Generate a failing assertion with a message `msg`.
249
250## t.pass(msg)
251
252Generate a passing assertion with a message `msg`.
253
254## t.timeoutAfter(ms)
255
256Automatically timeout the test after X ms.
257
258## t.skip(msg)
259
260Generate an assertion that will be skipped over.
261
262## t.ok(value, msg)
263
264Assert that `value` is truthy with an optional description of the assertion `msg`.
265
266Aliases: `t.true()`, `t.assert()`
267
268## t.notOk(value, msg)
269
270Assert that `value` is falsy with an optional description of the assertion `msg`.
271
272Aliases: `t.false()`, `t.notok()`
273
274## t.error(err, msg)
275
276Assert that `err` is falsy. If `err` is non-falsy, use its `err.message` as the description message.
277
278Aliases: `t.ifError()`, `t.ifErr()`, `t.iferror()`
279
280## t.equal(actual, expected, msg)
281
282Assert that `Object.is(actual, expected)` with an optional description of the assertion `msg`.
283
284Aliases: `t.equals()`, `t.isEqual()`, `t.strictEqual()`, `t.strictEquals()`, `t.is()`
285
286## t.notEqual(actual, expected, msg)
287
288Assert that `!Object.is(actual, expected)` with an optional description of the assertion `msg`.
289
290Aliases: `t.notEquals()`, `t.isNotEqual()`, `t.doesNotEqual()`, `t.isInequal()`, `t.notStrictEqual()`, `t.notStrictEquals()`, `t.isNot()`, `t.not()`
291
292## t.looseEqual(actual, expected, msg)
293
294Assert that `actual == expected` with an optional description of the assertion `msg`.
295
296Aliases: `t.looseEquals()`
297
298## t.notLooseEqual(actual, expected, msg)
299
300Assert that `actual != expected` with an optional description of the assertion `msg`.
301
302Aliases: `t.notLooseEquals()`
303
304## t.deepEqual(actual, expected, msg)
305
306Assert that `actual` and `expected` have the same structure and nested values using [node's deepEqual() algorithm](https://github.com/inspect-js/node-deep-equal) with strict comparisons (`===`) on leaf nodes and an optional description of the assertion `msg`.
307
308Aliases: `t.deepEquals()`, `t.isEquivalent()`, `t.same()`
309
310## t.notDeepEqual(actual, expected, msg)
311
312Assert that `actual` and `expected` do not have the same structure and nested values using [node's deepEqual() algorithm](https://github.com/inspect-js/node-deep-equal) with strict comparisons (`===`) on leaf nodes and an optional description of the assertion `msg`.
313
314Aliases: `t.notDeepEquals`, `t.notEquivalent()`, `t.notDeeply()`, `t.notSame()`,
315`t.isNotDeepEqual()`, `t.isNotDeeply()`, `t.isNotEquivalent()`,
316`t.isInequivalent()`
317
318## t.deepLooseEqual(actual, expected, msg)
319
320Assert that `actual` and `expected` have the same structure and nested values using [node's deepEqual() algorithm](https://github.com/inspect-js/node-deep-equal) with loose comparisons (`==`) on leaf nodes and an optional description of the assertion `msg`.
321
322## t.notDeepLooseEqual(actual, expected, msg)
323
324Assert that `actual` and `expected` do not have the same structure and nested values using [node's deepEqual() algorithm](https://github.com/inspect-js/node-deep-equal) with loose comparisons (`==`) on leaf nodes and an optional description of the assertion `msg`.
325
326Aliases: `t.notLooseEqual()`, `t.notLooseEquals()`
327
328## t.throws(fn, expected, msg)
329
330Assert that the function call `fn()` throws an exception. `expected`, if present, must be a `RegExp`, `Function`, or `Object`. The `RegExp` matches the string representation of the exception, as generated by `err.toString()`. For example, if you set `expected` to `/user/`, the test will pass only if the string representation of the exception contains the word `user`. Any other exception will result in a failed test. The `Function` could be the constructor for the Error type thrown, or a predicate function to be called with that exception. `Object` in this case corresponds to a so-called validation object, in which each property is tested for strict deep equality. As an example, see the following two tests--each passes a validation object to `t.throws()` as the second parameter. The first test will pass, because all property values in the actual error object are deeply strictly equal to the property values in the validation object.
331```
332 const err = new TypeError("Wrong value");
333 err.code = 404;
334 err.check = true;
335
336 // Passing test.
337 t.throws(
338 () => {
339 throw err;
340 },
341 {
342 code: 404,
343 check: true
344 },
345 "Test message."
346 );
347```
348This next test will fail, because all property values in the actual error object are _not_ deeply strictly equal to the property values in the validation object.
349```
350 const err = new TypeError("Wrong value");
351 err.code = 404;
352 err.check = "true";
353
354 // Failing test.
355 t.throws(
356 () => {
357 throw err;
358 },
359 {
360 code: 404,
361 check: true // This is not deeply strictly equal to err.check.
362 },
363 "Test message."
364 );
365```
366
367This is very similar to how Node's `assert.throws()` method tests validation objects (please see the [Node _assert.throws()_ documentation](https://nodejs.org/api/assert.html#assert_assert_throws_fn_error_message) for more information).
368
369If `expected` is not of type `RegExp`, `Function`, or `Object`, or omitted entirely, any exception will result in a passed test. `msg` is an optional description of the assertion.
370
371Please note that the second parameter, `expected`, cannot be of type `string`. If a value of type `string` is provided for `expected`, then `t.throws(fn, expected, msg)` will execute, but the value of `expected` will be set to `undefined`, and the specified string will be set as the value for the `msg` parameter (regardless of what _actually_ passed as the third parameter). This can cause unexpected results, so please be mindful.
372
373## t.doesNotThrow(fn, expected, msg)
374
375Assert that the function call `fn()` does not throw an exception. `expected`, if present, limits what should not be thrown, and must be a `RegExp` or `Function`. The `RegExp` matches the string representation of the exception, as generated by `err.toString()`. For example, if you set `expected` to `/user/`, the test will fail only if the string representation of the exception contains the word `user`. Any other exception will result in a passed test. The `Function` is the exception thrown (e.g. `Error`). If `expected` is not of type `RegExp` or `Function`, or omitted entirely, any exception will result in a failed test. `msg` is an optional description of the assertion.
376
377Please note that the second parameter, `expected`, cannot be of type `string`. If a value of type `string` is provided for `expected`, then `t.doesNotThrows(fn, expected, msg)` will execute, but the value of `expected` will be set to `undefined`, and the specified string will be set as the value for the `msg` parameter (regardless of what _actually_ passed as the third parameter). This can cause unexpected results, so please be mindful.
378
379## t.test(name, [opts], cb)
380
381Create a subtest with a new test handle `st` from `cb(st)` inside the current test `t`. `cb(st)` will only fire when `t` finishes. Additional tests queued up after `t` will not be run until all subtests finish.
382
383You may pass the same options that [`test()`](#testname-opts-cb) accepts.
384
385## t.comment(message)
386
387Print a message without breaking the tap output.
388(Useful when using e.g. `tap-colorize` where output is buffered & `console.log` will print in incorrect order vis-a-vis tap output.)
389
390Multiline output will be split by `\n` characters, and each one printed as a comment.
391
392## t.match(string, regexp, message)
393
394Assert that `string` matches the RegExp `regexp`. Will fail when the first two arguments are the wrong type.
395
396## t.doesNotMatch(string, regexp, message)
397
398Assert that `string` does not match the RegExp `regexp`. Will fail when the first two arguments are the wrong type.
399
400## t.capture(obj, method, implementation = () => {})
401
402Replaces `obj[method]` with the supplied implementation.
403`obj` must be a non-primitive, `method` must be a valid property key (string or symbol), and `implementation`, if provided, must be a function.
404
405Calling the returned `results()` function will return an array of call result objects.
406The array of calls will be reset whenever the function is called.
407Call result objects will match one of these forms:
408 - `{ args: [x, y, z], receiver: o, returned: a }`
409 - `{ args: [x, y, z], receiver: o, threw: true }`
410
411The replacement will automatically be restored on test teardown.
412You can restore it manually, if desired, by calling `.restore()` on the returned results function.
413
414Modeled after [tap](https://tapjs.github.io/tapjs/modules/_tapjs_intercept.html).
415
416## t.captureFn(original)
417
418Wraps the supplied function.
419The returned wrapper has a `.calls` property, which is an array that will be populated with call result objects, described under `t.capture()`.
420
421Modeled after [tap](https://tapjs.github.io/tapjs/modules/_tapjs_intercept.html).
422
423## t.intercept(obj, property, desc = {}, strictMode = true)
424
425Similar to `t.capture()``, but can be used to track get/set operations for any arbitrary property.
426Calling the returned `results()` function will return an array of call result objects.
427The array of calls will be reset whenever the function is called.
428Call result objects will match one of these forms:
429 - `{ type: 'get', value: '1.2.3', success: true, args: [x, y, z], receiver: o }`
430 - `{ type: 'set', value: '2.4.6', success: false, args: [x, y, z], receiver: o }`
431
432If `strictMode` is `true`, and `writable` is `false`, and no `get` or `set` is provided, an exception will be thrown when `obj[property]` is assigned to.
433If `strictMode` is `false` in this scenario, nothing will be set, but the attempt will still be logged.
434
435Providing both `desc.get` and `desc.set` are optional and can still be useful for logging get/set attempts.
436
437`desc` must be a valid property descriptor, meaning that `get`/`set` are mutually exclusive with `writable`/`value`.
438Additionally, explicitly setting `configurable` to `false` is not permitted, so that the property can be restored.
439
440## var htest = test.createHarness()
441
442Create a new test harness instance, which is a function like `test()`, but with a new pending stack and test state.
443
444By default the TAP output goes to `console.log()`. You can pipe the output to someplace else if you `htest.createStream().pipe()` to a destination stream on the first tick.
445
446## test.only([name], [opts], cb)
447
448Like `test([name], [opts], cb)` except if you use `.only` this is the only test case that will run for the entire process, all other test cases using `tape` will be ignored.
449
450Check out how the usage of [the --no-only flag](#--no-only) could help ensure there is no `.only` test running in a specified environment.
451
452## var stream = test.createStream(opts)
453
454Create a stream of output, bypassing the default output stream that writes messages to `console.log()`. By default `stream` will be a text stream of TAP output, but you can get an object stream instead by setting `opts.objectMode` to `true`.
455
456### tap stream reporter
457
458You can create your own custom test reporter using this `createStream()` api:
459
460``` js
461var test = require('tape');
462var path = require('path');
463
464test.createStream().pipe(process.stdout);
465
466process.argv.slice(2).forEach(function (file) {
467 require(path.resolve(file));
468});
469```
470
471You could substitute `process.stdout` for whatever other output stream you want, like a network connection or a file.
472
473Pass in test files to run as arguments:
474
475```sh
476$ node tap.js test/x.js test/y.js
477TAP version 13
478# (anonymous)
479not ok 1 should be strictly equal
480 ---
481 operator: equal
482 expected: "boop"
483 actual: "beep"
484 ...
485# (anonymous)
486ok 2 should be strictly equal
487ok 3 (unnamed assert)
488# wheee
489ok 4 (unnamed assert)
490
4911..4
492# tests 4
493# pass 3
494# fail 1
495```
496
497### object stream reporter
498
499Here's how you can render an object stream instead of TAP:
500
501``` js
502var test = require('tape');
503var path = require('path');
504
505test.createStream({ objectMode: true }).on('data', function (row) {
506 console.log(JSON.stringify(row))
507});
508
509process.argv.slice(2).forEach(function (file) {
510 require(path.resolve(file));
511});
512```
513
514The output for this runner is:
515
516```sh
517$ node object.js test/x.js test/y.js
518{"type":"test","name":"(anonymous)","id":0}
519{"id":0,"ok":false,"name":"should be strictly equal","operator":"equal","actual":"beep","expected":"boop","error":{},"test":0,"type":"assert"}
520{"type":"end","test":0}
521{"type":"test","name":"(anonymous)","id":1}
522{"id":0,"ok":true,"name":"should be strictly equal","operator":"equal","actual":2,"expected":2,"test":1,"type":"assert"}
523{"id":1,"ok":true,"name":"(unnamed assert)","operator":"ok","actual":true,"expected":true,"test":1,"type":"assert"}
524{"type":"end","test":1}
525{"type":"test","name":"wheee","id":2}
526{"id":0,"ok":true,"name":"(unnamed assert)","operator":"ok","actual":true,"expected":true,"test":2,"type":"assert"}
527{"type":"end","test":2}
528```
529
530A convenient alternative to achieve the same:
531```js
532// report.js
533var test = require('tape');
534
535test.createStream({ objectMode: true }).on('data', function (row) {
536 console.log(JSON.stringify(row)) // for example
537});
538```
539and then:
540```sh
541$ tape -r ./report.js **/*.test.js
542```
543
544# install
545
546With [npm](https://npmjs.org) do:
547
548```sh
549npm install tape --save-dev
550```
551
552# troubleshooting
553
554Sometimes `t.end()` doesn’t preserve the expected output ordering.
555
556For instance the following:
557
558```js
559var test = require('tape');
560
561test('first', function (t) {
562
563 setTimeout(function () {
564 t.ok(1, 'first test');
565 t.end();
566 }, 200);
567
568 t.test('second', function (t) {
569 t.ok(1, 'second test');
570 t.end();
571 });
572});
573
574test('third', function (t) {
575 setTimeout(function () {
576 t.ok(1, 'third test');
577 t.end();
578 }, 100);
579});
580```
581
582will output:
583
584```
585ok 1 second test
586ok 2 third test
587ok 3 first test
588```
589
590because `second` and `third` assume `first` has ended before it actually does.
591
592Use `t.plan()` instead to let other tests know they should wait:
593
594```diff
595var test = require('tape');
596
597test('first', function (t) {
598
599+ t.plan(2);
600
601 setTimeout(function () {
602 t.ok(1, 'first test');
603- t.end();
604 }, 200);
605
606 t.test('second', function (t) {
607 t.ok(1, 'second test');
608 t.end();
609 });
610});
611
612test('third', function (t) {
613 setTimeout(function () {
614 t.ok(1, 'third test');
615 t.end();
616 }, 100);
617});
618```
619
620# license
621
622MIT
623
624[package-url]: https://npmjs.org/package/tape
625[npm-version-svg]: https://versionbadg.es/ljharb/tape.svg
626[deps-svg]: https://david-dm.org/ljharb/tape.svg
627[deps-url]: https://david-dm.org/ljharb/tape
628[dev-deps-svg]: https://david-dm.org/ljharb/tape/dev-status.svg
629[dev-deps-url]: https://david-dm.org/ljharb/tape#info=devDependencies
630[npm-badge-png]: https://nodei.co/npm/tape.png?downloads=true&stars=true
631[license-image]: https://img.shields.io/npm/l/tape.svg
632[license-url]: LICENSE
633[downloads-image]: https://img.shields.io/npm/dm/tape.svg
634[downloads-url]: https://npm-stat.com/charts.html?package=tape
635[codecov-image]: https://codecov.io/gh/ljharb/tape/branch/master/graphs/badge.svg
636[codecov-url]: https://app.codecov.io/gh/ljharb/tape/
637[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/tape
638[actions-url]: https://github.com/ljharb/tape/actions