From 030501e8dbbe1a7e442a5ab21d7f1e9fe0586ae9 Mon Sep 17 00:00:00 2001 From: Johan Hargne Date: Sat, 20 May 2023 11:29:59 +0200 Subject: [PATCH 1/5] Added additional escaping of terminal color codes --- src/htmlreporter.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/htmlreporter.ts b/src/htmlreporter.ts index 6ab95fb..b8a6a7c 100644 --- a/src/htmlreporter.ts +++ b/src/htmlreporter.ts @@ -760,10 +760,12 @@ class HTMLReporter { */ private sanitizeOutput(input: string) { return stripAnsi( - input.replace( - /([^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFC\u{10000}-\u{10FFFF}])/gu, - "" - ) + input + .replace(/(\x1b\[\d*m)/g, "") + .replace( + /([^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFC\u{10000}-\u{10FFFF}])/gu, + "" + ) ); } } From fe38c2e818337a04cfed4d9d8010087f694cf802 Mon Sep 17 00:00:00 2001 From: Johan Hargne Date: Sat, 20 May 2023 15:24:06 +0200 Subject: [PATCH 2/5] Added possibility to collapse test suits --- README.md | 1 + src/htmlreporter.ts | 135 +++++++++++++++++++------------------- src/types/index.d.ts | 1 + style/defaultTheme.css | 28 ++++++++ test/htmlreporter.spec.ts | 47 +++++++------ 5 files changed, 123 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 8a08c98..476d616 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Please note that all configuration properties are optional. | ------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | `append` | `BOOLEAN` | If set to true, new test results will be appended to the existing test report | `false` | | `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` | +| `collapseSuitsByDefault` | `BOOLEAN` | Whether to collapse test suites by default or not | `false` | | `customScriptPath` | `STRING` | Path to a javascript file that should be injected into the test report | `null` | | `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"` | | `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` | diff --git a/src/htmlreporter.ts b/src/htmlreporter.ts index 6ab95fb..37264f8 100644 --- a/src/htmlreporter.ts +++ b/src/htmlreporter.ts @@ -127,11 +127,25 @@ class HTMLReporter { }; } - public renderTestSuiteInfo(parent: XMLElement, suite: TestResult) { - const suiteInfo = parent.ele("div", { class: "suite-info" }); - // Suite Path + public renderTestSuiteInfo( + parent: XMLElement, + suite: TestResult, + suiteIndex: number + ) { + const collapsibleId = `collapsible-${suiteIndex}`; + parent.ele("input", { + id: collapsibleId, + type: "checkbox", + class: "toggle", + checked: !this.getConfigValue("collapseSuitsByDefault") + ? "checked" + : null, + }); + const collapsibleLabel = parent.ele("label", { for: collapsibleId }); + const suiteInfo = collapsibleLabel.ele("div", { + class: "suite-info", + }); suiteInfo.ele("div", { class: "suite-path" }, suite.testFilePath); - // Suite execution time const executionTime = (suite.perfStats.end - suite.perfStats.start) / 1000; suiteInfo.ele( "div", @@ -147,38 +161,6 @@ class HTMLReporter { ); } - public renderSuiteFailure(parent: XMLElement, suite: TestResult, i: number) { - const suiteContainer = parent.ele("div", { - id: `suite-${i + 1}`, - class: "suite-container", - }); - - // Suite Information - this.renderTestSuiteInfo(suiteContainer, suite); - - // Test Container - const suiteTests = suiteContainer.ele("div", { - class: "suite-tests", - }); - - const testResult = suiteTests.ele("div", { - class: "test-result failed", - }); - - const failureMsgDiv = testResult.ele( - "div", - { - class: "failureMessages suiteFailure", - }, - " " - ); - failureMsgDiv.ele( - "pre", - { class: "failureMsg" }, - this.sanitizeOutput(suite.failureMessage) - ); - } - public async renderTestReportContent() { try { if (!this.testData || Object.entries(this.testData).length === 0) { @@ -229,6 +211,14 @@ class HTMLReporter { const summaryContainer = metaDataContainer.ele("div", { id: "summary" }); // Suite Summary const suiteSummary = summaryContainer.ele("div", { id: "suite-summary" }); + + const getSummaryClass = ( + type: "passed" | "failed" | "pending", + numberOfSuites: number + ) => + [`summary-${type}`, numberOfSuites > 0 ? "" : " summary-empty"].join( + " " + ); suiteSummary.ele( "div", { class: "summary-total" }, @@ -237,27 +227,21 @@ class HTMLReporter { suiteSummary.ele( "div", { - class: `summary-passed${ - this.testData.numPassedTestSuites === 0 ? " summary-empty" : "" - }`, + class: getSummaryClass("passed", this.testData.numPassedTestSuites), }, `${this.testData.numPassedTestSuites} passed` ); suiteSummary.ele( "div", { - class: `summary-failed${ - this.testData.numFailedTestSuites === 0 ? " summary-empty" : "" - }`, + class: getSummaryClass("failed", this.testData.numFailedTestSuites), }, `${this.testData.numFailedTestSuites} failed` ); suiteSummary.ele( "div", { - class: `summary-pending${ - this.testData.numPendingTestSuites === 0 ? " summary-empty" : "" - }`, + class: getSummaryClass("pending", this.testData.numPendingTestSuites), }, `${this.testData.numPendingTestSuites} pending` ); @@ -285,27 +269,21 @@ class HTMLReporter { testSummary.ele( "div", { - class: `summary-passed${ - this.testData.numPassedTests === 0 ? " summary-empty" : "" - }`, + class: getSummaryClass("passed", this.testData.numPassedTests), }, `${this.testData.numPassedTests} passed` ); testSummary.ele( "div", { - class: `summary-failed${ - this.testData.numFailedTests === 0 ? " summary-empty" : "" - }`, + class: getSummaryClass("failed", this.testData.numFailedTests), }, `${this.testData.numFailedTests} failed` ); testSummary.ele( "div", { - class: `summary-pending${ - this.testData.numPendingTests === 0 ? " summary-empty" : "" - }`, + class: getSummaryClass("pending", this.testData.numPendingTests), }, `${this.testData.numPendingTests} pending` ); @@ -336,31 +314,44 @@ class HTMLReporter { * Test Suites */ if (sortedTestResults) { - sortedTestResults.forEach((suite, i) => { + sortedTestResults.forEach((suite, suiteIndex) => { + const suiteContainer = reportBody.ele("div", { + id: `suite-${suiteIndex + 1}`, + class: "suite-container", + }); + // Suite Information + this.renderTestSuiteInfo(suiteContainer, suite, suiteIndex); + // Test Container + const suiteTests = suiteContainer.ele("div", { + class: "suite-tests", + }); + // Ignore this suite if there are no results if (!suite.testResults || suite.testResults.length <= 0) { + // Include the suite failure message if it exists if ( suite.failureMessage && this.getConfigValue("includeSuiteFailure") ) { - this.renderSuiteFailure(reportBody, suite, i); + const testResult = suiteTests.ele("div", { + class: "test-result failed", + }); + const failureMsgDiv = testResult.ele( + "div", + { + class: "failureMessages suiteFailure", + }, + " " + ); + failureMsgDiv.ele( + "pre", + { class: "failureMsg" }, + this.sanitizeOutput(suite.failureMessage) + ); } return; } - const suiteContainer = reportBody.ele("div", { - id: `suite-${i + 1}`, - class: "suite-container", - }); - - // Suite Information - this.renderTestSuiteInfo(suiteContainer, suite); - - // Test Container - const suiteTests = suiteContainer.ele("div", { - class: "suite-tests", - }); - // Test Results suite.testResults // Filter out the test results with statuses that equals the statusIgnoreFilter @@ -520,6 +511,7 @@ class HTMLReporter { const { append, boilerplate, + collapseSuitsByDefault, customScriptPath, dateFormat, executionTimeWarningThreshold, @@ -549,6 +541,11 @@ class HTMLReporter { environmentVariable: "JEST_HTML_REPORTER_BOILERPLATE", configValue: boilerplate, }, + collapseSuitsByDefault: { + defaultValue: false, + environmentVariable: "JEST_HTML_REPORTER_COLLAPSE_SUITES_BY_DEFAULT", + configValue: collapseSuitsByDefault, + }, customScriptPath: { defaultValue: null, environmentVariable: "JEST_HTML_REPORTER_CUSTOM_SCRIPT_PATH", diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 02ddf44..63847e0 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -12,6 +12,7 @@ export interface JestHTMLReporterProps { export type IJestHTMLReporterConfigOptions = { append?: boolean; boilerplate?: string; + collapseSuitsByDefault?: boolean; customScriptPath?: string; dateFormat?: string; executionTimeWarningThreshold?: number; diff --git a/style/defaultTheme.css b/style/defaultTheme.css index bb659ec..48bafc7 100644 --- a/style/defaultTheme.css +++ b/style/defaultTheme.css @@ -149,6 +149,21 @@ header { .suite-container { margin-bottom: 2rem; } +.suite-container > input[type="checkbox"] { + position: absolute; + left: -100vw; +} +.suite-container label { + display: block; +} +.suite-container .suite-tests { + overflow-y: hidden; + height: 0; +} +.suite-container > input[type="checkbox"]:checked ~ .suite-tests { + height: auto; + overflow: visible; +} .suite-info { padding: 1rem; background-color: #eee; @@ -157,6 +172,10 @@ header { align-items: center; margin-bottom: 0.25rem; } +.suite-info:hover { + background-color: #ddd; + cursor: pointer; +} .suite-info .suite-path { word-break: break-all; flex-grow: 1; @@ -172,6 +191,15 @@ header { background-color: #d8000c; color: #fff; } +.suite-info:before { + content: "\2303"; + display: inline-block; + margin-right: 0.5rem; + transform: rotate(0deg); +} +.suite-container > input[type="checkbox"]:checked ~ label .suite-info:before { + transform: rotate(180deg); +} /* CONSOLE LOGS */ .suite-consolelog { diff --git a/test/htmlreporter.spec.ts b/test/htmlreporter.spec.ts index aa39fd6..5413e94 100644 --- a/test/htmlreporter.spec.ts +++ b/test/htmlreporter.spec.ts @@ -51,26 +51,6 @@ describe("HTMLReporter", () => { }); describe("config options", () => { - /* TODO: The following test runs locally, but fails in Travis CI - describe("boilerplate", () => { - it("should insert the test report HTML into the given file", async () => { - const mockedFS = jest.spyOn(fs, "readFileSync"); - mockedFS.mockImplementation( - () => "
{jesthtmlreporter-content}
" - ); - const reporter = new HTMLReporter(mockedJestResponseSingleTestResult, { - boilerplate: path.join(process.cwd(), "/path/to/boilerplate.html") - }); - - const report = await reporter.renderTestReport(); - expect(report).toEqual( - `
${mockedSingleTestResultReportHTML}
` - ); - mockedFS.mockRestore(); - }); - }); - */ - describe("styleOverridePath", () => { it("should insert a link to the overriding stylesheet path", async () => { const reporter = new HTMLReporter({ @@ -406,4 +386,31 @@ describe("HTMLReporter", () => { expect(result).toBe("test/reporter.html"); }); }); + + describe("collapseSuitsByDefault", () => { + it("should show the contents of test suites by default", async () => { + const reporter = new HTMLReporter({ + testData: mockedJestResponseSingleTestResult, + options: {}, + }); + const report = await reporter.renderTestReport(); + console.log(report.fullHtml); + expect( + report.fullHtml.indexOf('class="toggle" checked="checked"') + ).toBeGreaterThan(-1); + }); + + it("should hide the contents of test suites", async () => { + const reporter = new HTMLReporter({ + testData: mockedJestResponseSingleTestResult, + options: { + collapseSuitsByDefault: true, + }, + }); + const report = await reporter.renderTestReport(); + expect(report.fullHtml.indexOf('class="toggle" checked="checked"')).toBe( + -1 + ); + }); + }); }); From c37a2070321c32ddf050fac57bb70747af648b63 Mon Sep 17 00:00:00 2001 From: Johan Hargne Date: Sat, 20 May 2023 22:50:23 +0200 Subject: [PATCH 3/5] Minor refactoring --- src/htmlreporter.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/htmlreporter.ts b/src/htmlreporter.ts index 37264f8..41f7519 100644 --- a/src/htmlreporter.ts +++ b/src/htmlreporter.ts @@ -127,36 +127,39 @@ class HTMLReporter { }; } - public renderTestSuiteInfo( + public renderTestSuiteHeader( parent: XMLElement, suite: TestResult, suiteIndex: number ) { - const collapsibleId = `collapsible-${suiteIndex}`; + const collapsibleBtnId = `collapsible-${suiteIndex}`; parent.ele("input", { - id: collapsibleId, + id: collapsibleBtnId, type: "checkbox", class: "toggle", checked: !this.getConfigValue("collapseSuitsByDefault") ? "checked" : null, }); - const collapsibleLabel = parent.ele("label", { for: collapsibleId }); - const suiteInfo = collapsibleLabel.ele("div", { + const collapsibleBtnContainer = parent.ele("label", { + for: collapsibleBtnId, + }); + + const suiteInfo = collapsibleBtnContainer.ele("div", { class: "suite-info", }); suiteInfo.ele("div", { class: "suite-path" }, suite.testFilePath); const executionTime = (suite.perfStats.end - suite.perfStats.start) / 1000; + const suiteExecutionTimeClass = ["suite-time"]; + if ( + executionTime > + (this.getConfigValue("executionTimeWarningThreshold") as number) + ) { + suiteExecutionTimeClass.push("warn"); + } suiteInfo.ele( "div", - { - class: `suite-time${ - executionTime > - (this.getConfigValue("executionTimeWarningThreshold") as number) - ? " warn" - : "" - }`, - }, + { class: suiteExecutionTimeClass.join(" ") }, `${executionTime}s` ); } @@ -320,7 +323,7 @@ class HTMLReporter { class: "suite-container", }); // Suite Information - this.renderTestSuiteInfo(suiteContainer, suite, suiteIndex); + this.renderTestSuiteHeader(suiteContainer, suite, suiteIndex); // Test Container const suiteTests = suiteContainer.ele("div", { class: "suite-tests", From 2fc92555cc8c1fbd3683a6137babc26ae232bd02 Mon Sep 17 00:00:00 2001 From: Johan Hargne Date: Sat, 20 May 2023 22:54:28 +0200 Subject: [PATCH 4/5] Fixed typo --- README.md | 2 +- src/htmlreporter.ts | 8 ++++---- src/types/index.d.ts | 2 +- test/htmlreporter.spec.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 476d616..e539af3 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Please note that all configuration properties are optional. | ------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | `append` | `BOOLEAN` | If set to true, new test results will be appended to the existing test report | `false` | | `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` | -| `collapseSuitsByDefault` | `BOOLEAN` | Whether to collapse test suites by default or not | `false` | +| `collapseSuitesByDefault` | `BOOLEAN` | Whether to collapse test suites by default or not | `false` | | `customScriptPath` | `STRING` | Path to a javascript file that should be injected into the test report | `null` | | `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"` | | `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` | diff --git a/src/htmlreporter.ts b/src/htmlreporter.ts index 41f7519..6b71a50 100644 --- a/src/htmlreporter.ts +++ b/src/htmlreporter.ts @@ -137,7 +137,7 @@ class HTMLReporter { id: collapsibleBtnId, type: "checkbox", class: "toggle", - checked: !this.getConfigValue("collapseSuitsByDefault") + checked: !this.getConfigValue("collapseSuitesByDefault") ? "checked" : null, }); @@ -514,7 +514,7 @@ class HTMLReporter { const { append, boilerplate, - collapseSuitsByDefault, + collapseSuitesByDefault, customScriptPath, dateFormat, executionTimeWarningThreshold, @@ -544,10 +544,10 @@ class HTMLReporter { environmentVariable: "JEST_HTML_REPORTER_BOILERPLATE", configValue: boilerplate, }, - collapseSuitsByDefault: { + collapseSuitesByDefault: { defaultValue: false, environmentVariable: "JEST_HTML_REPORTER_COLLAPSE_SUITES_BY_DEFAULT", - configValue: collapseSuitsByDefault, + configValue: collapseSuitesByDefault, }, customScriptPath: { defaultValue: null, diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 63847e0..5f5114b 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -12,7 +12,7 @@ export interface JestHTMLReporterProps { export type IJestHTMLReporterConfigOptions = { append?: boolean; boilerplate?: string; - collapseSuitsByDefault?: boolean; + collapseSuitesByDefault?: boolean; customScriptPath?: string; dateFormat?: string; executionTimeWarningThreshold?: number; diff --git a/test/htmlreporter.spec.ts b/test/htmlreporter.spec.ts index 5413e94..28054d5 100644 --- a/test/htmlreporter.spec.ts +++ b/test/htmlreporter.spec.ts @@ -387,7 +387,7 @@ describe("HTMLReporter", () => { }); }); - describe("collapseSuitsByDefault", () => { + describe("collapseSuitesByDefault", () => { it("should show the contents of test suites by default", async () => { const reporter = new HTMLReporter({ testData: mockedJestResponseSingleTestResult, @@ -404,7 +404,7 @@ describe("HTMLReporter", () => { const reporter = new HTMLReporter({ testData: mockedJestResponseSingleTestResult, options: { - collapseSuitsByDefault: true, + collapseSuitesByDefault: true, }, }); const report = await reporter.renderTestReport(); From 5370b242cf1e322231e39a05648b599757ec0643 Mon Sep 17 00:00:00 2001 From: Johan Hargne Date: Thu, 25 May 2023 20:07:27 +0200 Subject: [PATCH 5/5] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6eff704..979bd05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jest-html-reporter", - "version": "3.9.0", + "version": "3.10.0", "description": "Jest test results processor for generating a summary in HTML", "main": "dist/index.js", "unpkg": "dist/index.js",