Skip to content

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
voxpelli committed Jul 24, 2023
1 parent 3801b7c commit 3854c65
Show file tree
Hide file tree
Showing 14 changed files with 479 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ jobs:
test:
uses: voxpelli/ghatemplates/.github/workflows/test.yml@main
with:
node-versions: '16,18,20'
node-versions: '18,20'
os: 'ubuntu-latest'
2 changes: 1 addition & 1 deletion .github/workflows/ts-internal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ jobs:
uses: voxpelli/ghatemplates/.github/workflows/type-check.yml@main
with:
ts-versions: ${{ github.event.schedule && 'next' || '5.1,next' }}
ts-libs: 'es2020;esnext'
ts-libs: 'es2021;esnext'
5 changes: 4 additions & 1 deletion .knip.jsonc
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"$schema": "https://unpkg.com/knip@2/schema.json",
"ignoreDependencies": ["@types/mocha", "mocha"]
"entry": [
"index.js",
"index.d.ts"
]
}
60 changes: 43 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,63 @@
# Node Module Template
# @voxpelli/node-test-pretty-reporter

A GitHub template repo for node modules
Reporter for [node:test](https://nodejs.org/api/test.html#custom-reporters) that supports colorful diffs etc

<!--
[![npm version](https://img.shields.io/npm/v/buffered-async-iterable.svg?style=flat)](https://www.npmjs.com/package/buffered-async-iterable)
[![npm downloads](https://img.shields.io/npm/dm/buffered-async-iterable.svg?style=flat)](https://www.npmjs.com/package/buffered-async-iterable)
-->
[![npm version](https://img.shields.io/npm/v/@voxpelli/node-test-pretty-reporter.svg?style=flat)](https://www.npmjs.com/package/@voxpelli/node-test-pretty-reporter)
[![npm downloads](https://img.shields.io/npm/dm/@voxpelli/node-test-pretty-reporter.svg?style=flat)](https://www.npmjs.com/package/@voxpelli/node-test-pretty-reporter)
[![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg)](https://github.com/voxpelli/eslint-config)
[![Module type: ESM](https://img.shields.io/badge/module%20type-esm-brightgreen)](https://github.com/voxpelli/badges-cjs-esm)
[![Types in JS](https://img.shields.io/badge/types_in_js-yes-brightgreen)](https://github.com/voxpelli/types-in-js)
[![Follow @voxpelli@mastodon.social](https://img.shields.io/mastodon/follow/109247025527949675?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@voxpelli)

## Usage

### Simple

```javascript
import { something } from '@voxpelli/node-module-template';
```sh
npm install -D @voxpelli/node-test-pretty-reporter
```

// Use that something
```sh
node --test --test-reporter=@voxpelli/node-test-pretty-reporter
```

## API
## Why another test reporter?

This one is similar to the built-in `spec` reporter but differs in some ways which I personally prefer.

### Rendering diffs from assertions

Outputs colored diffs when a test is failed with an `Error` that has `expected` and `actual` properties (respecting a `showDiff` property set to `false`).

Diff is generated by [`jest-diff`](https://www.npmjs.com/package/jest-diff) (no other part of `jest` is used in this reporter).

Assertion libraries that outputs compatible errors:
* [`node:assert`](https://nodejs.org/api/assert.html#assert)
* [`chai`](https://www.chaijs.com/)

### `something(input, { configParam }) => Promise<output>`
As with other changes, this makes the reporter on par [with Mocha](https://mochajs.org/#diffs).

### Output styling

The output styling aligns more with Mocha's [`spec` reporter](https://mochajs.org/#spec):

* Errors are presented at the end instead of in the list of tests
* Less visually intense, eg. no `` in front of names and only failed tests gets colored
* Durations are only reported if considered slow (using same default [as Mocha](https://mochajs.org/#test-duration): 75ms)
* No redundant mentioning of a test suite after the suite has completed – opts for a clean tree from top to bottom instead

### _Planned:_ Outputting the full `cause` chain of an `Error`

Originally [a PR of mine to Mocha](https://github.com/mochajs/mocha/pull/4829), my plan was to output the full `cause` chain in this reporter instead, but seems like the full `cause` chain is not available to the reporters, probably due to the reporter and the tests living in different processes. Will investigate further, follow in [issue #2](https://github.com/voxpelli/node-test-pretty-reporter/issues/2).

Takes a value (`input`), does something configured by the config (`configParam`) and returns the processed value asyncly(`output`)

## Similar modules

* [`example`](https://example.com/) – is similar in this way
I have not tested any of these myself yet so can't say if they work well or not, but adding here for reference.

* [`MoLow/reporters`](https://github.com/MoLow/reporters) – many custom reporters for `node:test`
* [`nearform/node-test-github-reporter`](https://github.com/nearform/node-test-github-reporter) – another custom report for `node:test`, this one from [@nearform](https://github.com/nearform) and geared towards GitHub Actions

## See also

* [Announcement blog post](#)
* [Announcement tweet](#)
* [`node:test`](https://nodejs.org/api/test.html) – the full documentation for the `node:test` module that shipped in Node.js 18
* [`nodejs/node-core-test`](https://github.com/nodejs/node-core-test) – a userland port of `node:test` making it available in Node.js 14 and later (this reporter has not been tested with this userland port)
* [`@matteo.collina/tspl`](https://github.com/mcollina/tspl) – test planner for `node:test` and `node:assert`
145 changes: 143 additions & 2 deletions lib/advanced-types.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,143 @@
// TODO: Replace with proper types or remove file
import {} from 'node:path';
export interface TestsStreamEvents {
'test:diagnostic': TestDiagnosticData
'test:fail': TestFail
'test:pass': TestPass
'test:plan': TestPlan
'test:start': TestFileEvent
'test:coverage': TestCoverage
// Not exposed to custom reporter?
'test:dequeue': TestFileEvent
'test:enqueue': TestFileEvent
'test:stderr': TestStdout
'test:stdout': TestStdout
'test:watch:drained': TestEmptyEvent

}

export type TestsStreamEventPayloads = {
[type in keyof TestsStreamEvents]: {
type: type,
data: TestsStreamEvents[type],
}
}[keyof TestsStreamEvents];


// *** Event payloads not yet in @types/node ***

export interface TestEventBasic {
nesting: number;
}

export interface TestFileEventBasic extends TestEventBasic {
file: string|undefined,
}

export interface TestFileEvent extends TestFileEventBasic {
name: string,
}

export interface TestPlan extends TestFileEventBasic {
count: number;
}

export interface TestDiagnosticData extends TestFileEventBasic {
message: string;
}


export interface TestStdout {
file: string,
message: string,
}

export interface TestEmptyEvent {}

// *** test:coverage event ***

export interface TestCoverageSummary {
totalLineCount: number
totalBranchCount: number
totalFunctionCount: number
coveredLineCount: number
coveredBranchCount: number
coveredFunctionCount: number
coveredLinePercent: number
coveredBranchPercent: number
coveredFunctionPercent: number
}

export interface TestCoverageSummaryFile extends TestCoverageSummary {
path: string
uncoveredLineNumbers: number[]
}

export interface TestCoverage extends TestEventBasic {
summary: {
files: TestCoverageSummaryFile[]
totals: TestCoverageSummary
workingDirectory: string
}
}

// *** Somewhat of a copy and paste from @types/node ***

// TODO: Have @types/node export these instead of copy and pasting them here

export interface TestFail extends TestFileEvent {
/**
* Additional execution metadata.
*/
details: {
/**
* The duration of the test in milliseconds.
*/
duration_ms: number;

/**
* The error thrown by the test.
*/
error: Error;
};

/**
* The ordinal number of the test.
*/
testNumber: number;

/**
* Present if `context.todo` is called.
*/
todo?: string | boolean;

/**
* Present if `context.skip` is called.
*/
skip?: string | boolean;
}

export interface TestPass extends TestFileEvent {
/**
* Additional execution metadata.
*/
details: {
/**
* The duration of the test in milliseconds.
*/
duration_ms: number;
};

/**
* The ordinal number of the test.
*/
testNumber: number;

/**
* Present if `context.todo` is called.
*/
todo?: string | boolean;

/**
* Present if `context.skip` is called.
*/
skip?: string | boolean;
}
29 changes: 29 additions & 0 deletions lib/diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { diff } from 'jest-diff';

/**
* @typedef DiffableError
* @property {unknown} expected
* @property {unknown} actual
* @property {boolean|undefined} [showDiff]
*/

/**
* @template {Error} T
* @param {T} input
* @returns {input is (DiffableError & T)}
*/
export function errIsDiffable (input) {
if (!('expected' in input)) return false;
if (!('actual' in input)) return false;
if (input.expected === undefined) return false;

return true;
}

/**
* @param {DiffableError} input
* @returns {string|undefined}
*/
export function generateErrDiff ({ actual, expected }) {
return diff(actual, expected) || undefined;
}
51 changes: 51 additions & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { errIsDiffable, generateErrDiff } from './diff.js';
import { getErrorAndCauses } from './utils.js';

/**
* @param {import('markdown-or-chalk').MarkdownOrChalk} format
* @param {Error} err
* @returns {string}
*/
export function formatErrorAndCauses (format, err) {
if ('code' in err && err.code === 'ERR_TEST_FAILURE' && err.cause instanceof Error) {
err = err.cause;
}

return getErrorAndCauses(err)
.map(value => formatError(format, value))
.join('\n\ncaused by:\n\n');
}

/**
* @param {import('markdown-or-chalk').MarkdownOrChalk} format
* @param {Error} err
* @returns {string}
*/
function formatError (format, err) {
let message = err.message;
let stack = (err.stack || '').replaceAll(/^\s+/gm, '');

const index = stack.indexOf(message);

if (index > -1) {
const splitIndex = index + message.length;

message = stack.slice(0, splitIndex);
stack = stack.slice(splitIndex + 1);
}

if (format.chalk) {
message = format.chalk.red(message);
stack = format.chalk.gray(stack);
}

const diff = errIsDiffable(err) && err.showDiff !== false
? generateErrDiff(err)
: undefined;

return (
diff
? [message, diff, stack]
: [message, stack]
).join('\n\n');
}
Loading

0 comments on commit 3854c65

Please sign in to comment.