diff --git a/.travis.yml b/.travis.yml
index 4796715..c3a66e3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,3 @@
language: node_js
node_js:
- - "node"
\ No newline at end of file
+ - "11.10.1"
\ No newline at end of file
diff --git a/README.md b/README.md
index c6a58ba..9e81398 100644
--- a/README.md
+++ b/README.md
@@ -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).
diff --git a/package.json b/package.json
index 915d232..3a46f12 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/config.js b/src/config.js
index 83b1073..ccc55b2 100644
--- a/src/config.js
+++ b/src/config.js
@@ -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}
@@ -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,
@@ -125,7 +139,9 @@ module.exports = {
shouldIncludeConsoleLog,
shouldUseCssFile,
getExecutionTimeWarningThreshold,
+ getBoilerplatePath,
getTheme,
getDateFormat,
getSort,
+ getStatusIgnoreFilter,
};
diff --git a/src/reportGenerator.js b/src/reportGenerator.js
index 7bf26fd..4414935 100644
--- a/src/reportGenerator.js
+++ b/src/reportGenerator.js
@@ -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');
@@ -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 }) {
@@ -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(``);
}
+ 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
- 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
+ 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(``);
+
+ // 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;
}
}
diff --git a/src/utils.js b/src/utils.js
index cac72d6..224b767 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -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}
@@ -77,6 +90,7 @@ const sortAlphabetically = ({ a, b, reversed }) => {
module.exports = {
logMessage,
writeFile,
+ getFileContent,
createHtmlBase,
sortAlphabetically,
};
diff --git a/test/boilerplate.html b/test/boilerplate.html
new file mode 100644
index 0000000..0428b81
--- /dev/null
+++ b/test/boilerplate.html
@@ -0,0 +1,9 @@
+
+
+ jest-html-reporter Boilerplate Test
+
+
+ This is a boilerplate test. The body of the report should be rendered below.
+ {jesthtmlreporter-content}
+
+
\ No newline at end of file
diff --git a/test/config.spec.js b/test/config.spec.js
index bb6f23a..93b2cd6 100644
--- a/test/config.spec.js
+++ b/test/config.spec.js
@@ -10,6 +10,7 @@ describe('config', () => {
config.setConfigData({
outputPath: null,
theme: null,
+ boilerplate: null,
styleOverridePath: null,
pageTitle: null,
logo: null,
@@ -19,9 +20,11 @@ describe('config', () => {
dateFormat: null,
sort: null,
executionMode: null,
+ statusIgnoreFilter: null,
});
delete process.env.JEST_HTML_REPORTER_OUTPUT_PATH;
delete process.env.JEST_HTML_REPORTER_THEME;
+ delete process.env.JEST_HTML_REPORTER_BOILERPLATE;
delete process.env.JEST_HTML_REPORTER_STYLE_OVERRIDE_PATH;
delete process.env.JEST_HTML_REPORTER_PAGE_TITLE;
delete process.env.JEST_HTML_REPORTER_LOGO;
@@ -31,6 +34,7 @@ describe('config', () => {
delete process.env.JEST_HTML_REPORTER_DATE_FORMAT;
delete process.env.JEST_HTML_REPORTER_SORT;
delete process.env.JEST_HTML_REPORTER_EXECUTION_MODE;
+ delete process.env.JEST_HTML_REPORTER_STATUS_FILTER;
});
describe('setup', () => {
@@ -182,4 +186,32 @@ describe('config', () => {
expect(config.getSort()).toEqual('default');
});
});
+
+ describe('getBoilerplatePath', () => {
+ it('should return the value from package.json or jesthtmlreporter.config.json', () => {
+ config.setConfigData({ boilerplate: 'setInJson' });
+ expect(config.getBoilerplatePath()).toEqual('setInJson');
+ });
+ it('should return the environment variable', () => {
+ process.env.JEST_HTML_REPORTER_BOILERPLATE = 'setInEnv';
+ expect(config.getBoilerplatePath()).toEqual('setInEnv');
+ });
+ it('should return the default value if no setting was provided', () => {
+ expect(config.getBoilerplatePath()).toBeNull();
+ });
+ });
+
+ describe('getStatusIgnoreFilter', () => {
+ it('should return the value from package.json or jesthtmlreporter.config.json', () => {
+ config.setConfigData({ statusIgnoreFilter: 'setInJson' });
+ expect(config.getStatusIgnoreFilter()).toEqual('setInJson');
+ });
+ it('should return the environment variable', () => {
+ process.env.JEST_HTML_REPORTER_STATUS_FILTER = 'setInEnv';
+ expect(config.getStatusIgnoreFilter()).toEqual('setInEnv');
+ });
+ it('should return the default value if no setting was provided', () => {
+ expect(config.getStatusIgnoreFilter()).toBeNull();
+ });
+ });
});
diff --git a/test/reportGenerator.spec.js b/test/reportGenerator.spec.js
index eeb3555..c5942cd 100644
--- a/test/reportGenerator.spec.js
+++ b/test/reportGenerator.spec.js
@@ -1,44 +1,46 @@
const mockdata = require('./mockdata');
const ReportGenerator = require('../src/reportGenerator');
+const mockedConfig = {
+ getOutputFilepath: () => 'test-report.html',
+ getBoilerplatePath: () => 'test/boilerplate.html',
+ getStylesheetFilepath: () => '../style/defaultTheme.css',
+ getPageTitle: () => 'Test Report',
+ getLogo: () => 'testLogo.png',
+ getDateFormat: () => 'yyyy-mm-dd HH:MM:ss',
+ getSort: () => 'default',
+ shouldIncludeFailureMessages: () => true,
+ getExecutionTimeWarningThreshold: () => 5,
+ getCustomScriptFilepath: () => 'test.js',
+ shouldUseCssFile: () => false,
+ getStatusIgnoreFilter: () => null,
+};
+
describe('reportGenerator', () => {
describe('renderHtmlReport', () => {
it('should return a HTML report based on the given input data', () => {
- const mockedConfig = {
- getOutputFilepath: () => 'test-report.html',
- getStylesheetFilepath: () => '../style/defaultTheme.css',
- getPageTitle: () => 'Test Report',
- getLogo: () => 'testLogo.png',
- getDateFormat: () => 'yyyy-mm-dd HH:MM:ss',
- getSort: () => 'default',
- shouldIncludeFailureMessages: () => true,
- getExecutionTimeWarningThreshold: () => 5,
- getCustomScriptFilepath: () => 'test.js',
- shouldUseCssFile: () => false,
- };
const reportGenerator = new ReportGenerator(mockedConfig);
return reportGenerator.renderHtmlReport({ data: mockdata.jestResponse.multipleTestResults, stylesheet: '' })
- .then(xmlBuilderOutput =>
- expect(xmlBuilderOutput).not.toBeNull());
+ .then(xmlBuilderOutput => expect(xmlBuilderOutput.substring(0, 6)).toEqual(''));
});
it('should return reject the promise if no data was provided', () => {
expect.assertions(1);
- const mockedConfig = {
- getOutputFilepath: () => 'test-report.html',
- getStylesheetFilepath: () => '../style/defaultTheme.css',
- getPageTitle: () => 'Test Report',
- getDateFormat: () => 'yyyy-mm-dd HH:MM:ss',
- getSort: () => 'default',
- shouldIncludeFailureMessages: () => true,
- getExecutionTimeWarningThreshold: () => 5,
- getCustomScriptFilepath: () => 'test.js',
- };
const reportGenerator = new ReportGenerator(mockedConfig);
return expect(reportGenerator.renderHtmlReport({ data: null, stylesheet: null })).rejects
.toHaveProperty('message', 'Test data missing or malformed');
});
});
+
+ describe('getReportBody', () => {
+ it('should filter the ignored result statuses', () => {
+ mockedConfig.getStatusIgnoreFilter = () => 'passed';
+ const reportGenerator = new ReportGenerator(mockedConfig);
+
+ const result = reportGenerator.getReportBody({ data: mockdata.jestResponse.multipleTestResults, pageTitle: '' });
+ return expect(result.toString().indexOf('
') !== -1).toEqual(false);
+ });
+ });
});