-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'pr/26' (HTML escaping of dashboard results)
- Loading branch information
Showing
5 changed files
with
176 additions
and
88 deletions.
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
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,105 @@ | ||
import escapeHTML from "./escape_html" | ||
import { TestResult, TestStatus } from "./test_parser" | ||
|
||
const dashboardUrl = "https://svg.test-summary.com/dashboard.svg" | ||
const passIconUrl = "https://svg.test-summary.com/icon/pass.svg?s=12" | ||
const failIconUrl = "https://svg.test-summary.com/icon/fail.svg?s=12" | ||
const skipIconUrl = "https://svg.test-summary.com/icon/skip.svg?s=12" | ||
// not used: const noneIconUrl = 'https://svg.test-summary.com/icon/none.svg?s=12' | ||
|
||
const unnamedTestCase = "<no name>" | ||
|
||
const footer = `This test report was produced by the <a href="https://github.com/test-summary/action">test-summary action</a>. Made with ❤️ in Cambridge.` | ||
|
||
export function dashboardSummary(result: TestResult): string { | ||
const count = result.counts | ||
let summary = "" | ||
|
||
if (count.passed > 0) { | ||
summary += `${count.passed} passed` | ||
} | ||
if (count.failed > 0) { | ||
summary += `${summary ? ", " : ""}${count.failed} failed` | ||
} | ||
if (count.skipped > 0) { | ||
summary += `${summary ? ", " : ""}${count.skipped} skipped` | ||
} | ||
|
||
return `<img src="${dashboardUrl}?p=${count.passed}&f=${count.failed}&s=${count.skipped}" alt="${summary}">` | ||
} | ||
|
||
export function dashboardResults(result: TestResult, show: number): string { | ||
let table = "<table>" | ||
let count = 0 | ||
|
||
table += `<tr><th align="left">${statusTitle(show)}:</th></tr>` | ||
|
||
for (const suite of result.suites) { | ||
for (const testcase of suite.cases) { | ||
if (show !== 0 && (show & testcase.status) === 0) { | ||
continue | ||
} | ||
|
||
table += "<tr><td>" | ||
|
||
const icon = statusIcon(testcase.status) | ||
if (icon) { | ||
table += icon | ||
table += " " | ||
} | ||
|
||
table += escapeHTML(testcase.name || unnamedTestCase) | ||
|
||
if (testcase.description) { | ||
table += ": " | ||
table += escapeHTML(testcase.description) | ||
} | ||
|
||
if (testcase.details) { | ||
table += "<br/>\n" | ||
table += "<pre><code>" | ||
table += escapeHTML(testcase.details) | ||
table += "</code></pre>" | ||
} | ||
|
||
table += "</td></tr>\n" | ||
|
||
count++ | ||
} | ||
} | ||
|
||
table += `<tr><td><sub>${footer}</sub></td></tr>` | ||
table += "</table>" | ||
|
||
if (count === 0) { | ||
return "" | ||
} | ||
|
||
return table | ||
} | ||
|
||
function statusTitle(status: TestStatus): string { | ||
switch (status) { | ||
case TestStatus.Fail: | ||
return "Test failures" | ||
case TestStatus.Skip: | ||
return "Skipped tests" | ||
case TestStatus.Pass: | ||
return "Passing tests" | ||
default: | ||
return "Test results" | ||
} | ||
} | ||
|
||
function statusIcon(status: TestStatus): string | undefined { | ||
switch (status) { | ||
case TestStatus.Pass: | ||
return `<img src="${passIconUrl}" alt="" />` | ||
case TestStatus.Fail: | ||
return `<img src="${failIconUrl}" alt="" />` | ||
case TestStatus.Skip: | ||
return `<img src="${skipIconUrl}" alt="" />` | ||
default: | ||
return | ||
} | ||
} |
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,11 @@ | ||
const lookup: Record<string, string> = { | ||
"&": "&", | ||
'"': """, | ||
"'": "'", | ||
"<": "<", | ||
">": ">" | ||
} | ||
|
||
export default function escapeHTML(s: string): string { | ||
return s.replace(/[&"'<>]/g, c => lookup[c]) | ||
} |
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,57 @@ | ||
import { expect } from "chai" | ||
|
||
import { TestStatus, TestResult } from "../src/test_parser" | ||
import { dashboardResults } from "../src/dashboard" | ||
|
||
describe("dashboard", async () => { | ||
it("escapes HTML entities", async () => { | ||
const result: TestResult = { | ||
counts: { passed: 0, failed: 2, skipped: 0 }, | ||
suites: [ | ||
{ | ||
cases: [ | ||
{ | ||
status: TestStatus.Fail, | ||
name: "name escaped <properly>", // "<" and ">" require escaping | ||
description: "description escaped \"properly\"", // double quotes require escaping | ||
}, | ||
{ | ||
status: TestStatus.Fail, | ||
name: "another name escaped 'properly'", // single quotes require escaping | ||
description: "another description escaped & properly", // ampersand requires escaping | ||
}, | ||
{ | ||
status: TestStatus.Fail, | ||
name: "entities ' are & escaped < in > proper & order", | ||
description: "order is important in a multi-pass replacement", | ||
} | ||
] | ||
} | ||
] | ||
} | ||
const actual = dashboardResults(result, TestStatus.Fail) | ||
expect(actual).contains("name escaped <properly>") | ||
expect(actual).contains("description escaped "properly"") | ||
expect(actual).contains("another name escaped 'properly'") | ||
expect(actual).contains("another description escaped & properly") | ||
expect(actual).contains("entities ' are & escaped < in > proper & order") | ||
}) | ||
|
||
it("uses <no name> for test cases without name", async () => { | ||
const result: TestResult = { | ||
counts: { passed: 0, failed: 1, skipped: 0 }, | ||
suites: [ | ||
{ | ||
cases: [ | ||
{ | ||
status: TestStatus.Fail, | ||
// <-- no name | ||
} | ||
] | ||
} | ||
] | ||
} | ||
const actual = dashboardResults(result, TestStatus.Fail) | ||
expect(actual).contains("<no name>") | ||
}) | ||
}) |