Tweak report

This commit is contained in:
Patrik Husfloen 2023-06-09 12:53:26 +02:00
parent e1bf15b60e
commit c032c9b993
4 changed files with 222 additions and 64 deletions

View file

@ -70,6 +70,13 @@ inputs:
Detailed listing of test suites and test cases will be skipped. Detailed listing of test suites and test cases will be skipped.
default: 'false' default: 'false'
required: false required: false
output-to:
description: |
The location to write the report to. Supported options:
- checks
- step-summary
default: 'step-summary'
required: false
token: token:
description: GitHub Access Token description: GitHub Access Token
required: false required: false

105
dist/index.js generated vendored
View file

@ -256,6 +256,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const github = __importStar(__nccwpck_require__(5438)); const github = __importStar(__nccwpck_require__(5438));
const crypto_1 = __nccwpck_require__(6113);
const artifact_provider_1 = __nccwpck_require__(7171); const artifact_provider_1 = __nccwpck_require__(7171);
const local_file_provider_1 = __nccwpck_require__(9399); const local_file_provider_1 = __nccwpck_require__(9399);
const get_annotations_1 = __nccwpck_require__(5867); const get_annotations_1 = __nccwpck_require__(5867);
@ -267,7 +268,6 @@ const jest_junit_parser_1 = __nccwpck_require__(1113);
const mocha_json_parser_1 = __nccwpck_require__(6043); const mocha_json_parser_1 = __nccwpck_require__(6043);
const path_utils_1 = __nccwpck_require__(4070); const path_utils_1 = __nccwpck_require__(4070);
const github_utils_1 = __nccwpck_require__(3522); const github_utils_1 = __nccwpck_require__(3522);
const markdown_utils_1 = __nccwpck_require__(6482);
function main() { function main() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
@ -282,6 +282,15 @@ function main() {
} }
}); });
} }
function createSlugPrefix() {
const step_summary = process.env['GITHUB_STEP_SUMMARY'];
if (!step_summary || step_summary === '') {
return '';
}
const hash = (0, crypto_1.createHash)('sha1');
hash.update(step_summary);
return hash.digest('hex').substring(0, 8);
}
class TestReporter { class TestReporter {
constructor() { constructor() {
this.artifact = core.getInput('artifact', { required: false }); this.artifact = core.getInput('artifact', { required: false });
@ -296,7 +305,9 @@ class TestReporter {
this.workDirInput = core.getInput('working-directory', { required: false }); this.workDirInput = core.getInput('working-directory', { required: false });
this.buildDirInput = core.getInput('build-directory', { required: false }); this.buildDirInput = core.getInput('build-directory', { required: false });
this.onlySummary = core.getInput('only-summary', { required: false }) === 'true'; this.onlySummary = core.getInput('only-summary', { required: false }) === 'true';
this.outputTo = core.getInput('output-to', { required: false });
this.token = core.getInput('token', { required: true }); this.token = core.getInput('token', { required: true });
this.slugPrefix = '';
this.context = (0, github_utils_1.getCheckRunContext)(); this.context = (0, github_utils_1.getCheckRunContext)();
this.octokit = github.getOctokit(this.token); this.octokit = github.getOctokit(this.token);
if (this.listSuites !== 'all' && this.listSuites !== 'failed') { if (this.listSuites !== 'all' && this.listSuites !== 'failed') {
@ -311,6 +322,13 @@ class TestReporter {
core.setFailed(`Input parameter 'max-annotations' has invalid value`); core.setFailed(`Input parameter 'max-annotations' has invalid value`);
return; return;
} }
if (this.outputTo !== 'checks' && this.outputTo !== 'step-summary') {
core.setFailed(`Input parameter 'output-to' has invalid value`);
return;
}
if (this.outputTo === 'step-summary') {
this.slugPrefix = createSlugPrefix();
}
} }
run() { run() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
@ -376,6 +394,7 @@ class TestReporter {
}); });
} }
createReport(parser, name, files) { createReport(parser, name, files) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
if (files.length === 0) { if (files.length === 0) {
core.warning(`No file matches path ${this.path}`); core.warning(`No file matches path ${this.path}`);
@ -393,29 +412,82 @@ class TestReporter {
throw error; throw error;
} }
} }
let createResp = null;
let baseUrl = '';
let check_run_id = 0;
switch (this.outputTo) {
case 'checks': {
core.info(`Creating check run ${name}`); core.info(`Creating check run ${name}`);
const createResp = yield this.octokit.rest.checks.create(Object.assign({ head_sha: this.context.sha, name, status: 'in_progress', output: { createResp = yield this.octokit.rest.checks.create(Object.assign({ head_sha: this.context.sha, name, status: 'in_progress', output: {
title: name, title: name,
summary: '' summary: ''
} }, github.context.repo)); } }, github.context.repo));
baseUrl = (_a = createResp.data.html_url) !== null && _a !== void 0 ? _a : '';
check_run_id = createResp.data.id;
break;
}
case 'step-summary': {
const run_attempt = (_b = process.env['GITHUB_RUN_ATTEMPT']) !== null && _b !== void 0 ? _b : 1;
baseUrl = `https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/actions/runs/${github.context.runId}/attempts/${run_attempt}`;
break;
}
}
core.info('Creating report summary'); core.info('Creating report summary');
const { listSuites, listTests, onlySummary } = this; const { listSuites, listTests, onlySummary, slugPrefix } = this;
const baseUrl = createResp.data.html_url; const summary = (0, get_report_1.getReport)(results, { listSuites, listTests, baseUrl, slugPrefix, onlySummary });
const summary = (0, get_report_1.getReport)(results, { listSuites, listTests, baseUrl, onlySummary });
core.info('Creating annotations'); core.info('Creating annotations');
const annotations = (0, get_annotations_1.getAnnotations)(results, this.maxAnnotations); const annotations = (0, get_annotations_1.getAnnotations)(results, this.maxAnnotations);
const isFailed = this.failOnError && results.some(tr => tr.result === 'failed'); const isFailed = this.failOnError && results.some(tr => tr.result === 'failed');
const conclusion = isFailed ? 'failure' : 'success'; const conclusion = isFailed ? 'failure' : 'success';
const icon = isFailed ? markdown_utils_1.Icon.fail : markdown_utils_1.Icon.success; const passed = results.reduce((sum, tr) => sum + tr.passed, 0);
const failed = results.reduce((sum, tr) => sum + tr.failed, 0);
const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0);
const shortSummary = `${passed} passed, ${failed} failed and ${skipped} skipped `;
core.info(`Updating check run conclusion (${conclusion}) and output`); core.info(`Updating check run conclusion (${conclusion}) and output`);
const resp = yield this.octokit.rest.checks.update(Object.assign({ check_run_id: createResp.data.id, conclusion, status: 'completed', output: { switch (this.outputTo) {
title: `${name} ${icon}`, case 'checks': {
const resp = yield this.octokit.rest.checks.update(Object.assign({ check_run_id,
conclusion, status: 'completed', output: {
title: shortSummary,
summary, summary,
annotations annotations
} }, github.context.repo)); } }, github.context.repo));
core.info(`Check run create response: ${resp.status}`); core.info(`Check run create response: ${resp.status}`);
core.info(`Check run URL: ${resp.data.url}`); core.info(`Check run URL: ${resp.data.url}`);
core.info(`Check run HTML: ${resp.data.html_url}`); core.info(`Check run HTML: ${resp.data.html_url}`);
break;
}
case 'step-summary': {
core.summary.addRaw(`# ${shortSummary}`);
core.summary.addRaw(summary);
yield core.summary.write();
for (const annotation of annotations) {
let fn;
switch (annotation.annotation_level) {
case 'failure':
fn = core.error;
break;
case 'warning':
fn = core.warning;
break;
case 'notice':
fn = core.notice;
break;
default:
continue;
}
fn(annotation.message, {
title: annotation.title,
file: annotation.path,
startLine: annotation.start_line,
endLine: annotation.end_line,
startColumn: annotation.start_column,
endColumn: annotation.end_column
});
}
break;
}
}
return results; return results;
}); });
} }
@ -1526,6 +1598,7 @@ const MAX_REPORT_LENGTH = 65535;
const defaultOptions = { const defaultOptions = {
listSuites: 'all', listSuites: 'all',
listTests: 'all', listTests: 'all',
slugPrefix: '',
baseUrl: '', baseUrl: '',
onlySummary: false onlySummary: false
}; };
@ -1629,7 +1702,7 @@ function getTestRunsReport(testRuns, options) {
const tableData = testRuns.map((tr, runIndex) => { const tableData = testRuns.map((tr, runIndex) => {
const time = (0, markdown_utils_1.formatTime)(tr.time); const time = (0, markdown_utils_1.formatTime)(tr.time);
const name = tr.path; const name = tr.path;
const addr = options.baseUrl + makeRunSlug(runIndex).link; const addr = options.baseUrl + makeRunSlug(runIndex, options.slugPrefix).link;
const nameLink = (0, markdown_utils_1.link)(name, addr); const nameLink = (0, markdown_utils_1.link)(name, addr);
const statusIcon = tr.failed > 0 ? markdown_utils_1.Icon.fail : tr.passed > 0 ? markdown_utils_1.Icon.success : markdown_utils_1.Icon.skip; const statusIcon = tr.failed > 0 ? markdown_utils_1.Icon.fail : tr.passed > 0 ? markdown_utils_1.Icon.success : markdown_utils_1.Icon.skip;
const passed = tr.passed === 0 ? '' : tr.passed; const passed = tr.passed === 0 ? '' : tr.passed;
@ -1648,7 +1721,7 @@ function getTestRunsReport(testRuns, options) {
} }
function getSuitesReport(tr, runIndex, options) { function getSuitesReport(tr, runIndex, options) {
const sections = []; const sections = [];
const trSlug = makeRunSlug(runIndex); const trSlug = makeRunSlug(runIndex, options.slugPrefix);
const nameLink = `<a id="${trSlug.id}" href="${options.baseUrl + trSlug.link}">${tr.path}</a>`; const nameLink = `<a id="${trSlug.id}" href="${options.baseUrl + trSlug.link}">${tr.path}</a>`;
const icon = getResultIcon(tr.result); const icon = getResultIcon(tr.result);
sections.push(`## ${icon}\xa0${nameLink}`); sections.push(`## ${icon}\xa0${nameLink}`);
@ -1663,7 +1736,7 @@ function getSuitesReport(tr, runIndex, options) {
const tsTime = (0, markdown_utils_1.formatTime)(s.time); const tsTime = (0, markdown_utils_1.formatTime)(s.time);
const tsName = s.name; const tsName = s.name;
const skipLink = options.listTests === 'none' || (options.listTests === 'failed' && s.result !== 'failed'); const skipLink = options.listTests === 'none' || (options.listTests === 'failed' && s.result !== 'failed');
const tsAddr = options.baseUrl + makeSuiteSlug(runIndex, suiteIndex).link; const tsAddr = options.baseUrl + makeSuiteSlug(runIndex, suiteIndex, options.slugPrefix).link;
const tsNameLink = skipLink ? tsName : (0, markdown_utils_1.link)(tsName, tsAddr); const tsNameLink = skipLink ? tsName : (0, markdown_utils_1.link)(tsName, tsAddr);
const statusIcon = s.failed > 0 ? markdown_utils_1.Icon.fail : s.passed > 0 ? markdown_utils_1.Icon.success : markdown_utils_1.Icon.skip; const statusIcon = s.failed > 0 ? markdown_utils_1.Icon.fail : s.passed > 0 ? markdown_utils_1.Icon.success : markdown_utils_1.Icon.skip;
const passed = s.passed === 0 ? '' : s.passed; const passed = s.passed === 0 ? '' : s.passed;
@ -1692,7 +1765,7 @@ function getTestsReport(ts, runIndex, suiteIndex, options) {
} }
const sections = []; const sections = [];
const tsName = ts.name; const tsName = ts.name;
const tsSlug = makeSuiteSlug(runIndex, suiteIndex); const tsSlug = makeSuiteSlug(runIndex, suiteIndex, options.slugPrefix);
const tsNameLink = `<a id="${tsSlug.id}" href="${options.baseUrl + tsSlug.link}">${tsName}</a>`; const tsNameLink = `<a id="${tsSlug.id}" href="${options.baseUrl + tsSlug.link}">${tsName}</a>`;
const icon = getResultIcon(ts.result); const icon = getResultIcon(ts.result);
sections.push(`### ${icon}\xa0${tsNameLink}`); sections.push(`### ${icon}\xa0${tsNameLink}`);
@ -1716,13 +1789,13 @@ function getTestsReport(ts, runIndex, suiteIndex, options) {
sections.push('```'); sections.push('```');
return sections; return sections;
} }
function makeRunSlug(runIndex) { function makeRunSlug(runIndex, slugPrefix) {
// use prefix to avoid slug conflicts after escaping the paths // use prefix to avoid slug conflicts after escaping the paths
return (0, slugger_1.slug)(`r${runIndex}`); return (0, slugger_1.slug)(`r${slugPrefix}${runIndex}`);
} }
function makeSuiteSlug(runIndex, suiteIndex) { function makeSuiteSlug(runIndex, suiteIndex, slugPrefix) {
// use prefix to avoid slug conflicts after escaping the paths // use prefix to avoid slug conflicts after escaping the paths
return (0, slugger_1.slug)(`r${runIndex}s${suiteIndex}`); return (0, slugger_1.slug)(`r${slugPrefix}${runIndex}s${suiteIndex}`);
} }
function getResultIcon(result) { function getResultIcon(result) {
switch (result) { switch (result) {

View file

@ -1,5 +1,6 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import {createHash} from 'crypto'
import {GitHub} from '@actions/github/lib/utils' import {GitHub} from '@actions/github/lib/utils'
import {ArtifactProvider} from './input-providers/artifact-provider' import {ArtifactProvider} from './input-providers/artifact-provider'
@ -18,7 +19,6 @@ import {MochaJsonParser} from './parsers/mocha-json/mocha-json-parser'
import {normalizeDirPath, normalizeFilePath} from './utils/path-utils' import {normalizeDirPath, normalizeFilePath} from './utils/path-utils'
import {getCheckRunContext} from './utils/github-utils' import {getCheckRunContext} from './utils/github-utils'
import {Icon} from './utils/markdown-utils'
async function main(): Promise<void> { async function main(): Promise<void> {
try { try {
@ -30,6 +30,16 @@ async function main(): Promise<void> {
} }
} }
function createSlugPrefix(): string {
const step_summary = process.env['GITHUB_STEP_SUMMARY']
if (!step_summary || step_summary === '') {
return ''
}
const hash = createHash('sha1')
hash.update(step_summary)
return hash.digest('hex').substring(0, 8)
}
class TestReporter { class TestReporter {
readonly artifact = core.getInput('artifact', {required: false}) readonly artifact = core.getInput('artifact', {required: false})
readonly name = core.getInput('name', {required: true}) readonly name = core.getInput('name', {required: true})
@ -43,7 +53,9 @@ class TestReporter {
readonly workDirInput = core.getInput('working-directory', {required: false}) readonly workDirInput = core.getInput('working-directory', {required: false})
readonly buildDirInput = core.getInput('build-directory', {required: false}) readonly buildDirInput = core.getInput('build-directory', {required: false})
readonly onlySummary = core.getInput('only-summary', {required: false}) === 'true' readonly onlySummary = core.getInput('only-summary', {required: false}) === 'true'
readonly outputTo = core.getInput('output-to', {required: false})
readonly token = core.getInput('token', {required: true}) readonly token = core.getInput('token', {required: true})
readonly slugPrefix: string = ''
readonly octokit: InstanceType<typeof GitHub> readonly octokit: InstanceType<typeof GitHub>
readonly context = getCheckRunContext() readonly context = getCheckRunContext()
@ -64,6 +76,15 @@ class TestReporter {
core.setFailed(`Input parameter 'max-annotations' has invalid value`) core.setFailed(`Input parameter 'max-annotations' has invalid value`)
return return
} }
if (this.outputTo !== 'checks' && this.outputTo !== 'step-summary') {
core.setFailed(`Input parameter 'output-to' has invalid value`)
return
}
if (this.outputTo === 'step-summary') {
this.slugPrefix = createSlugPrefix()
}
} }
async run(): Promise<void> { async run(): Promise<void> {
@ -164,8 +185,14 @@ class TestReporter {
} }
} }
let createResp = null
let baseUrl = ''
let check_run_id = 0
switch (this.outputTo) {
case 'checks': {
core.info(`Creating check run ${name}`) core.info(`Creating check run ${name}`)
const createResp = await this.octokit.rest.checks.create({ createResp = await this.octokit.rest.checks.create({
head_sha: this.context.sha, head_sha: this.context.sha,
name, name,
status: 'in_progress', status: 'in_progress',
@ -175,26 +202,41 @@ class TestReporter {
}, },
...github.context.repo ...github.context.repo
}) })
baseUrl = createResp.data.html_url ?? ''
check_run_id = createResp.data.id
break
}
case 'step-summary': {
const run_attempt = process.env['GITHUB_RUN_ATTEMPT'] ?? 1
baseUrl = `https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/actions/runs/${github.context.runId}/attempts/${run_attempt}`
break
}
}
core.info('Creating report summary') core.info('Creating report summary')
const {listSuites, listTests, onlySummary} = this const {listSuites, listTests, onlySummary, slugPrefix} = this
const baseUrl = createResp.data.html_url as string const summary = getReport(results, {listSuites, listTests, baseUrl, slugPrefix, onlySummary})
const summary = getReport(results, {listSuites, listTests, baseUrl, onlySummary})
core.info('Creating annotations') core.info('Creating annotations')
const annotations = getAnnotations(results, this.maxAnnotations) const annotations = getAnnotations(results, this.maxAnnotations)
const isFailed = this.failOnError && results.some(tr => tr.result === 'failed') const isFailed = this.failOnError && results.some(tr => tr.result === 'failed')
const conclusion = isFailed ? 'failure' : 'success' const conclusion = isFailed ? 'failure' : 'success'
const icon = isFailed ? Icon.fail : Icon.success
const passed = results.reduce((sum, tr) => sum + tr.passed, 0)
const failed = results.reduce((sum, tr) => sum + tr.failed, 0)
const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0)
const shortSummary = `${passed} passed, ${failed} failed and ${skipped} skipped `
core.info(`Updating check run conclusion (${conclusion}) and output`) core.info(`Updating check run conclusion (${conclusion}) and output`)
switch (this.outputTo) {
case 'checks': {
const resp = await this.octokit.rest.checks.update({ const resp = await this.octokit.rest.checks.update({
check_run_id: createResp.data.id, check_run_id,
conclusion, conclusion,
status: 'completed', status: 'completed',
output: { output: {
title: `${name} ${icon}`, title: shortSummary,
summary, summary,
annotations annotations
}, },
@ -203,6 +245,40 @@ class TestReporter {
core.info(`Check run create response: ${resp.status}`) core.info(`Check run create response: ${resp.status}`)
core.info(`Check run URL: ${resp.data.url}`) core.info(`Check run URL: ${resp.data.url}`)
core.info(`Check run HTML: ${resp.data.html_url}`) core.info(`Check run HTML: ${resp.data.html_url}`)
break
}
case 'step-summary': {
core.summary.addRaw(`# ${shortSummary}`)
core.summary.addRaw(summary)
await core.summary.write()
for (const annotation of annotations) {
let fn
switch (annotation.annotation_level) {
case 'failure':
fn = core.error
break
case 'warning':
fn = core.warning
break
case 'notice':
fn = core.notice
break
default:
continue
}
fn(annotation.message, {
title: annotation.title,
file: annotation.path,
startLine: annotation.start_line,
endLine: annotation.end_line,
startColumn: annotation.start_column,
endColumn: annotation.end_column
})
}
break
}
}
return results return results
} }

View file

@ -10,6 +10,7 @@ const MAX_REPORT_LENGTH = 65535
export interface ReportOptions { export interface ReportOptions {
listSuites: 'all' | 'failed' listSuites: 'all' | 'failed'
listTests: 'all' | 'failed' | 'none' listTests: 'all' | 'failed' | 'none'
slugPrefix: string
baseUrl: string baseUrl: string
onlySummary: boolean onlySummary: boolean
} }
@ -17,6 +18,7 @@ export interface ReportOptions {
const defaultOptions: ReportOptions = { const defaultOptions: ReportOptions = {
listSuites: 'all', listSuites: 'all',
listTests: 'all', listTests: 'all',
slugPrefix: '',
baseUrl: '', baseUrl: '',
onlySummary: false onlySummary: false
} }
@ -139,7 +141,7 @@ function getTestRunsReport(testRuns: TestRunResult[], options: ReportOptions): s
const tableData = testRuns.map((tr, runIndex) => { const tableData = testRuns.map((tr, runIndex) => {
const time = formatTime(tr.time) const time = formatTime(tr.time)
const name = tr.path const name = tr.path
const addr = options.baseUrl + makeRunSlug(runIndex).link const addr = options.baseUrl + makeRunSlug(runIndex, options.slugPrefix).link
const nameLink = link(name, addr) const nameLink = link(name, addr)
const statusIcon = tr.failed > 0 ? Icon.fail : tr.passed > 0 ? Icon.success : Icon.skip const statusIcon = tr.failed > 0 ? Icon.fail : tr.passed > 0 ? Icon.success : Icon.skip
const passed = tr.passed === 0 ? '' : tr.passed const passed = tr.passed === 0 ? '' : tr.passed
@ -166,7 +168,7 @@ function getTestRunsReport(testRuns: TestRunResult[], options: ReportOptions): s
function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOptions): string[] { function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOptions): string[] {
const sections: string[] = [] const sections: string[] = []
const trSlug = makeRunSlug(runIndex) const trSlug = makeRunSlug(runIndex, options.slugPrefix)
const nameLink = `<a id="${trSlug.id}" href="${options.baseUrl + trSlug.link}">${tr.path}</a>` const nameLink = `<a id="${trSlug.id}" href="${options.baseUrl + trSlug.link}">${tr.path}</a>`
const icon = getResultIcon(tr.result) const icon = getResultIcon(tr.result)
sections.push(`## ${icon}\xa0${nameLink}`) sections.push(`## ${icon}\xa0${nameLink}`)
@ -187,7 +189,7 @@ function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOpt
const tsTime = formatTime(s.time) const tsTime = formatTime(s.time)
const tsName = s.name const tsName = s.name
const skipLink = options.listTests === 'none' || (options.listTests === 'failed' && s.result !== 'failed') const skipLink = options.listTests === 'none' || (options.listTests === 'failed' && s.result !== 'failed')
const tsAddr = options.baseUrl + makeSuiteSlug(runIndex, suiteIndex).link const tsAddr = options.baseUrl + makeSuiteSlug(runIndex, suiteIndex, options.slugPrefix).link
const tsNameLink = skipLink ? tsName : link(tsName, tsAddr) const tsNameLink = skipLink ? tsName : link(tsName, tsAddr)
const statusIcon = s.failed > 0 ? Icon.fail : s.passed > 0 ? Icon.success : Icon.skip const statusIcon = s.failed > 0 ? Icon.fail : s.passed > 0 ? Icon.success : Icon.skip
const passed = s.passed === 0 ? '' : s.passed const passed = s.passed === 0 ? '' : s.passed
@ -222,7 +224,7 @@ function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: numbe
const sections: string[] = [] const sections: string[] = []
const tsName = ts.name const tsName = ts.name
const tsSlug = makeSuiteSlug(runIndex, suiteIndex) const tsSlug = makeSuiteSlug(runIndex, suiteIndex, options.slugPrefix)
const tsNameLink = `<a id="${tsSlug.id}" href="${options.baseUrl + tsSlug.link}">${tsName}</a>` const tsNameLink = `<a id="${tsSlug.id}" href="${options.baseUrl + tsSlug.link}">${tsName}</a>`
const icon = getResultIcon(ts.result) const icon = getResultIcon(ts.result)
sections.push(`### ${icon}\xa0${tsNameLink}`) sections.push(`### ${icon}\xa0${tsNameLink}`)
@ -251,14 +253,14 @@ function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: numbe
return sections return sections
} }
function makeRunSlug(runIndex: number): {id: string; link: string} { function makeRunSlug(runIndex: number, slugPrefix: string): {id: string; link: string} {
// use prefix to avoid slug conflicts after escaping the paths // use prefix to avoid slug conflicts after escaping the paths
return slug(`r${runIndex}`) return slug(`r${slugPrefix}${runIndex}`)
} }
function makeSuiteSlug(runIndex: number, suiteIndex: number): {id: string; link: string} { function makeSuiteSlug(runIndex: number, suiteIndex: number, slugPrefix: string): {id: string; link: string} {
// use prefix to avoid slug conflicts after escaping the paths // use prefix to avoid slug conflicts after escaping the paths
return slug(`r${runIndex}s${suiteIndex}`) return slug(`r${slugPrefix}${runIndex}s${suiteIndex}`)
} }
function getResultIcon(result: TestExecutionResult): string { function getResultIcon(result: TestExecutionResult): string {