Skip to content

Commit

Permalink
Merge branch 'pr/26' (HTML escaping of dashboard results)
Browse files Browse the repository at this point in the history
  • Loading branch information
ethomson committed Dec 12, 2023
2 parents 2d28437 + 2312788 commit 3e6e4b0
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 88 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
"semi": "off",
"@typescript-eslint/semi": ["error", "never"],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error"
"@typescript-eslint/unbound-method": "error",
"i18n-text/no-en": "off" // allow English string literals
},
"env": {
"node": true,
Expand Down
105 changes: 105 additions & 0 deletions src/dashboard.ts
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>.&nbsp; 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 += "&nbsp; "
}

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
}
}
11 changes: 11 additions & 0 deletions src/escape_html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const lookup: Record<string, string> = {
"&": "&amp;",
'"': "&quot;",
"'": "&apos;",
"<": "&lt;",
">": "&gt;"
}

export default function escapeHTML(s: string): string {
return s.replace(/[&"'<>]/g, c => lookup[c])
}
88 changes: 1 addition & 87 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,7 @@ import * as core from "@actions/core"
import * as glob from "glob-promise"

import { TestResult, TestStatus, parseFile } 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'
const noneIconUrl = 'https://svg.test-summary.com/icon/none.svg?s=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.`
import { dashboardResults, dashboardSummary } from "./dashboard"

async function run(): Promise<void> {
try {
Expand Down Expand Up @@ -131,83 +124,4 @@ async function run(): Promise<void> {
}
}

function dashboardSummary(result: TestResult) {
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}">`
}

function dashboardResults(result: TestResult, show: number) {
let table = "<table>"
let count = 0
let title: string

if (show == TestStatus.Fail) {
title = "Test failures"
} else if (show === TestStatus.Skip) {
title = "Skipped tests"
} else if (show === TestStatus.Pass) {
title = "Passing tests"
} else {
title = "Test results"
}

table += `<tr><th align="left">${title}:</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>"

if (testcase.status == TestStatus.Pass) {
table += `<img src="${passIconUrl}" alt="">&nbsp; `
} else if (testcase.status == TestStatus.Fail) {
table += `<img src="${failIconUrl}" alt="">&nbsp; `
} else if (testcase.status == TestStatus.Skip) {
table += `<img src="${skipIconUrl}" alt="">&nbsp; `
}

table += testcase.name

if (testcase.description) {
table += ": "
table += testcase.description
}

if (testcase.details) {
table += "<br/><pre><code>"
table += 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
}

run()
57 changes: 57 additions & 0 deletions test/dashboard.ts
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 &lt;properly&gt;")
expect(actual).contains("description escaped &quot;properly&quot;")
expect(actual).contains("another name escaped &apos;properly&apos;")
expect(actual).contains("another description escaped &amp; properly")
expect(actual).contains("entities &apos; are &amp; escaped &lt; in &gt; proper &amp; 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("&lt;no name&gt;")
})
})

0 comments on commit 3e6e4b0

Please sign in to comment.