Skip to content

Commit

Permalink
Merge pull request #65 from Hargne/feature/2.5.0
Browse files Browse the repository at this point in the history
v2.5.0
  • Loading branch information
Hargne authored Mar 9, 2019
2 parents 8dab5aa + 72217f9 commit b1e70de
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 126 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
language: node_js
node_js:
- "node"
- "11.10.1"
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ Please note that all configuration properties are optional.
| `executionTimeWarningThreshold` | `NUMBER` | The threshold for test execution time (in seconds) in each test suite that will render a warning on the report page. 5 seconds is the default timeout in Jest. | `5`
| `dateFormat` | `STRING` | The format in which date/time should be formatted in the test report. Have a look in the [documentation](https://github.com/Hargne/jest-html-reporter/wiki/Date-Format) for the available date format variables. | `"yyyy-mm-dd HH:MM:ss"`
| `sort` | `STRING` | Sorts the test results using the given method. Available sorting methods can be found in the [documentation](https://github.com/Hargne/jest-html-reporter/wiki/Sorting-Methods). | `"default"`
| `statusIgnoreFilter` | `STRING` | A comma-separated string of the test result statuses that should be ignored when rendering the report. Available statuses are: `"passed"`, `"pending"`, `"failed"` | `null`
| `boilerplate` | `STRING` | The path to a boilerplate file that should be used to render the body of the test results into. `{jesthtmlreporter-content}` within the boilerplate will be replaced with the test results | `null`

> *The plugin will search for the *styleOverridePath* from the root directory, therefore there is no need to prepend the string with `./` or `../` - You can read more about the themes in the [documentation](https://github.com/Hargne/jest-html-reporter/wiki/Test-Report-Themes).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jest-html-reporter",
"version": "2.4.5",
"version": "2.5.0",
"description": "Jest test results processor for generating a summary in HTML",
"main": "dist/main",
"unpkg": "dist/main.min.js",
Expand Down
16 changes: 16 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ const setup = () => {
const getOutputFilepath = () =>
process.env.JEST_HTML_REPORTER_OUTPUT_PATH || config.outputPath || path.join(process.cwd(), 'test-report.html');

/**
* Returns the configured path to a boilerplate file to be used
* @return {String}
*/
const getBoilerplatePath = () =>
process.env.JEST_HTML_REPORTER_BOILERPLATE || config.boilerplate || null;

/**
* Returns the configured name of theme to be used for styling the report
* @return {String}
Expand Down Expand Up @@ -112,6 +119,13 @@ const getDateFormat = () =>
const getSort = () =>
process.env.JEST_HTML_REPORTER_SORT || config.sort || 'default';

/**
* Returns the filter of test result statuses to be ignored
* @return {String}
*/
const getStatusIgnoreFilter = () =>
process.env.JEST_HTML_REPORTER_STATUS_FILTER || config.statusIgnoreFilter || null;

module.exports = {
config,
setup,
Expand All @@ -125,7 +139,9 @@ module.exports = {
shouldIncludeConsoleLog,
shouldUseCssFile,
getExecutionTimeWarningThreshold,
getBoilerplatePath,
getTheme,
getDateFormat,
getSort,
getStatusIgnoreFilter,
};
245 changes: 145 additions & 100 deletions src/reportGenerator.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

const fs = require('fs');
const dateFormat = require('dateformat');
const stripAnsi = require('strip-ansi');
const xmlbuilder = require('xmlbuilder');
const utils = require('./utils');
const sorting = require('./sorting');

Expand Down Expand Up @@ -46,27 +46,34 @@ class ReportGenerator {
}));
}

/**
* Reads the boilerplate file given from config
* and attempts to replace any occurrence of {jesthtmlreporter-content}
* with the given content.
* @return {Promise}
*/
integrateContentIntoBoilerplate({ content }) {
const filePath = this.config.getBoilerplatePath();
return new Promise((resolve, reject) => utils.getFileContent({ filePath })
.then(boilerplateContent => resolve(boilerplateContent.replace('{jesthtmlreporter-content}', content)))
.catch(err => reject(err)));
}

/**
* Returns the stylesheet to be required in the test report.
* If styleOverridePath is not defined, it will return the defined theme file.
* @return {Promise}
*/
getStylesheetContent() {
const pathToStylesheet = this.config.getStylesheetFilepath();
return new Promise((resolve, reject) => {
fs.readFile(pathToStylesheet, 'utf8', (err, content) => {
if (err) {
return reject(new Error(`Could not locate the stylesheet: '${pathToStylesheet}': ${err}`));
}
return resolve(content);
});
});
const filePath = this.config.getStylesheetFilepath();
return utils.getFileContent({ filePath });
}

/**
* Returns a HTML containing the test report.
* @param {String} stylesheet
* @param {Object} data The test result data
* @param {Object} data The test result data (required)
* @param {String} stylesheet Optional stylesheet content
* @param {String} stylesheetPath Optional path to an external stylesheet
* @return {xmlbuilder}
*/
renderHtmlReport({ data, stylesheet, stylesheetPath }) {
Expand All @@ -76,104 +83,142 @@ class ReportGenerator {

// Fetch Page Title from config
const pageTitle = this.config.getPageTitle();
// Create Report Body
const reportContent = this.getReportBody({ data, pageTitle });

// ** (CUSTOM) BOILERPLATE OPTION
// Check if a boilerplate has been specified
const boilerplatePath = this.config.getBoilerplatePath();
if (boilerplatePath) {
return this.integrateContentIntoBoilerplate({ content: reportContent })
.then(output => resolve(output));
}

// Create an xmlbuilder object with HTML and Body tags

const htmlOutput = utils.createHtmlBase({
pageTitle,
stylesheet,
stylesheetPath,
});

// HEADER
const header = htmlOutput.ele('header');

// Page Title
header.ele('h1', { id: 'title' }, pageTitle);
// Logo
const logo = this.config.getLogo();
if (logo) {
header.ele('img', { id: 'logo', src: logo });
// ** (DEFAULT) PREDEFINED HTML OPTION
// Create an xmlbuilder object with HTML and HEAD tag
const htmlOutput = utils.createHtmlBase({ pageTitle, stylesheet, stylesheetPath });
// Body tag
const body = htmlOutput.ele('body');
// Add report content to body
body.raw(reportContent);
// Custom Javascript
const customScript = this.config.getCustomScriptFilepath();
if (customScript) {
body.raw(`<script src="${customScript}"></script>`);
}
return resolve(htmlOutput);
});
}

// METADATA
const metaDataContainer = htmlOutput.ele('div', { id: 'metadata-container' });
// Timestamp
const timestamp = new Date(data.startTime);
metaDataContainer.ele('div', { id: 'timestamp' }, `Start: ${dateFormat(timestamp, this.config.getDateFormat())}`);
// Test Summary
metaDataContainer.ele('div', { id: 'summary' }, `
${data.numTotalTests} tests --
${data.numPassedTests} passed /
${data.numFailedTests} failed /
${data.numPendingTests} pending
`);

// Apply the configured sorting of test data
const sortedTestData = sorting.sortSuiteResults({
testData: data.testResults,
sortMethod: this.config.getSort(),
});
/**
* Returns a HTML containing the test report body contentt.
* @param {Object} data The test result data
* @param {String} pageTitle The title of the report
* @return {xmlbuilder}
*/
getReportBody({ data, pageTitle }) {
const reportBody = xmlbuilder.begin().element('div', { id: 'jesthtml-content' });
// HEADER
// **
const header = reportBody.ele('header');
// Page Title
header.ele('h1', { id: 'title' }, pageTitle);
// Logo
const logo = this.config.getLogo();
if (logo) {
header.ele('img', { id: 'logo', src: logo });
}

// Test Suites
sortedTestData.forEach((suite) => {
if (!suite.testResults || suite.testResults.length <= 0) { return; }

// Suite Information
const suiteInfo = htmlOutput.ele('div', { class: 'suite-info' });
// Suite Path
suiteInfo.ele('div', { class: 'suite-path' }, suite.testFilePath);
// Suite execution time
const executionTime = (suite.perfStats.end - suite.perfStats.start) / 1000;
suiteInfo.ele('div', { class: `suite-time${executionTime > 5 ? ' warn' : ''}` }, `${executionTime}s`);

// Suite Test Table
const suiteTable = htmlOutput.ele('table', { class: 'suite-table', cellspacing: '0', cellpadding: '0' });

// Test Results
suite.testResults.forEach((test) => {
const testTr = suiteTable.ele('tr', { class: test.status });

// Suite Name(s)
testTr.ele('td', { class: 'suite' }, test.ancestorTitles.join(' > '));

// Test name
const testTitleTd = testTr.ele('td', { class: 'test' }, test.title);

// Test Failure Messages
if (test.failureMessages && (this.config.shouldIncludeFailureMessages())) {
const failureMsgDiv = testTitleTd.ele('div', { class: 'failureMessages' });
test.failureMessages.forEach((failureMsg) => {
failureMsgDiv.ele('pre', { class: 'failureMsg' }, stripAnsi(failureMsg));
});
}

// Append data to <tr>
testTr.ele('td', { class: 'result' }, (test.status === 'passed') ? `${test.status} in ${test.duration / 1000}s` : test.status);
});
// ** METADATA
const metaDataContainer = reportBody.ele(
'div',
{ id: 'metadata-container' },
);
// Timestamp
const timestamp = new Date(data.startTime);
metaDataContainer.ele(
'div',
{ id: 'timestamp' },
`Start: ${dateFormat(timestamp, this.config.getDateFormat())}`,
);
// Test Summary
metaDataContainer.ele(
'div',
{ id: 'summary' },
`${data.numTotalTests} tests -- ${data.numPassedTests} passed / ${data.numFailedTests} failed / ${data.numPendingTests} pending`,
);

// ** SORTING
// Apply the configured sorting of test data
const sortedTestData = sorting.sortSuiteResults({
testData: data.testResults,
sortMethod: this.config.getSort(),
});

// Test Suite console.logs
if (suite.console && suite.console.length > 0 && (this.config.shouldIncludeConsoleLog())) {
// Console Log Container
const consoleLogContainer = htmlOutput.ele('div', { class: 'suite-consolelog' });
// Console Log Header
consoleLogContainer.ele('div', { class: 'suite-consolelog-header' }, 'Console Log');

// Logs
suite.console.forEach((log) => {
const logElement = consoleLogContainer.ele('div', { class: 'suite-consolelog-item' });
logElement.ele('pre', { class: 'suite-consolelog-item-origin' }, stripAnsi(log.origin));
logElement.ele('pre', { class: 'suite-consolelog-item-message' }, stripAnsi(log.message));
// ** IGNORED STATUSES FILTER
// Setup ignored Test Result Statuses
const statusIgnoreFilter = this.config.getStatusIgnoreFilter();
let ignoredStatuses = [];
if (statusIgnoreFilter) {
ignoredStatuses = statusIgnoreFilter.replace(/\s/g, '').toLowerCase().split(',');
}

// ** TEST SUITES
sortedTestData.forEach((suite) => {
// Filter out the test results with statuses that equals the statusIgnoreFilter
for (let i = suite.testResults.length - 1; i >= 0; i -= 1) {
if (ignoredStatuses.includes(suite.testResults[i].status)) {
suite.testResults.splice(i, 1);
}
}
// Ignore this suite if there are no results
if (!suite.testResults || suite.testResults.length <= 0) { return; }

// Suite Information
const suiteInfo = reportBody.ele('div', { class: 'suite-info' });
// Suite Path
suiteInfo.ele('div', { class: 'suite-path' }, suite.testFilePath);
// Suite execution time
const executionTime = (suite.perfStats.end - suite.perfStats.start) / 1000;
suiteInfo.ele('div', { class: `suite-time${executionTime > 5 ? ' warn' : ''}` }, `${executionTime}s`);

// Suite Test Table
const suiteTable = reportBody.ele('table', { class: 'suite-table', cellspacing: '0', cellpadding: '0' });

// Test Results
suite.testResults.forEach((test) => {
const testTr = suiteTable.ele('tr', { class: test.status });
// Suite Name(s)
testTr.ele('td', { class: 'suite' }, test.ancestorTitles.join(' > '));
// Test name
const testTitleTd = testTr.ele('td', { class: 'test' }, test.title);
// Test Failure Messages
if (test.failureMessages && (this.config.shouldIncludeFailureMessages())) {
const failureMsgDiv = testTitleTd.ele('div', { class: 'failureMessages' });
test.failureMessages.forEach((failureMsg) => {
failureMsgDiv.ele('pre', { class: 'failureMsg' }, stripAnsi(failureMsg));
});
}
// Append data to <tr>
testTr.ele('td', { class: 'result' }, (test.status === 'passed') ? `${test.status} in ${test.duration / 1000}s` : test.status);
});
// Custom Javascript
const customScript = this.config.getCustomScriptFilepath();
if (customScript) {
htmlOutput.raw(`<script src="${customScript}"></script>`);

// Test Suite console.logs
if (suite.console && suite.console.length > 0 && (this.config.shouldIncludeConsoleLog())) {
// Console Log Container
const consoleLogContainer = reportBody.ele('div', { class: 'suite-consolelog' });
// Console Log Header
consoleLogContainer.ele('div', { class: 'suite-consolelog-header' }, 'Console Log');
// Logs
suite.console.forEach((log) => {
const logElement = consoleLogContainer.ele('div', { class: 'suite-consolelog-item' });
logElement.ele('pre', { class: 'suite-consolelog-item-origin' }, stripAnsi(log.origin));
logElement.ele('pre', { class: 'suite-consolelog-item-message' }, stripAnsi(log.message));
});
}
return resolve(htmlOutput);
});

return reportBody;
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ const writeFile = ({ filePath, content }) => new Promise((resolve, reject) => {
});
});

/**
* Reads and returns the content of a given file
* @param {String} filePath
*/
const getFileContent = ({ filePath }) => new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, content) => {
if (err) {
return reject(new Error(`Could not locate file: '${filePath}': ${err}`));
}
return resolve(content);
});
});

/**
* Sets up a basic HTML page to apply the content to
* @return {xmlbuilder}
Expand Down Expand Up @@ -77,6 +90,7 @@ const sortAlphabetically = ({ a, b, reversed }) => {
module.exports = {
logMessage,
writeFile,
getFileContent,
createHtmlBase,
sortAlphabetically,
};
9 changes: 9 additions & 0 deletions test/boilerplate.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<html>
<head>
<title>jest-html-reporter Boilerplate Test</title>
</head>
<body>
This is a boilerplate test. The body of the report should be rendered below.
{jesthtmlreporter-content}
</body>
</html>
Loading

0 comments on commit b1e70de

Please sign in to comment.