Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
104 changes: 104 additions & 0 deletions src/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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/><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()
51 changes: 51 additions & 0 deletions test/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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
}
]
}
]
}
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")
})

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;")
})
})