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 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
5 changes: 5 additions & 0 deletions e2e/__tests__/__snapshots__/annotationExample.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

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

exports[`passing snapshot example 1`] = `"some thing"`;
40 changes: 40 additions & 0 deletions e2e/__tests__/annotationExample.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
test.todo('work in progress example');

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

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

let i = 0;

jest.retryTimes(3, {logErrorsBeforeRetry: true});

test('retryTimes example', () => {
i++;
if (i === 3) {
expect(true).toBeTruthy();
} else {
expect(true).toBeFalsy();
}
});

test('failing snapshot example', () => {
expect('nothing').toMatchSnapshot();
});

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

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

describe('nested', () => {
test('failing example', () => {
expect(abc).toBe(1);
});
});
1 change: 1 addition & 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 Down
93 changes: 92 additions & 1 deletion packages/jest-reporters/src/GitHubActionsReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@
* LICENSE file in the root directory of this source tree.
*/

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

type AnnotationOptions = {
Expand Down Expand Up @@ -53,6 +60,90 @@ export default class GitHubActionsReporter extends BaseReporter {
});
}

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

#getMessageDetails(failureMessage: string, config: Config.ProjectConfig) {
const {message, stack} = separateMessageFromStack(failureMessage);

Expand Down
125 changes: 124 additions & 1 deletion packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,34 @@
* LICENSE file in the root directory of this source tree.
*/

import type {Test, TestCaseResult, TestResult} from '@jest/test-result';
import {summary as actionsSummary} from '@actions/core';
import type {
AggregatedResult,
Test,
TestCaseResult,
TestContext,
TestResult,
} from '@jest/test-result';
import GitHubActionsReporter from '../GitHubActionsReporter';

process.stderr.write = jest.fn();

jest.spyOn(Date, 'now').mockReturnValue(6000);

jest.mock('@actions/core', () => {
const addRaw = jest.fn(() => summary);
const addTable = jest.fn(() => summary);
const write = jest.fn(() => summary);

const summary = {
addRaw,
addTable,
write,
} as unknown as jest.Mocked<typeof actionsSummary>;

return {summary};
});

afterEach(() => {
jest.clearAllMocks();
});
Expand Down Expand Up @@ -70,6 +93,8 @@ const testCaseResult = {
title: 'example test',
} as TestCaseResult;

const testContexts = new Set<TestContext>();

describe('logs error annotation', () => {
test('when an expectation fails to pass', () => {
reporter.onTestFileResult(testMeta, {
Expand Down Expand Up @@ -133,3 +158,101 @@ describe('logs warning annotation before logging errors', () => {
expect(jest.mocked(process.stderr.write).mock.calls).toMatchSnapshot();
});
});

describe("passes summary report to '@actions/core'", () => {
test('when zero tests were ran', async () => {
await reporter.onRunComplete(testContexts, {
numTotalTests: 0,
} as AggregatedResult);

expect(actionsSummary.write).not.toBeCalled();
});

test('when just one test was ran', async () => {
await reporter.onRunComplete(testContexts, {
numPassedTests: 1,
numTotalTestSuites: 0,
numTotalTests: 1,
snapshot: {total: 0},
startTime: 4500,
} as AggregatedResult);

expect(jest.mocked(actionsSummary.addRaw).mock.calls).toMatchSnapshot();
expect(jest.mocked(actionsSummary.addTable).mock.calls).toMatchSnapshot();
expect(actionsSummary.write).toBeCalledTimes(1);
});

test('when more than one test was ran', async () => {
await reporter.onRunComplete(testContexts, {
numFailedTests: 32,
numPassedTests: 436,
numPendingTests: 54,
numTodoTests: 7,
numTotalTestSuites: 0,
numTotalTests: 529,
snapshot: {total: 0},
startTime: 1200,
} as AggregatedResult);

expect(jest.mocked(actionsSummary.addRaw).mock.calls).toMatchSnapshot();
expect(jest.mocked(actionsSummary.addTable).mock.calls).toMatchSnapshot();
expect(actionsSummary.write).toBeCalledTimes(1);
});

test('when just one test suite was ran', async () => {
await reporter.onRunComplete(testContexts, {
numPassedTestSuites: 1,
numPendingTests: 2,
numTotalTestSuites: 1,
numTotalTests: 12,
snapshot: {total: 0},
startTime: 4500,
} as AggregatedResult);

expect(jest.mocked(actionsSummary.addRaw).mock.calls).toMatchSnapshot();
expect(jest.mocked(actionsSummary.addTable).mock.calls).toMatchSnapshot();
expect(actionsSummary.write).toBeCalledTimes(1);
});

test('when more than one test suite was ran', async () => {
await reporter.onRunComplete(testContexts, {
numFailedTestSuites: 5,
numPassedTestSuites: 24,
numPendingTestSuites: 13,
numTotalTestSuites: 42,
numTotalTests: 522,
snapshot: {total: 0},
startTime: 1200,
} as AggregatedResult);

expect(jest.mocked(actionsSummary.addRaw).mock.calls).toMatchSnapshot();
expect(jest.mocked(actionsSummary.addTable).mock.calls).toMatchSnapshot();
expect(actionsSummary.write).toBeCalledTimes(1);
});

test('when just one snapshot was matched', async () => {
await reporter.onRunComplete(testContexts, {
numTotalTestSuites: 0,
numTotalTests: 12,
snapshot: {matched: 1, total: 1},
startTime: 3800,
} as AggregatedResult);

expect(jest.mocked(actionsSummary.addRaw).mock.calls).toMatchSnapshot();
expect(jest.mocked(actionsSummary.addTable).mock.calls).toMatchSnapshot();
expect(actionsSummary.write).toBeCalledTimes(1);
});

test('when more than one snapshot was matched', async () => {
await reporter.onRunComplete(testContexts, {
numTotalTestSuites: 42,
numTotalTests: 529,
snapshot: {matched: 72, total: 85, unchecked: 1, unmatched: 12},
startTime: 200,
} as AggregatedResult);

expect(jest.mocked(actionsSummary.addRaw).mock.calls).toMatchSnapshot();
expect(jest.mocked(actionsSummary.addTable).mock.calls).toMatchSnapshot();
expect(actionsSummary.write).toBeCalledTimes(1);
});
});
Loading