-
Notifications
You must be signed in to change notification settings - Fork 783
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Core: Make
tap
and console
reporters generally available
This change makes the reporters, from the js-reporters package, generally available through the primary distribution, instead of being limited to the QUnit CLI. The built-in reporters are exposed via `QUnit.reporters`. This is a major step toward truly supporting the TAP ecosystem, and popular runners such as Airtap for browser testing, and the node-tap CLI (why not). There is still more work to be done, though, such as making the library more friendly to require'ing, and further decoupling of our HTML Runner/Reporter. As such, this remains experimental as of yet. It can be tried out by calling `QUnit.reporters.tap.init(QUnit)` between importing QUnit and importing your tests, e.g. from an inline script or setup script. This overall direction is outlined in qunitjs/js-reporters#133, and as such apart from bridging older versions, the js-reporters package will not be as actively developed going forward. I'm copying the reporters we developed there into QUnit repository for further developement here, without the indirection of that package. Co-authored-by: Florentin Simion <florentins@gmail.com> Co-authored-by: Franziska Carstens <franziska.carstens@wwu.de> Co-authored-by: Martin Olsson <martin@minimum.se> Co-authored-by: Robert Jackson <me@rwjblue.com> Co-authored-by: Timo Tijhof <krinklemail@gmail.com> Co-authored-by: Trent Willis <trentmwillis@gmail.com> Co-authored-by: Zachary Mulgrew <zackthehuman@users.noreply.github.com> Co-authored-by: jeberger <jeberger@free.fr>
- Loading branch information
1 parent
134c59b
commit f8948c9
Showing
9 changed files
with
725 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import ConsoleReporter from "./reporters/ConsoleReporter.js"; | ||
import TapReporter from "./reporters/TapReporter.js"; | ||
|
||
export default { | ||
console: ConsoleReporter, | ||
tap: TapReporter | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { console } from "../globals"; | ||
|
||
export default class ConsoleReporter { | ||
constructor( runner, options = {} ) { | ||
|
||
// Cache references to console methods to ensure we can report failures | ||
// from tests tests that mock the console object itself. | ||
// https://github.com/qunitjs/qunit/issues/1340 | ||
this.log = options.log || console.log.bind( console ); | ||
|
||
runner.on( "runStart", this.onRunStart.bind( this ) ); | ||
runner.on( "testStart", this.onTestStart.bind( this ) ); | ||
runner.on( "testEnd", this.onTestEnd.bind( this ) ); | ||
runner.on( "runEnd", this.onRunEnd.bind( this ) ); | ||
} | ||
|
||
static init( runner, options ) { | ||
return new ConsoleReporter( runner, options ); | ||
} | ||
|
||
onRunStart( runStart ) { | ||
this.log( "runStart", runStart ); | ||
} | ||
|
||
onTestStart( test ) { | ||
this.log( "testStart", test ); | ||
} | ||
|
||
onTestEnd( test ) { | ||
this.log( "testEnd", test ); | ||
} | ||
|
||
onRunEnd( runEnd ) { | ||
this.log( "runEnd", runEnd ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
import kleur from "kleur"; | ||
import { console } from "../globals"; | ||
const hasOwn = Object.hasOwnProperty; | ||
|
||
/** | ||
* Format a given value into YAML. | ||
* | ||
* YAML is a superset of JSON that supports all the same data | ||
* types and syntax, and more. As such, it is always possible | ||
* to fallback to JSON.stringfify, but we generally avoid | ||
* that to make output easier to read for humans. | ||
* | ||
* Supported data types: | ||
* | ||
* - null | ||
* - boolean | ||
* - number | ||
* - string | ||
* - array | ||
* - object | ||
* | ||
* Anything else (including NaN, Infinity, and undefined) | ||
* must be described in strings, for display purposes. | ||
* | ||
* Note that quotes are optional in YAML strings if the | ||
* strings are "simple", and as such we generally prefer | ||
* that for improved readability. We output strings in | ||
* one of three ways: | ||
* | ||
* - bare unquoted text, for simple one-line strings. | ||
* - JSON (quoted text), for complex one-line strings. | ||
* - YAML Block, for complex multi-line strings. | ||
* | ||
* Objects with cyclical references will be stringifed as | ||
* "[Circular]" as they cannot otherwise be represented. | ||
*/ | ||
function prettyYamlValue( value, indent = 4 ) { | ||
if ( value === undefined ) { | ||
|
||
// Not supported in JSON/YAML, turn into string | ||
// and let the below output it as bare string. | ||
value = String( value ); | ||
} | ||
|
||
// Support IE 9-11: Use isFinite instead of ES6 Number.isFinite | ||
if ( typeof value === "number" && !isFinite( value ) ) { | ||
|
||
// Turn NaN and Infinity into simple strings. | ||
// Paranoia: Don't return directly just in case there's | ||
// a way to add special characters here. | ||
value = String( value ); | ||
} | ||
|
||
if ( typeof value === "number" ) { | ||
|
||
// Simple numbers | ||
return JSON.stringify( value ); | ||
} | ||
|
||
if ( typeof value === "string" ) { | ||
|
||
// If any of these match, then we can't output it | ||
// as bare unquoted text, because that would either | ||
// cause data loss or invalid YAML syntax. | ||
// | ||
// - Quotes, escapes, line breaks, or JSON-like stuff. | ||
const rSpecialJson = /['"\\/[{}\]\r\n]/; | ||
|
||
// - Characters that are special at the start of a YAML value | ||
const rSpecialYaml = /[-?:,[\]{}#&*!|=>'"%@`]/; | ||
|
||
// - Leading or trailing whitespace. | ||
const rUntrimmed = /(^\s|\s$)/; | ||
|
||
// - Ambiguous as YAML number, e.g. '2', '-1.2', '.2', or '2_000' | ||
const rNumerical = /^[\d._-]+$/; | ||
|
||
// - Ambiguous as YAML bool. | ||
// Use case-insensitive match, although technically only | ||
// fully-lower, fully-upper, or uppercase-first would be ambiguous. | ||
// e.g. true/True/TRUE, but not tRUe. | ||
const rBool = /^(true|false|y|n|yes|no|on|off)$/i; | ||
|
||
// Is this a complex string? | ||
if ( | ||
value === "" || | ||
rSpecialJson.test( value ) || | ||
rSpecialYaml.test( value[ 0 ] ) || | ||
rUntrimmed.test( value ) || | ||
rNumerical.test( value ) || | ||
rBool.test( value ) | ||
) { | ||
if ( !/\n/.test( value ) ) { | ||
|
||
// Complex one-line string, use JSON (quoted string) | ||
return JSON.stringify( value ); | ||
} | ||
|
||
// See also <https://yaml-multiline.info/> | ||
// Support IE 9-11: Avoid ES6 String#repeat | ||
const prefix = ( new Array( indent + 1 ) ).join( " " ); | ||
|
||
const trailingLinebreakMatch = value.match( /\n+$/ ); | ||
const trailingLinebreaks = trailingLinebreakMatch ? | ||
trailingLinebreakMatch[ 0 ].length : 0; | ||
|
||
if ( trailingLinebreaks === 1 ) { | ||
|
||
// Use the most straight-forward "Block" string in YAML | ||
// without any "Chomping" indicators. | ||
const lines = value | ||
|
||
// Ignore the last new line, since we'll get that one for free | ||
// with the straight-forward Block syntax. | ||
.replace( /\n$/, "" ) | ||
.split( "\n" ) | ||
.map( line => prefix + line ); | ||
return "|\n" + lines.join( "\n" ); | ||
} else { | ||
|
||
// This has either no trailing new lines, or more than 1. | ||
// Use |+ so that YAML parsers will preserve it exactly. | ||
const lines = value | ||
.split( "\n" ) | ||
.map( line => prefix + line ); | ||
return "|+\n" + lines.join( "\n" ); | ||
} | ||
} else { | ||
|
||
// Simple string, use bare unquoted text | ||
return value; | ||
} | ||
} | ||
|
||
// Handle null, boolean, array, and object | ||
return JSON.stringify( decycledShallowClone( value ), null, 2 ); | ||
} | ||
|
||
/** | ||
* Creates a shallow clone of an object where cycles have | ||
* been replaced with "[Circular]". | ||
*/ | ||
function decycledShallowClone( object, ancestors = [] ) { | ||
if ( ancestors.indexOf( object ) !== -1 ) { | ||
return "[Circular]"; | ||
} | ||
|
||
let clone; | ||
|
||
const type = Object.prototype.toString | ||
.call( object ) | ||
.replace( /^\[.+\s(.+?)]$/, "$1" ) | ||
.toLowerCase(); | ||
|
||
switch ( type ) { | ||
case "array": | ||
ancestors.push( object ); | ||
clone = object.map( function( element ) { | ||
return decycledShallowClone( element, ancestors ); | ||
} ); | ||
ancestors.pop(); | ||
break; | ||
case "object": | ||
ancestors.push( object ); | ||
clone = {}; | ||
Object.keys( object ).forEach( function( key ) { | ||
clone[ key ] = decycledShallowClone( object[ key ], ancestors ); | ||
} ); | ||
ancestors.pop(); | ||
break; | ||
default: | ||
clone = object; | ||
} | ||
|
||
return clone; | ||
} | ||
|
||
export default class TapReporter { | ||
constructor( runner, options = {} ) { | ||
|
||
// Cache references to console methods to ensure we can report failures | ||
// from tests tests that mock the console object itself. | ||
// https://github.com/qunitjs/qunit/issues/1340 | ||
this.log = options.log || console.log.bind( console ); | ||
|
||
this.testCount = 0; | ||
|
||
runner.on( "runStart", this.onRunStart.bind( this ) ); | ||
runner.on( "testEnd", this.onTestEnd.bind( this ) ); | ||
runner.on( "runEnd", this.onRunEnd.bind( this ) ); | ||
} | ||
|
||
static init( runner, options ) { | ||
return new TapReporter( runner, options ); | ||
} | ||
|
||
onRunStart( _globalSuite ) { | ||
this.log( "TAP version 13" ); | ||
} | ||
|
||
onTestEnd( test ) { | ||
this.testCount = this.testCount + 1; | ||
|
||
if ( test.status === "passed" ) { | ||
this.log( `ok ${this.testCount} ${test.fullName.join( " > " )}` ); | ||
} else if ( test.status === "skipped" ) { | ||
this.log( | ||
kleur.yellow( `ok ${this.testCount} # SKIP ${test.fullName.join( " > " )}` ) | ||
); | ||
} else if ( test.status === "todo" ) { | ||
this.log( | ||
kleur.cyan( `not ok ${this.testCount} # TODO ${test.fullName.join( " > " )}` ) | ||
); | ||
test.errors.forEach( ( error ) => this.logError( error, "todo" ) ); | ||
} else { | ||
this.log( | ||
kleur.red( `not ok ${this.testCount} ${test.fullName.join( " > " )}` ) | ||
); | ||
test.errors.forEach( ( error ) => this.logError( error ) ); | ||
} | ||
} | ||
|
||
onRunEnd( globalSuite ) { | ||
this.log( `1..${globalSuite.testCounts.total}` ); | ||
this.log( `# pass ${globalSuite.testCounts.passed}` ); | ||
this.log( kleur.yellow( `# skip ${globalSuite.testCounts.skipped}` ) ); | ||
this.log( kleur.cyan( `# todo ${globalSuite.testCounts.todo}` ) ); | ||
this.log( kleur.red( `# fail ${globalSuite.testCounts.failed}` ) ); | ||
} | ||
|
||
logError( error, severity ) { | ||
let out = " ---"; | ||
out += `\n message: ${prettyYamlValue( error.message || "failed" )}`; | ||
out += `\n severity: ${prettyYamlValue( severity || "failed" )}`; | ||
|
||
if ( hasOwn.call( error, "actual" ) ) { | ||
out += `\n actual : ${prettyYamlValue( error.actual )}`; | ||
} | ||
|
||
if ( hasOwn.call( error, "expected" ) ) { | ||
out += `\n expected: ${prettyYamlValue( error.expected )}`; | ||
} | ||
|
||
if ( error.stack ) { | ||
|
||
// Since stacks aren't user generated, take a bit of liberty by | ||
// adding a trailing new line to allow a straight-forward YAML Blocks. | ||
out += `\n stack: ${prettyYamlValue( error.stack + "\n" )}`; | ||
} | ||
|
||
out += "\n ..."; | ||
this.log( out ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
const { EventEmitter } = require( "events" ); | ||
|
||
QUnit.module( "ConsoleReporter", hooks => { | ||
let emitter; | ||
let callCount; | ||
|
||
hooks.beforeEach( function() { | ||
emitter = new EventEmitter(); | ||
callCount = 0; | ||
const con = { | ||
log: () => { | ||
callCount++; | ||
} | ||
}; | ||
QUnit.reporters.console.init( emitter, con ); | ||
} ); | ||
|
||
QUnit.test( "Event \"runStart\"", assert => { | ||
emitter.emit( "runStart", {} ); | ||
assert.equal( callCount, 1 ); | ||
} ); | ||
|
||
QUnit.test( "Event \"runEnd\"", assert => { | ||
emitter.emit( "runEnd", {} ); | ||
assert.equal( callCount, 1 ); | ||
} ); | ||
|
||
QUnit.test( "Event \"testStart\"", assert => { | ||
emitter.emit( "testStart", {} ); | ||
assert.equal( callCount, 1 ); | ||
} ); | ||
|
||
QUnit.test( "Event \"testEnd\"", assert => { | ||
emitter.emit( "testEnd", {} ); | ||
assert.equal( callCount, 1 ); | ||
} ); | ||
} ); |
Oops, something went wrong.