diff --git a/.labrc.js b/.labrc.js new file mode 100644 index 00000000..26359752 --- /dev/null +++ b/.labrc.js @@ -0,0 +1,3 @@ +module.exports = { + enableTemplateStringHTMLReporter: 'true' +}; \ No newline at end of file diff --git a/README.md b/README.md index 48fa3277..1596a5dc 100755 --- a/README.md +++ b/README.md @@ -15,3 +15,7 @@ - [Changelog](https://hapi.dev/family/lab/changelog/) - [Project policies](https://hapi.dev/policies/) - [Free and commercial support options](https://hapi.dev/support/) + + +# Notes +- Replacing Handlebars with template strings. Implement similar to https://medium.com/your-majesty-co/frameworkless-javascript-template-literals-the-best-thing-since-sliced-bread-d97f000ce955 diff --git a/lib/reporters/html.js b/lib/reporters/html.js index e0a1295f..132a5d1e 100755 --- a/lib/reporters/html.js +++ b/lib/reporters/html.js @@ -7,10 +7,19 @@ const Handlebars = require('handlebars'); const Hoek = require('@hapi/hoek'); const SourceMap = require('source-map'); const SourceMapSupport = require('source-map-support'); +const { reportTemplate } = require('./html/report.js'); +const FindRc = require('find-rc'); const internals = {}; +internals.rcPath = FindRc('lab'); +/* $lab:coverage:off$ */ +internals.rc = internals.rcPath ? require(internals.rcPath) : {}; +/* $lab:coverage:on$ */ + +const { enableTemplateStringHTMLReporter } = internals.rc; + exports = module.exports = internals.Reporter = function (options) { @@ -18,7 +27,7 @@ exports = module.exports = internals.Reporter = function (options) { const filename = Path.join(__dirname, 'html', 'report.html'); const template = Fs.readFileSync(filename, 'utf8'); - + /* $lab:coverage:off$ */ // Display all valid numbers except zeros Handlebars.registerHelper('number', (number) => { @@ -60,6 +69,7 @@ exports = module.exports = internals.Reporter = function (options) { const stack = err.stack.slice(err.stack.indexOf('\n') + 1).replace(/^\s*/gm, ' '); return new Handlebars.SafeString(Hoek.escapeHtml(stack)); }); + /* $lab:coverage:on$ */ const partialsPath = Path.join(__dirname, 'html', 'partials'); const partials = Fs.readdirSync(partialsPath); @@ -284,7 +294,14 @@ internals.Reporter.prototype.end = async function (notebook) { }, this); } - this.report(this.view(context)); + /* $lab:coverage:off$ */ + if (enableTemplateStringHTMLReporter === 'true') { + this.report(reportTemplate(context)); + } + else { + this.report(this.view(context)); + } +/* $lab:coverage:on$ */ }; internals.findLint = function (lint, file) { diff --git a/lib/reporters/html/helpers.js b/lib/reporters/html/helpers.js new file mode 100644 index 00000000..b6ae76ac --- /dev/null +++ b/lib/reporters/html/helpers.js @@ -0,0 +1,45 @@ +'use strict'; + +const Hoek = require('@hapi/hoek'); + +exports.replace = (str, from, to, flags) => { + + return str.replace(new RegExp(from, flags), to); +}; + +// Display all valid numbers except zeros +exports.number = (number) => { + + return +number || ''; +}; + +exports.join = (array, separator) => { + + return array.join(separator); +}; + +exports.lintJoin = (array) => { + + let str = ''; + + for (let i = 0; i < array.length; ++i) { + if (str) { + str += ' '; // This is a line break + } + + str += Hoek.escapeHtml(array[i]); + } + + return `${str}`; +}; + +exports.errorMessage = (err) => { + + return `${Hoek.escapeHtml('' + err.message)}`; +}; + +exports.errorStack = (err) => { + + const stack = err.stack.slice(err.stack.indexOf('\n') + 1).replace(/^\s*/gm, ' '); + return `${Hoek.escapeHtml(stack)}`; +}; diff --git a/lib/reporters/html/partials/cov.js b/lib/reporters/html/partials/cov.js new file mode 100644 index 00000000..5981bfd9 --- /dev/null +++ b/lib/reporters/html/partials/cov.js @@ -0,0 +1,26 @@ +'use strict'; + +const { file } = require('./file'); + +exports.cov = (coverage) => { + + return `
+

Code Coverage Report

+
+
${coverage.percent}%
+
${coverage.cov.sloc}
+
${coverage.cov.hits}
+
${coverage.cov.misses}
+
+
+ + +
+
+ ${coverage.cov.files.map((item, i) => { + + return file(item); + })} +
+
`; +}; diff --git a/lib/reporters/html/partials/css.js b/lib/reporters/html/partials/css.js new file mode 100644 index 00000000..0d6b5de5 --- /dev/null +++ b/lib/reporters/html/partials/css.js @@ -0,0 +1,446 @@ +'use strict'; + +exports.css = () => ``; diff --git a/lib/reporters/html/partials/file.js b/lib/reporters/html/partials/file.js new file mode 100644 index 00000000..5838d979 --- /dev/null +++ b/lib/reporters/html/partials/file.js @@ -0,0 +1,38 @@ +'use strict'; + +const { line } = require('./line'); + +exports.file = (item) => { + + const { filename } = item; + + return `
+

${item.filename} ${item.generated ? `(transformed to ${item.generated})` : ``}

+
+
${item.percent}%
+
${item.sloc}
+
${item.hits}
+
${item.misses}
+
+ + + + + + + + ${item.sourcemaps ? `` : ``} + + + + ${item.source ? + ` + ${Object.entries(item.source).map((subitem,i) => + + ` + ${line(filename, subitem[1], i)} + `)}` : ``} + +
LineLintHitsSourceOriginal line
+
`; +}; diff --git a/lib/reporters/html/partials/line.js b/lib/reporters/html/partials/line.js new file mode 100644 index 00000000..5c19fb82 --- /dev/null +++ b/lib/reporters/html/partials/line.js @@ -0,0 +1,22 @@ +'use strict'; + +const { number } = require('../helpers'); +const { lint } = require('./lint'); + +exports.line = (filename, item, key) => { + + return ` + ${key} + ${lint(item.lintErrors)} + ${item.miss ? `${number(item.percent)}` : `${number(item.hits)}`} + ${item.chunks ? + `${item.chunks.map((subitem,i) => + + `${subitem.miss ? `
${subitem.source}
` : `
${subitem.source}
`}` + ).join('')}` + : + `${item.source}` +} + ${item.originalFilename ? `${item.originalLine}` : ``} +`; +}; diff --git a/lib/reporters/html/partials/lint-file.js b/lib/reporters/html/partials/lint-file.js new file mode 100644 index 00000000..bacfda92 --- /dev/null +++ b/lib/reporters/html/partials/lint-file.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.lintFile = (item) => { + + return `
+ ${item.errors ? + `

${item.filename}

+ ` : ``} +
`; +}; diff --git a/lib/reporters/html/partials/lint.js b/lib/reporters/html/partials/lint.js new file mode 100644 index 00000000..5be984bb --- /dev/null +++ b/lib/reporters/html/partials/lint.js @@ -0,0 +1,20 @@ +'use strict'; + +const { lintJoin } = require('../helpers'); + +exports.lint = (lintErrors) => { + + if ( lintErrors ) { + return ` + ${lintErrors.errors ? + `` + : `test`} + ${lintErrors.warnings ? + `` + : ``} + `; + } + + return ``; + +}; diff --git a/lib/reporters/html/partials/linting.js b/lib/reporters/html/partials/linting.js new file mode 100644 index 00000000..2dc3a667 --- /dev/null +++ b/lib/reporters/html/partials/linting.js @@ -0,0 +1,40 @@ +'use strict'; + +const { lintFile } = require('./lint-file'); + +const lintingPartialMainChild = function (lint) { + + if ( lint.total ) { + return `${lint.lint.map((item,i) => + + lintFile(item,i) + )}`; + } + + return ``; + +}; + +const lintingPartialMain = function (lint) { + + if ( lint.disabled ) { + return `Nothing to show here, linting is disabled.`; + } + + return `
+ ${lint.totalErrors} + ${lint.totalWarnings} +
+
+ ${lintingPartialMainChild(lint)} +
`; + +}; + +exports.linting = (lint) => { + + return `
+

Linting Report

+ ${lintingPartialMain(lint)} +
`; +}; diff --git a/lib/reporters/html/partials/menu.js b/lib/reporters/html/partials/menu.js new file mode 100644 index 00000000..fe71e5c5 --- /dev/null +++ b/lib/reporters/html/partials/menu.js @@ -0,0 +1,25 @@ +'use strict'; + +exports.menu = (coverage, lint) => { + + return ``; +}; diff --git a/lib/reporters/html/partials/scripts.js b/lib/reporters/html/partials/scripts.js new file mode 100644 index 00000000..2b24db51 --- /dev/null +++ b/lib/reporters/html/partials/scripts.js @@ -0,0 +1,89 @@ +'use strict'; + +exports.scripts = () => { + + return ``; +}; diff --git a/lib/reporters/html/partials/tests.js b/lib/reporters/html/partials/tests.js new file mode 100644 index 00000000..57d37295 --- /dev/null +++ b/lib/reporters/html/partials/tests.js @@ -0,0 +1,65 @@ +'use strict'; + +const { replace, join, errorMessage, errorStack } = require('../helpers'); + +exports.tests = (failures, skipped, tests, duration, paths, errors) => { + + return `
+

Test Report

+
+
${failures.length}
+
${skipped.length}
+
${tests.length}
+
${duration}
+
+
+ + + + +
+ ${paths.map((item,i) => + + ` + + + `)} +
+ + + + + + + + + + ${tests.map((item,i) => + + ` + + + + + + `)} + +
IDTitleDuration (ms)
${item.id}${item.title} + ${item.err ? `
${item.err.stack}
` : ``} +
${item.duration}
+ + ${errors.length ? + `

Script errors :

+ ` : ``} +
`; +}; diff --git a/lib/reporters/html/report.js b/lib/reporters/html/report.js new file mode 100644 index 00000000..4b610ac5 --- /dev/null +++ b/lib/reporters/html/report.js @@ -0,0 +1,26 @@ +'use strict'; + +const { scripts } = require('./partials/scripts'); +const { css } = require('./partials/css'); +const { menu } = require('./partials/menu'); +const { tests } = require('./partials/tests'); +const { cov } = require('./partials/cov'); +const { linting } = require('./partials/linting'); + +exports.reportTemplate = function (context) { + + return ` + + + Tests & Coverage + ${scripts()} + ${css()} + + + ${menu(context.coverage, context.lint)} + ${tests(context.failures, context.skipped, context.tests, context.duration, context.paths, context.errors)} + ${cov(context.coverage)} + ${linting(context.lint)} + +`; +}; diff --git a/package.json b/package.json index 291af18c..1f7a9f9b 100755 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "eslint": "6.x.x", "find-rc": "4.x.x", "globby": "10.x.x", - "handlebars": "4.x.x", + "handlebars": "^4.7.3", "seedrandom": "3.x.x", "source-map": "0.7.x", "source-map-support": "0.5.x", @@ -44,7 +44,8 @@ }, "scripts": { "test": "node ./bin/_lab -f -L -t 100 -m 10000 --types-test test/index.ts", - "test-cov-html": "node ./bin/_lab -f -L -r html -m 10000 -o coverage.html" + "test-cov-html": "node ./bin/_lab -f -L -r html -m 10000 -o coverage.html", + "lint": "node ./bin/_lab -d -f -L" }, "license": "BSD-3-Clause" } diff --git a/test/reporters.js b/test/reporters.js index e227fd61..82313bc6 100755 --- a/test/reporters.js +++ b/test/reporters.js @@ -1385,6 +1385,193 @@ describe('Reporter', () => { }); describe('html', () => { + + it('generates corresponding lint-file parital when there are errors', () => { + + const { lintFile } = require('../lib/reporters/html/partials/lint-file'); + + const options = { + errors: [ + { + line: 19, + severity: 'high', + message: 'some message' + } + ], + filename: 'some-file-name.js' + + }; + expect(lintFile(options)).to.include('
  • L19 - high - some message
  • '); + }); + + it('generates corresponding lint-file partial when there are no errors', () => { + + const { lintFile } = require('../lib/reporters/html/partials/lint-file'); + + const options = { + filename: 'some-file-name.js' + + }; + expect(lintFile(options)).to.include('
    \n \n
    '); + }); + + it('generates corresponding lint partial when there are lint errors', () => { + + const { lint } = require('../lib/reporters/html/partials/lint'); + + const options = { + errors: ['error 1','error 2', 'error 3', 'error 4'] + }; + expect(lint(options)).to.include(''); + }); + + it('generates corresponding lint partial when there are lint warnings', () => { + + const { lint } = require('../lib/reporters/html/partials/lint'); + + const options = { + warnings: ['warning 1', 'warning 2'] + }; + expect(lint(options)).to.include(''); + }); + + it('generates corresponding linting partial when lint is disabled', () => { + + const { linting } = require('../lib/reporters/html/partials/linting'); + + const options = { + disabled: true + }; + expect(linting(options)).to.include('Nothing to show here, linting is disabled.'); + }); + + it('generates corresponding linting partial when lint is enabled ', () => { + + const { linting } = require('../lib/reporters/html/partials/linting'); + + const options = { + errorClass: 'error', + totalErrors: 10, + warningClass: 'warning', + totalWarnings: 13, + lint: [ + { + errors: ['error1'], + warnings: ['warning 1', 'warning 2'] + } + ], + total: 23 + }; + + expect(linting(options)).to.include('10'); + }); + + it('generates corresponding linting partial when total is undefined', () => { + + const { linting } = require('../lib/reporters/html/partials/linting'); + + const options = { + errorClass: 'error', + totalErrors: 10, + warningClass: 'warning', + totalWarnings: 13, + lint: [ + { + warnings: ['warning 1', 'warning 2'] + } + ] + }; + + expect(linting(options)).to.not.include('
    '); + }); + + it('generates corresponding tests partial when there are failures', () => { + + const { tests } = require('../lib/reporters/html/partials/tests'); + + const options = { + failures: ['failure 1, failure 2'], + skipped: ['s1', 's2', 's3'], + tests: [{ + path: ['p1', 'p2'] + }], + duration: 10, + paths: [], + errors: [] + }; + + expect(tests(options.failures, options.skipped, options.tests, options.duration, options.paths, options.errors)).to.include('
    '); + }); + + it('generates corresponding tests partial for errors with stack', () => { + + const { tests } = require('../lib/reporters/html/partials/tests'); + + const options = { + failures: ['failure 1, failure 2'], + skipped: ['s1', 's2', 's3'], + tests: [{ + path: ['p1', 'p2'] + }], + duration: 10, + paths: [], + errors: [ + { + message: 'some error message', + stack: `Unexpected identifier + at wrapSafe (internal/modules/cjs/loader.js:1072:16) + at Module._compile (internal/modules/cjs/loader.js:1122:27) + at Object.require.extensions. [as .js] (/Users/aorinevo/Repositories/hapi/lab/lib/coverage.js:127:113) + at Module.load (internal/modules/cjs/loader.js:1002:32)` + } + ] + + }; + const compiledTests = tests(options.failures, options.skipped, options.tests, options.duration, options.paths, options.errors); + expect(compiledTests).to.include('
    some error message
    '); + expect(compiledTests).to.include('
      at wrapSafe (internal/modules/cjs/loader.js:1072:16)
    ');
    +        });
    +
    +        it('generates corresponding tests partial for errors without stack', () => {
    +
    +            const { tests } = require('../lib/reporters/html/partials/tests');
    +
    +            const options = {
    +                failures: [],
    +                skipped: ['s1', 's2', 's3'],
    +                tests: [{
    +                    path: ['p1', 'p2']
    +                }],
    +                duration: 10,
    +                paths: [],
    +                errors: [
    +                    {
    +                        message: 'some error message'
    +                    }
    +                ]
    +            };
    +            const compiledTests = tests(options.failures, options.skipped, options.tests, options.duration, options.paths, options.errors);
    +            expect(compiledTests).to.include('
    some error message
    '); + expect(compiledTests).to.not.include(' { + + const { tests } = require('../lib/reporters/html/partials/tests'); + + const options = { + failures: [], + skipped: ['s1', 's2', 's3'], + tests: [{ + path: ['p1', 'p2'] + }], + duration: 10, + paths: [], + errors: [] + }; + + expect(tests(options.failures, options.skipped, options.tests, options.duration, options.paths, options.errors)).to.include('
    '); + }); it('generates a coverage report', async () => { @@ -1427,7 +1614,7 @@ describe('Reporter', () => { '
    while (
    value )
    {
    ']); expect(output, 'missed original line not included').to.contains([ '', - ' value = false;']); + ' value = false;']); delete global.__$$testCovHtml; });