Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(@jest/reporters): GitHubActionsReporter version 2 #12822

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions e2e/quick/__snapshots__/quick.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`quick failing snapshot test 1`] = `"some thing"`;

exports[`quick passing snapshot test 1`] = `"some thing"`;

exports[`quick snapshot test 1`] = `"some thing"`;
9 changes: 9 additions & 0 deletions e2e/quick/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "quick",
"jest": {
"reporters": [
"github-actions",
"summary"
]
}
}
27 changes: 27 additions & 0 deletions e2e/quick/quick.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
test.todo('one the way');

test('quick passing test', () => {
expect(10).toBe(10);
});

test('quick passing snapshot test', () => {
expect('some thing').toMatchSnapshot();
});

test('quick failing snapshot test', () => {
expect('another thing').toMatchSnapshot();
});

test.skip('quick skipped test', () => {
expect(10).toBe(10);
});

test('quick failing test', () => {
expect(10).toBe(1);
});

describe('nested', () => {
test('failing test', () => {
expect(abc).toBe(1);
});
});
2 changes: 2 additions & 0 deletions packages/jest-reporters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@actions/core": "^1.8.0",
"@bcoe/v8-coverage": "^0.2.3",
"@jest/console": "^28.1.0",
"@jest/test-result": "^28.1.0",
Expand All @@ -29,6 +30,7 @@
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^4.0.0",
"istanbul-reports": "^3.1.3",
"jest-message-util": "^28.1.0",
"jest-util": "^28.1.0",
"jest-worker": "^28.1.0",
"slash": "^3.0.0",
Expand Down
149 changes: 113 additions & 36 deletions packages/jest-reporters/src/GitHubActionsReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,132 @@
* LICENSE file in the root directory of this source tree.
*/

import {
summary as actionsSummary,
error as errorAnnotation,
} from '@actions/core';
import stripAnsi = require('strip-ansi');
import type {
AggregatedResult,
Test,
TestCaseResult,
TestContext,
TestResult,
} from '@jest/test-result';
import {
getStackTraceLines,
getTopFrame,
separateMessageFromStack,
} from 'jest-message-util';
import {pluralize} from 'jest-util';
import BaseReporter from './BaseReporter';

const lineAndColumnInStackTrace = /^.*?:([0-9]+):([0-9]+).*$/;

function replaceEntities(s: string): string {
// https://github.com/actions/toolkit/blob/b4639928698a6bfe1c4bdae4b2bfdad1cb75016d/packages/core/src/command.ts#L80-L85
const substitutions: Array<[RegExp, string]> = [
[/%/g, '%25'],
[/\r/g, '%0D'],
[/\n/g, '%0A'],
];
return substitutions.reduce((acc, sub) => acc.replace(...sub), s);
}
const ancestrySeparator = ' \u203A ';
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

export default class GitHubActionsReporter extends BaseReporter {
static readonly filename = __filename;

override onRunComplete(
_testContexts?: Set<TestContext>,
aggregatedResults?: AggregatedResult,
override onTestCaseResult(
{path: testPath}: Test,
{failureMessages, ancestorTitles, title}: TestCaseResult,
): void {
const messages = getMessages(aggregatedResults?.testResults);
failureMessages.forEach(failureMessage => {
const {message, stack} = separateMessageFromStack(failureMessage);
const stackTraceLines = getStackTraceLines(stack);
const topFrame = getTopFrame(stackTraceLines);

for (const message of messages) {
this.log(message);
}
const errorTitle = [...ancestorTitles, title].join(ancestrySeparator);
const errorMessage = stripAnsi([message, ...stackTraceLines].join('\n'));

errorAnnotation(errorMessage, {
file: testPath,
startColumn: topFrame?.column,
startLine: topFrame?.line,
title: errorTitle,
});
});
}
}

function getMessages(results: Array<TestResult> | undefined) {
if (!results) return [];

return results.flatMap(({testFilePath, testResults}) =>
testResults
.filter(r => r.status === 'failed')
.flatMap(r => r.failureMessages)
.map(m => stripAnsi(m))
.map(m => replaceEntities(m))
.map(m => lineAndColumnInStackTrace.exec(m))
.filter((m): m is RegExpExecArray => m !== null)
.map(
([message, line, col]) =>
`\n::error file=${testFilePath},line=${line},col=${col}::${message}`,
),
);
override async onRunComplete(
_testContexts?: Set<TestContext>,
result?: AggregatedResult,
): Promise<void> {
if (!result?.numTotalTests) return;

const summary = actionsSummary.addRaw('Jest ran ');
if (result?.numTotalTestSuites) {
const suitesCount = pluralize('test suite', result.numTotalTestSuites);
summary.addRaw(`**${suitesCount}** with totally `);
}
if (result?.numTotalTests) {
const testsCount = pluralize('test', result.numTotalTests);
summary.addRaw(`**${testsCount}** `);
}
if (result?.snapshot.total) {
const snapshotsCount = pluralize('snapshot', result.snapshot.total);
summary.addRaw(`and matched **${snapshotsCount}** `);
}
if (result?.startTime) {
const runTime = (Date.now() - result.startTime) / 1000;
const duration = pluralize('second', Math.floor(runTime));
summary.addRaw(`in just ${duration}.`, true);
}

const shieldColor = {
failed: 'red',
obsolete: 'yellow',
passed: 'brightgreen',
skipped: 'yellow',
todo: 'blueviolet',
};

type GetBadgeOptions = {
status: keyof typeof shieldColor;
count?: number;
};

function getBadge({status, count = 0}: GetBadgeOptions) {
if (count === 0) return;

const src = `https://img.shields.io/badge/${status}-${count}-${shieldColor[status]}`;
const alt = `${count} ${status}`;

return `<img src="${src}" alt="${alt}">`;
}

summary.addTable([
[
{data: 'Test Suites', header: true},
{data: 'Tests', header: true},
{data: 'Snapshots', header: true},
],
[
[
getBadge({count: result?.numFailedTestSuites, status: 'failed'}),
getBadge({count: result?.numPendingTestSuites, status: 'skipped'}),
getBadge({count: result?.numPassedTestSuites, status: 'passed'}),
]
.filter(shield => shield !== undefined)
.join('<br>'),

[
getBadge({count: result?.numFailedTests, status: 'failed'}),
getBadge({count: result?.numPendingTests, status: 'skipped'}),
getBadge({count: result?.numTodoTests, status: 'todo'}),
getBadge({count: result?.numPassedTests, status: 'passed'}),
]
.filter(shield => shield !== undefined)
.join('<br>'),

[
getBadge({count: result?.snapshot.unmatched, status: 'failed'}),
getBadge({count: result?.snapshot.unchecked, status: 'obsolete'}),
getBadge({count: result?.snapshot.matched, status: 'passed'}),
]
.filter(shield => shield !== undefined)
.join('<br>'),
],
]);

await summary.write();
}
}
118 changes: 0 additions & 118 deletions packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.js

This file was deleted.

Loading