Skip to content

Commit 963fbef

Browse files
html escape dashboard results
1 parent aa6f1b8 commit 963fbef

File tree

5 files changed

+169
-88
lines changed

5 files changed

+169
-88
lines changed

.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@
6060
"semi": "off",
6161
"@typescript-eslint/semi": ["error", "never"],
6262
"@typescript-eslint/type-annotation-spacing": "error",
63-
"@typescript-eslint/unbound-method": "error"
63+
"@typescript-eslint/unbound-method": "error",
64+
"i18n-text/no-en": "off" // allow English string literals
6465
},
6566
"env": {
6667
"node": true,

src/dashboard.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import escapeHTML from "./escape_html"
2+
import { TestResult, TestStatus } from "./test_parser"
3+
4+
const dashboardUrl = "https://svg.test-summary.com/dashboard.svg"
5+
const passIconUrl = "https://svg.test-summary.com/icon/pass.svg?s=12"
6+
const failIconUrl = "https://svg.test-summary.com/icon/fail.svg?s=12"
7+
const skipIconUrl = "https://svg.test-summary.com/icon/skip.svg?s=12"
8+
// not used: const noneIconUrl = 'https://svg.test-summary.com/icon/none.svg?s=12'
9+
10+
const unnamedTestCase = "<no name>"
11+
12+
const footer = `This test report was produced by the <a href="https://github.com/test-summary/action">test-summary action</a>.&nbsp; Made with ❤️ in Cambridge.`
13+
14+
export function dashboardSummary(result: TestResult): string {
15+
const count = result.counts
16+
let summary = ""
17+
18+
if (count.passed > 0) {
19+
summary += `${count.passed} passed`
20+
}
21+
if (count.failed > 0) {
22+
summary += `${summary ? ", " : ""}${count.failed} failed`
23+
}
24+
if (count.skipped > 0) {
25+
summary += `${summary ? ", " : ""}${count.skipped} skipped`
26+
}
27+
28+
return `<img src="${dashboardUrl}?p=${count.passed}&f=${count.failed}&s=${count.skipped}" alt="${summary}">`
29+
}
30+
31+
export function dashboardResults(result: TestResult, show: number): string {
32+
let table = "<table>"
33+
let count = 0
34+
35+
table += `<tr><th align="left">${statusTitle(show)}:</th></tr>`
36+
37+
for (const suite of result.suites) {
38+
for (const testcase of suite.cases) {
39+
if (show !== 0 && (show & testcase.status) === 0) {
40+
continue
41+
}
42+
43+
table += "<tr><td>"
44+
45+
const icon = statusIcon(testcase.status)
46+
if (icon) {
47+
table += icon
48+
table += "&nbsp; "
49+
}
50+
51+
table += escapeHTML(testcase.name || unnamedTestCase)
52+
53+
if (testcase.description) {
54+
table += ": "
55+
table += escapeHTML(testcase.description)
56+
}
57+
58+
if (testcase.details) {
59+
table += "<br/><pre><code>"
60+
table += escapeHTML(testcase.details)
61+
table += "</code></pre>"
62+
}
63+
64+
table += "</td></tr>\n"
65+
66+
count++
67+
}
68+
}
69+
70+
table += `<tr><td><sub>${footer}</sub></td></tr>`
71+
table += "</table>"
72+
73+
if (count === 0) {
74+
return ""
75+
}
76+
77+
return table
78+
}
79+
80+
function statusTitle(status: TestStatus): string {
81+
switch (status) {
82+
case TestStatus.Fail:
83+
return "Test failures"
84+
case TestStatus.Skip:
85+
return "Skipped tests"
86+
case TestStatus.Pass:
87+
return "Passing tests"
88+
default:
89+
return "Test results"
90+
}
91+
}
92+
93+
function statusIcon(status: TestStatus): string | undefined {
94+
switch (status) {
95+
case TestStatus.Pass:
96+
return `<img src="${passIconUrl}" alt="" />`
97+
case TestStatus.Fail:
98+
return `<img src="${failIconUrl}" alt="" />`
99+
case TestStatus.Skip:
100+
return `<img src="${skipIconUrl}" alt="" />`
101+
default:
102+
return
103+
}
104+
}

src/escape_html.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const lookup: Record<string, string> = {
2+
"&": "&amp;",
3+
'"': "&quot;",
4+
"'": "&apos;",
5+
"<": "&lt;",
6+
">": "&gt;"
7+
}
8+
9+
export default function escapeHTML(s: string): string {
10+
return s.replace(/[&"'<>]/g, c => lookup[c])
11+
}

src/index.ts

+1-87
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,7 @@ import * as core from "@actions/core"
44
import * as glob from "glob-promise"
55

66
import { TestResult, TestStatus, parseFile } from "./test_parser"
7-
8-
const dashboardUrl = 'https://svg.test-summary.com/dashboard.svg'
9-
const passIconUrl = 'https://svg.test-summary.com/icon/pass.svg?s=12'
10-
const failIconUrl = 'https://svg.test-summary.com/icon/fail.svg?s=12'
11-
const skipIconUrl = 'https://svg.test-summary.com/icon/skip.svg?s=12'
12-
const noneIconUrl = 'https://svg.test-summary.com/icon/none.svg?s=12'
13-
14-
const footer = `This test report was produced by the <a href="https://github.com/test-summary/action">test-summary action</a>.&nbsp; Made with ❤️ in Cambridge.`
7+
import { dashboardResults, dashboardSummary } from "./dashboard"
158

169
async function run(): Promise<void> {
1710
try {
@@ -131,83 +124,4 @@ async function run(): Promise<void> {
131124
}
132125
}
133126

134-
function dashboardSummary(result: TestResult) {
135-
const count = result.counts
136-
let summary = ""
137-
138-
if (count.passed > 0) {
139-
summary += `${count.passed} passed`
140-
}
141-
if (count.failed > 0) {
142-
summary += `${summary ? ', ' : '' }${count.failed} failed`
143-
}
144-
if (count.skipped > 0) {
145-
summary += `${summary ? ', ' : '' }${count.skipped} skipped`
146-
}
147-
148-
return `<img src="${dashboardUrl}?p=${count.passed}&f=${count.failed}&s=${count.skipped}" alt="${summary}">`
149-
}
150-
151-
function dashboardResults(result: TestResult, show: number) {
152-
let table = "<table>"
153-
let count = 0
154-
let title: string
155-
156-
if (show == TestStatus.Fail) {
157-
title = "Test failures"
158-
} else if (show === TestStatus.Skip) {
159-
title = "Skipped tests"
160-
} else if (show === TestStatus.Pass) {
161-
title = "Passing tests"
162-
} else {
163-
title = "Test results"
164-
}
165-
166-
table += `<tr><th align="left">${title}:</th></tr>`
167-
168-
for (const suite of result.suites) {
169-
for (const testcase of suite.cases) {
170-
if (show != 0 && (show & testcase.status) == 0) {
171-
continue
172-
}
173-
174-
table += "<tr><td>"
175-
176-
if (testcase.status == TestStatus.Pass) {
177-
table += `<img src="${passIconUrl}" alt="">&nbsp; `
178-
} else if (testcase.status == TestStatus.Fail) {
179-
table += `<img src="${failIconUrl}" alt="">&nbsp; `
180-
} else if (testcase.status == TestStatus.Skip) {
181-
table += `<img src="${skipIconUrl}" alt="">&nbsp; `
182-
}
183-
184-
table += testcase.name
185-
186-
if (testcase.description) {
187-
table += ": "
188-
table += testcase.description
189-
}
190-
191-
if (testcase.details) {
192-
table += "<br/><pre><code>"
193-
table += testcase.details
194-
table += "</code></pre>"
195-
}
196-
197-
table += "</td></tr>\n"
198-
199-
count++
200-
}
201-
}
202-
203-
table += `<tr><td><sub>${footer}</sub></td></tr>`
204-
table += "</table>"
205-
206-
if (count == 0) {
207-
return ""
208-
}
209-
210-
return table
211-
}
212-
213127
run()

test/dashboard.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { expect } from "chai"
2+
3+
import { TestStatus, TestResult } from "../src/test_parser"
4+
import { dashboardResults } from "../src/dashboard"
5+
6+
describe("dashboard", async () => {
7+
it("escapes HTML entities", async () => {
8+
const result: TestResult = {
9+
counts: { passed: 0, failed: 2, skipped: 0 },
10+
suites: [
11+
{
12+
cases: [
13+
{
14+
status: TestStatus.Fail,
15+
name: "name escaped <properly>", // "<" and ">" require escaping
16+
description: "description escaped \"properly\"", // double quotes require escaping
17+
},
18+
{
19+
status: TestStatus.Fail,
20+
name: "another name escaped 'properly'", // single quotes require escaping
21+
description: "another description escaped & properly", // ampersand requires escaping
22+
}
23+
]
24+
}
25+
]
26+
}
27+
const actual = dashboardResults(result, TestStatus.Fail)
28+
expect(actual).contains("name escaped &lt;properly&gt;")
29+
expect(actual).contains("description escaped &quot;properly&quot;")
30+
expect(actual).contains("another name escaped &apos;properly&apos;")
31+
expect(actual).contains("another description escaped &amp; properly")
32+
})
33+
34+
it("uses <no name> for test cases without name", async () => {
35+
const result: TestResult = {
36+
counts: { passed: 0, failed: 1, skipped: 0 },
37+
suites: [
38+
{
39+
cases: [
40+
{
41+
status: TestStatus.Fail,
42+
// <-- no name
43+
}
44+
]
45+
}
46+
]
47+
}
48+
const actual = dashboardResults(result, TestStatus.Fail)
49+
expect(actual).contains("&lt;no name&gt;")
50+
})
51+
})

0 commit comments

Comments
 (0)