| undefined,
12 | threshold: number | undefined
13 | ): string =>
14 | table(
15 | [
16 | [
17 | withExplanation(i18n('status'), i18n('statusExplanation')),
18 | i18n('category'),
19 | i18n('percentage'),
20 | i18n('ratio'),
21 | ],
22 | ...headSummary.map((currSummary, index) => [
23 | getStatusOfPercents(currSummary.percentage, threshold),
24 | currSummary.title,
25 | formatPercentage(
26 | currSummary.percentage,
27 | baseSummary?.[index].percentage
28 | ),
29 | `${currSummary.covered}/${currSummary.total}`,
30 | ]),
31 | ],
32 | {
33 | align: ['c', 'l', 'l', 'c'],
34 | stringLength: () => 1,
35 | alignDelimiters: false,
36 | }
37 | );
38 |
--------------------------------------------------------------------------------
/src/format/summary/getSummary.ts:
--------------------------------------------------------------------------------
1 | import { CoverageSummary } from '../../typings/Coverage';
2 | import { CoverageMap, FileCoverage } from '../../typings/JsonReport';
3 | import { getPercents } from '../getPercents';
4 |
5 | export const getSummary = (
6 | map: CoverageMap,
7 | totalCounter: (value: FileCoverage) => number,
8 | coveredCounter: (value: FileCoverage) => number,
9 | title: string
10 | ): CoverageSummary => {
11 | const values = Object.values(map).map((value) =>
12 | 'statementMap' in value ? value : value.data
13 | );
14 |
15 | const total = values.reduce(
16 | (acc, currValue) => acc + totalCounter(currValue),
17 | 0
18 | );
19 |
20 | const covered = values.reduce(
21 | (acc, currValue) => acc + coveredCounter(currValue),
22 | 0
23 | );
24 |
25 | return {
26 | title,
27 | total,
28 | covered,
29 | percentage: getPercents(covered, total),
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/src/format/summary/parseSummary.ts:
--------------------------------------------------------------------------------
1 | import { getSummary } from './getSummary';
2 | import { JsonReport } from '../../typings/JsonReport';
3 | import { i18n } from '../../utils/i18n';
4 | import {
5 | coveredBranchesCounter,
6 | coveredLinesCounter,
7 | standardCoveredCounter,
8 | standardTotalCounter,
9 | totalBranchesCounter,
10 | totalLinesCounter,
11 | } from '../counters';
12 |
13 | export const parseSummary = (jsonReport: JsonReport) => {
14 | return [
15 | getSummary(
16 | jsonReport.coverageMap,
17 | standardTotalCounter('s'),
18 | standardCoveredCounter('s'),
19 | i18n('statements')
20 | ),
21 | getSummary(
22 | jsonReport.coverageMap,
23 | totalBranchesCounter,
24 | coveredBranchesCounter,
25 | i18n('branches')
26 | ),
27 | getSummary(
28 | jsonReport.coverageMap,
29 | standardTotalCounter('f'),
30 | standardCoveredCounter('f'),
31 | i18n('functions')
32 | ),
33 | getSummary(
34 | jsonReport.coverageMap,
35 | totalLinesCounter,
36 | coveredLinesCounter,
37 | i18n('lines')
38 | ),
39 | ];
40 | };
41 |
--------------------------------------------------------------------------------
/src/format/template.md:
--------------------------------------------------------------------------------
1 | {{ tag }}
2 |
3 | ## {{ title }}
4 |
5 | {{ body }}
6 |
7 | Report generated by 🧪jest coverage report action from {{ sha }}
8 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { run } from './run';
2 |
3 | run();
4 |
--------------------------------------------------------------------------------
/src/report/__mocks__/generateCommitReport.ts:
--------------------------------------------------------------------------------
1 | export const generateCommitReport = jest.fn();
2 |
--------------------------------------------------------------------------------
/src/report/__mocks__/generatePRReport.ts:
--------------------------------------------------------------------------------
1 | export const generatePRReport = jest.fn();
2 |
--------------------------------------------------------------------------------
/src/report/fetchPreviousReport.ts:
--------------------------------------------------------------------------------
1 | import { getOctokit } from '@actions/github';
2 |
3 | import { getReportTag } from '../constants/getReportTag';
4 | import { Options } from '../typings/Options';
5 |
6 | export async function fetchPreviousReport(
7 | octokit: ReturnType,
8 | repo: { owner: string; repo: string },
9 | pr: { number: number },
10 | options: Options
11 | ) {
12 | const commentList = await octokit.paginate(
13 | 'GET /repos/{owner}/{repo}/issues/{issue_number}/comments',
14 | {
15 | ...repo,
16 | issue_number: pr.number,
17 | }
18 | );
19 |
20 | const previousReport = commentList.find((comment) =>
21 | comment.body?.includes(getReportTag(options))
22 | );
23 |
24 | return !previousReport ? null : previousReport;
25 | }
26 |
--------------------------------------------------------------------------------
/src/report/generateCommitReport.ts:
--------------------------------------------------------------------------------
1 | import { context, getOctokit } from '@actions/github';
2 |
3 | export const generateCommitReport = async (
4 | report: string,
5 | repo: { owner: string; repo: string },
6 | octokit: ReturnType
7 | ) => {
8 | await octokit.rest.repos.createCommitComment({
9 | ...repo,
10 | commit_sha: context.sha,
11 | body: report,
12 | });
13 | };
14 |
--------------------------------------------------------------------------------
/src/report/generatePRReport.ts:
--------------------------------------------------------------------------------
1 | import { getOctokit } from '@actions/github';
2 |
3 | import { fetchPreviousReport } from './fetchPreviousReport';
4 | import { Options } from '../typings/Options';
5 |
6 | export const generatePRReport = async (
7 | report: string,
8 | options: Options,
9 | repo: { owner: string; repo: string },
10 | pr: { number: number },
11 | octokit: ReturnType
12 | ) => {
13 | const previousReport = await fetchPreviousReport(
14 | octokit,
15 | repo,
16 | pr,
17 | options
18 | );
19 |
20 | if (previousReport) {
21 | await octokit.rest.issues.updateComment({
22 | ...repo,
23 | body: report,
24 | comment_id: previousReport.id,
25 | });
26 | } else {
27 | await octokit.rest.issues.createComment({
28 | ...repo,
29 | body: report,
30 | issue_number: pr.number,
31 | });
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/src/stages/__mocks__/createReport.ts:
--------------------------------------------------------------------------------
1 | export const createReport = jest.fn();
2 |
--------------------------------------------------------------------------------
/src/stages/__mocks__/getCoverage.ts:
--------------------------------------------------------------------------------
1 | export const getCoverage = jest.fn();
2 |
--------------------------------------------------------------------------------
/src/stages/__mocks__/switchBranch.ts:
--------------------------------------------------------------------------------
1 | export const switchBranch = jest.fn();
2 |
3 | export const checkoutRef = jest.fn();
4 |
5 | export const getCurrentBranch = jest.fn();
6 |
--------------------------------------------------------------------------------
/src/stages/collectCoverage.ts:
--------------------------------------------------------------------------------
1 | import { readFile } from 'fs-extra';
2 |
3 | import { REPORT_PATH } from '../constants/REPORT_PATH';
4 | import { ActionError } from '../typings/ActionError';
5 | import { FailReason } from '../typings/Report';
6 | import { DataCollector } from '../utils/DataCollector';
7 | import { i18n } from '../utils/i18n';
8 | import { joinPaths } from '../utils/joinPaths';
9 |
10 | export const collectCoverage = async (
11 | dataCollector: DataCollector,
12 | workingDirectory?: string,
13 | coverageFile?: string
14 | ) => {
15 | const pathToCoverageFile = joinPaths(
16 | workingDirectory,
17 | coverageFile || REPORT_PATH
18 | );
19 |
20 | try {
21 | // Originally made by Jeremy Gillick (https://github.com/jgillick)
22 | // Modified after big refactor by Artiom Tretjakovas (https://github.com/ArtiomTr)
23 | // Load coverage from file
24 |
25 | dataCollector.info(
26 | i18n('loadingCoverageFromFile', {
27 | pathToCoverageFile,
28 | })
29 | );
30 |
31 | const outputBuffer = await readFile(pathToCoverageFile);
32 |
33 | return outputBuffer.toString();
34 | } catch (error) {
35 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
36 | throw new ActionError(FailReason.REPORT_NOT_FOUND, {
37 | coveragePath: pathToCoverageFile,
38 | });
39 | }
40 |
41 | throw new ActionError(FailReason.READING_COVERAGE_FILE_FAILED, {
42 | error: (error as Error).toString(),
43 | });
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/src/stages/createRunReport.ts:
--------------------------------------------------------------------------------
1 | import { getFailureDetails } from '../format/getFailureDetails';
2 | import { JsonReport } from '../typings/JsonReport';
3 | import { TestRunReport } from '../typings/Report';
4 | import { i18n } from '../utils/i18n';
5 |
6 | export const createRunReport = (headReport: JsonReport): TestRunReport => {
7 | return headReport.success
8 | ? {
9 | success: true,
10 | title: i18n('testsSuccess'),
11 | summary: i18n('testsSuccessSummary', {
12 | numPassedTests: headReport.numPassedTests,
13 | numPassedTestSuites: headReport.numPassedTestSuites,
14 | ending: headReport.numPassedTestSuites > 1 ? 's' : '',
15 | }),
16 | }
17 | : {
18 | success: false,
19 | title: i18n('testsFail'),
20 | summary: i18n('testsFailSummary', {
21 | numFailedTests: headReport.numFailedTests,
22 | numTotalTests: headReport.numTotalTests,
23 | numFailedTestSuites: headReport.numFailedTestSuites,
24 | numTotalTestSuites: headReport.numTotalTestSuites,
25 | }),
26 | failures: getFailureDetails(headReport),
27 | };
28 | };
29 |
--------------------------------------------------------------------------------
/src/stages/getCoverage.ts:
--------------------------------------------------------------------------------
1 | import { collectCoverage } from './collectCoverage';
2 | import { installDependencies } from './installDependencies';
3 | import { parseCoverage } from './parseCoverage';
4 | import { runTest } from './runTest';
5 | import { ActionError } from '../typings/ActionError';
6 | import { JsonReport } from '../typings/JsonReport';
7 | import {
8 | Options,
9 | shouldInstallDeps,
10 | shouldRunTestScript,
11 | } from '../typings/Options';
12 | import { FailReason } from '../typings/Report';
13 | import { DataCollector } from '../utils/DataCollector';
14 | import { runStage } from '../utils/runStage';
15 |
16 | export const getCoverage = async (
17 | dataCollector: DataCollector,
18 | options: Options,
19 | runAll: boolean,
20 | coverageFilePath: string | undefined
21 | ): Promise => {
22 | await runStage('install', dataCollector, async (skip) => {
23 | if (
24 | coverageFilePath ||
25 | (!runAll && !shouldInstallDeps(options.skipStep))
26 | ) {
27 | skip();
28 | }
29 |
30 | await installDependencies(
31 | options.packageManager,
32 | options.workingDirectory
33 | );
34 | });
35 |
36 | await runStage('runTest', dataCollector, async (skip) => {
37 | if (
38 | coverageFilePath ||
39 | (!runAll && !shouldRunTestScript(options.skipStep))
40 | ) {
41 | skip();
42 | }
43 |
44 | await runTest(options.testScript, options.workingDirectory);
45 | });
46 |
47 | const [isCoverageCollected, rawCoverage] = await runStage(
48 | 'collectCoverage',
49 | dataCollector,
50 | () =>
51 | collectCoverage(
52 | dataCollector as DataCollector,
53 | options.workingDirectory,
54 | coverageFilePath
55 | )
56 | );
57 |
58 | const [coverageParsed, jsonReport] = await runStage(
59 | 'parseCoverage',
60 | dataCollector,
61 | async (skip) => {
62 | if (!isCoverageCollected) {
63 | skip();
64 | }
65 |
66 | const jsonReport = parseCoverage(rawCoverage!);
67 |
68 | return jsonReport;
69 | }
70 | );
71 |
72 | if (!coverageParsed || !jsonReport) {
73 | throw new ActionError(FailReason.FAILED_GETTING_COVERAGE);
74 | }
75 |
76 | return jsonReport;
77 | };
78 |
--------------------------------------------------------------------------------
/src/stages/installDependencies.ts:
--------------------------------------------------------------------------------
1 | import { exec } from '@actions/exec';
2 |
3 | import { PackageManagerType } from '../typings/Options';
4 | import { joinPaths } from '../utils/joinPaths';
5 | import { removeDirectory } from '../utils/removeDirectory';
6 |
7 | export const installDependencies = async (
8 | packageManager: PackageManagerType = 'npm',
9 | workingDirectory?: string
10 | ) => {
11 | // NOTE: The `npm ci` command is not used. Because if your version of npm is old, the generated `package-lock.json` will also be old, and the latest version of `npm ci` will fail.
12 | await removeDirectory(joinPaths(workingDirectory, 'node_modules'));
13 |
14 | await exec(`${packageManager} install`, undefined, {
15 | cwd: workingDirectory,
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/src/stages/parseCoverage.ts:
--------------------------------------------------------------------------------
1 | import { ActionError } from '../typings/ActionError';
2 | import { JsonReport } from '../typings/JsonReport';
3 | import { FailReason } from '../typings/Report';
4 |
5 | export const parseCoverage = (src: string): JsonReport => {
6 | try {
7 | return JSON.parse(src);
8 | } catch (err) {
9 | throw new ActionError(FailReason.INVALID_COVERAGE_FORMAT);
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/stages/runTest.ts:
--------------------------------------------------------------------------------
1 | import { exec } from '@actions/exec';
2 |
3 | import { getTestCommand } from '../utils/getTestCommand';
4 |
5 | export const runTest = async (
6 | testCommand: string,
7 | workingDirectory?: string
8 | ) => {
9 | await exec(
10 | await getTestCommand(testCommand, 'report.json', workingDirectory),
11 | [],
12 | {
13 | cwd: workingDirectory,
14 | }
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/stages/switchBranch.ts:
--------------------------------------------------------------------------------
1 | import { exec } from '@actions/exec';
2 |
3 | import { GithubRef } from '../typings/Options';
4 |
5 | export const switchBranch = async (branch: string) => {
6 | try {
7 | await exec(`git fetch --all --depth=1`);
8 | } catch (err) {
9 | console.warn('Error fetching git repository', err);
10 | }
11 |
12 | await exec(`git checkout -f ${branch}`);
13 | };
14 |
15 | const checkoutRefNew = async (
16 | ref: GithubRef,
17 | remoteName: string,
18 | newBranchName: string
19 | ) => {
20 | if (!ref.ref || !ref.repo || !ref.repo.clone_url || !ref.sha) {
21 | throw new Error('Invalid ref in context - cannot checkout branch');
22 | }
23 |
24 | try {
25 | // Make sure repository is accessible
26 | await exec(`git fetch --depth=1 --dry-run ${ref.repo.clone_url}`);
27 |
28 | // And only then add it as remote
29 | await exec(`git remote add ${remoteName} ${ref.repo.clone_url}`);
30 | } catch {
31 | /* Ignore error */
32 | }
33 |
34 | try {
35 | // Try to forcibly fetch remote
36 | await exec(`git fetch --depth=1 ${remoteName}`);
37 | } catch {
38 | /* Ignore error */
39 | }
40 |
41 | await exec(
42 | `git checkout -b ${newBranchName} --track ${remoteName}/${ref.ref} -f`
43 | );
44 | };
45 |
46 | export const checkoutRef = async (
47 | ref: GithubRef,
48 | remoteName: string,
49 | newBranchName: string
50 | ) => {
51 | try {
52 | await checkoutRefNew(ref, remoteName, newBranchName);
53 | } catch {
54 | console.warn(
55 | 'Failed to perform new algorithm for checking out. ' +
56 | 'The action will automatically fallback and try to do as much as it could. ' +
57 | 'However, this may lead to inconsistent behavior. Usually, this issue is ' +
58 | 'caused by old version of `actions/checkout` action. Try to use modern ' +
59 | 'version, like `v2` or `v3`.'
60 | );
61 |
62 | try {
63 | await exec(`git fetch --depth=1`);
64 | } catch (err) {
65 | console.warn('Error fetching git repository', err);
66 | }
67 | try {
68 | await exec(`git checkout ${ref.ref} -f`);
69 | } catch {
70 | await exec(`git checkout ${ref.sha} -f`);
71 | }
72 | }
73 | };
74 |
75 | export const getCurrentBranch = async () => {
76 | try {
77 | let branchStr = '';
78 | await exec('git show -s --pretty=%D HEAD', undefined, {
79 | listeners: {
80 | stdout: (data) => {
81 | branchStr += data.toString();
82 | },
83 | },
84 | });
85 |
86 | const realBranchName = branchStr.trim().match(/\S+$/);
87 |
88 | if (realBranchName === null) {
89 | return;
90 | }
91 |
92 | return realBranchName[0].trim();
93 | } catch (err) {
94 | console.warn('Failed to get current branch', err);
95 | }
96 |
97 | return undefined;
98 | };
99 |
--------------------------------------------------------------------------------
/src/typings/ActionError.ts:
--------------------------------------------------------------------------------
1 | import { FailReason } from './Report';
2 | import { i18n } from '../utils/i18n';
3 |
4 | export class ActionError extends Error {
5 | public readonly failReason: FailReason;
6 |
7 | public constructor(reason: FailReason, details?: T) {
8 | super(
9 | i18n(
10 | `errors.${reason}`,
11 | (details as unknown) as Record
12 | )
13 | );
14 | this.failReason = reason;
15 | }
16 |
17 | public toString(): string {
18 | return this.message;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/typings/Coverage.ts:
--------------------------------------------------------------------------------
1 | export type CoverageDetail = {
2 | filename: string;
3 | statements: number;
4 | branches: number;
5 | functions: number;
6 | lines: number;
7 | };
8 |
9 | export type CoverageDetailsMap = Record;
10 |
11 | export type CoverageSummary = {
12 | title: string;
13 | covered: number;
14 | total: number;
15 | percentage: number;
16 | };
17 |
--------------------------------------------------------------------------------
/src/typings/JestThreshold.ts:
--------------------------------------------------------------------------------
1 | export type SingleThreshold = {
2 | branches?: number;
3 | functions?: number;
4 | lines?: number;
5 | statements?: number;
6 | };
7 |
8 | export type JestThreshold = Record & {
9 | global?: SingleThreshold;
10 | };
11 |
--------------------------------------------------------------------------------
/src/typings/JsonReport.ts:
--------------------------------------------------------------------------------
1 | export type JsonReport = {
2 | numFailedTestSuites: number;
3 | numFailedTests: number;
4 | numPassedTestSuites: number;
5 | numPassedTests: number;
6 | numPendingTestSuites: number;
7 | numPendingTests: number;
8 | numRuntimeErrorTestSuites: number;
9 | numTodoTests: number;
10 | numTotalTestSuites: number;
11 | numTotalTests: number;
12 | openHandles?: unknown[];
13 | snapshot: Snapshot;
14 | startTime: number;
15 | success: boolean;
16 | testResults?: TestResult[];
17 | wasInterrupted: boolean;
18 | coverageMap: CoverageMap;
19 | };
20 |
21 | export type Snapshot = {
22 | added: number;
23 | didUpdate: boolean;
24 | failure: boolean;
25 | filesAdded: number;
26 | filesRemoved: number;
27 | filesRemovedList?: unknown[];
28 | filesUnmatched: number;
29 | filesUpdated: number;
30 | matched: number;
31 | total: number;
32 | unchecked: number;
33 | uncheckedKeysByFile?: unknown[];
34 | unmatched: number;
35 | updated: number;
36 | };
37 |
38 | export type TestResult = {
39 | assertionResults?: AssertionResult[];
40 | endTime: number;
41 | message: string;
42 | name: string;
43 | startTime: number;
44 | status: string;
45 | summary: string;
46 | };
47 |
48 | export type AssertionResult = {
49 | ancestorTitles?: string[];
50 | failureMessages?: string[];
51 | fullName: string;
52 | location: Location;
53 | status: string;
54 | title: string;
55 | };
56 |
57 | export type Location = {
58 | column?: number;
59 | line: number;
60 | };
61 |
62 | export type Range = {
63 | start?: Location;
64 | end?: Location;
65 | };
66 |
67 | export type CoverageMap = Record;
68 |
69 | export type FileCoverage = {
70 | path: string;
71 | statementMap: StatementMap;
72 | fnMap: FunctionMap;
73 | branchMap: BranchMap;
74 | s: HitMap;
75 | f: HitMap;
76 | b: ArrayHitMap;
77 | };
78 |
79 | export type FileCoverageInData = {
80 | data: FileCoverage;
81 | };
82 |
83 | export type StatementMap = Record;
84 |
85 | export type StatementCoverage = {
86 | start: Location;
87 | end: Location;
88 | };
89 |
90 | export type FunctionMap = Record;
91 |
92 | export type FunctionCoverage = {
93 | name: string;
94 | decl: Range;
95 | loc: Range;
96 | };
97 |
98 | export type BranchMap = Record;
99 |
100 | export type BranchCoverage = {
101 | loc: Range;
102 | type: string;
103 | locations?: Range[];
104 | };
105 |
106 | export type HitMap = Record;
107 |
108 | export type ArrayHitMap = Record;
109 |
--------------------------------------------------------------------------------
/src/typings/Report.ts:
--------------------------------------------------------------------------------
1 | export enum FailReason {
2 | TESTS_FAILED = 'testsFailed',
3 | INVALID_COVERAGE_FORMAT = 'invalidFormat',
4 | UNDER_THRESHOLD = 'underThreshold',
5 | UNKNOWN_ERROR = 'unknownError',
6 | REPORT_NOT_FOUND = 'reportNotFound',
7 | READING_COVERAGE_FILE_FAILED = 'readingCoverageFileFailed',
8 | FAILED_GETTING_COVERAGE = 'failedGettingCoverage',
9 | MISSING_CHECKS_PERMISSION = 'missingChecksPermission',
10 | }
11 |
12 | export type TestRunReport =
13 | | {
14 | success: true;
15 | title: string;
16 | summary: string;
17 | }
18 | | {
19 | success: false;
20 | title: string;
21 | summary: string;
22 | failures: string;
23 | };
24 |
25 | export type SummaryReport = {
26 | text: string;
27 | };
28 |
--------------------------------------------------------------------------------
/src/typings/ThresholdResult.ts:
--------------------------------------------------------------------------------
1 | export enum ThresholdType {
2 | STATEMENTS = 'statements',
3 | FUNCTIONS = 'functions',
4 | BRANCHES = 'branches',
5 | LINES = 'lines',
6 | }
7 |
8 | export type ThresholdResult = {
9 | path: string;
10 | expected: number;
11 | received: number;
12 | type: ThresholdType;
13 | };
14 |
--------------------------------------------------------------------------------
/src/typings/__mocks__/Options.ts:
--------------------------------------------------------------------------------
1 | export const getOptions = jest.fn(() => ({
2 | token: 'TOKEN',
3 | testScript: 'test script',
4 | iconType: 'emoji',
5 | annotations: 'all',
6 | threshold: 80,
7 | packageManager: 'npm',
8 | skipStep: 'none',
9 | }));
10 |
--------------------------------------------------------------------------------
/src/utils/DataCollector.ts:
--------------------------------------------------------------------------------
1 | import * as core from '@actions/core';
2 |
3 | export const createDataCollector = (): DataCollector => {
4 | const errors: Array = [];
5 | const collectedData: Array = [];
6 | const messages: Array = [];
7 |
8 | const error = (error: Error) => {
9 | errors.push(error);
10 | core.error(
11 | error.toString().concat(error.stack ? `\n${error.stack}` : '')
12 | );
13 | };
14 |
15 | const add = (data: T) => {
16 | collectedData.push(data);
17 | };
18 |
19 | const info = (message: string) => {
20 | messages.push(message);
21 | core.info(message);
22 | };
23 |
24 | const get = () => ({
25 | data: collectedData,
26 | errors,
27 | messages,
28 | });
29 |
30 | return {
31 | error,
32 | add,
33 | get,
34 | info,
35 | };
36 | };
37 |
38 | export type CollectedData = {
39 | errors: Array;
40 | messages: Array;
41 | data: Array;
42 | };
43 |
44 | export type DataCollector = {
45 | error: (error: Error) => void;
46 | info: (message: string) => void;
47 | add: (data: T) => void;
48 | get: () => CollectedData;
49 | };
50 |
--------------------------------------------------------------------------------
/src/utils/__mocks__/removeDirectory.ts:
--------------------------------------------------------------------------------
1 | export const removeDirectory = jest.fn().mockName('removeDirectory');
2 |
--------------------------------------------------------------------------------
/src/utils/accumulateCoverageDetails.ts:
--------------------------------------------------------------------------------
1 | import { DetailedFileCoverage } from './getFileCoverageMap';
2 |
3 | export const accumulateCoverageDetails = (
4 | coverageDetails: DetailedFileCoverage[]
5 | ): DetailedFileCoverage =>
6 | coverageDetails.reduce(
7 | (acc, current) => {
8 | acc.totalStatements += current.totalStatements;
9 | acc.coveredStatements += current.coveredStatements;
10 | acc.totalFunctions += current.totalFunctions;
11 | acc.coveredFunctions += current.coveredFunctions;
12 | acc.totalBranches += current.totalBranches;
13 | acc.coveredBranches += current.coveredBranches;
14 | acc.totalLines += current.totalLines;
15 | acc.coveredLines += current.coveredLines;
16 |
17 | return acc;
18 | },
19 | {
20 | totalStatements: 0,
21 | coveredStatements: 0,
22 | totalFunctions: 0,
23 | coveredFunctions: 0,
24 | totalBranches: 0,
25 | coveredBranches: 0,
26 | totalLines: 0,
27 | coveredLines: 0,
28 | }
29 | );
30 |
--------------------------------------------------------------------------------
/src/utils/checkSingleThreshold.ts:
--------------------------------------------------------------------------------
1 | import isNil from 'lodash/isNil';
2 |
3 | import { DetailedFileCoverage } from './getFileCoverageMap';
4 | import { getPercents } from '../format/getPercents';
5 | import { SingleThreshold } from '../typings/JestThreshold';
6 | import { ThresholdResult, ThresholdType } from '../typings/ThresholdResult';
7 |
8 | export const checkSingleThreshold = (
9 | threshold: SingleThreshold,
10 | coverage: DetailedFileCoverage,
11 | path: string
12 | ): ThresholdResult | undefined => {
13 | const queue = [
14 | {
15 | total: coverage.totalStatements,
16 | covered: coverage.coveredStatements,
17 | threshold: threshold.statements,
18 | type: ThresholdType.STATEMENTS,
19 | },
20 | {
21 | total: coverage.totalBranches,
22 | covered: coverage.coveredBranches,
23 | threshold: threshold.branches,
24 | type: ThresholdType.BRANCHES,
25 | },
26 | {
27 | total: coverage.totalFunctions,
28 | covered: coverage.coveredFunctions,
29 | threshold: threshold.functions,
30 | type: ThresholdType.FUNCTIONS,
31 | },
32 | {
33 | total: coverage.totalLines,
34 | covered: coverage.coveredLines,
35 | threshold: threshold.lines,
36 | type: ThresholdType.LINES,
37 | },
38 | ];
39 |
40 | for (const { total, covered, threshold, type } of queue) {
41 | const result = checkSingleStat(total, covered, threshold, type, path);
42 |
43 | if (result) {
44 | return result;
45 | }
46 | }
47 |
48 | return undefined;
49 | };
50 |
51 | const checkSingleStat = (
52 | total: number,
53 | covered: number,
54 | threshold: number | undefined,
55 | type: ThresholdType,
56 | path: string
57 | ): ThresholdResult | undefined => {
58 | if (isNil(threshold)) {
59 | return undefined;
60 | }
61 |
62 | if (threshold >= 0) {
63 | const percents = getPercents(covered, total);
64 |
65 | return percents >= threshold
66 | ? undefined
67 | : {
68 | expected: threshold,
69 | received: percents,
70 | type,
71 | path,
72 | };
73 | }
74 |
75 | return covered >= -threshold
76 | ? undefined
77 | : {
78 | expected: threshold,
79 | received: covered,
80 | type,
81 | path,
82 | };
83 | };
84 |
--------------------------------------------------------------------------------
/src/utils/createMarkdownSpoiler.ts:
--------------------------------------------------------------------------------
1 | export type SpoilerConfig = {
2 | body: string;
3 | summary: string;
4 | };
5 |
6 | export const createMarkdownSpoiler = ({
7 | body,
8 | summary,
9 | }: SpoilerConfig): string => `
10 | ${summary}
11 |
12 |
13 | ${body}
14 |
15 |
16 | `;
17 |
--------------------------------------------------------------------------------
/src/utils/decimalToString.ts:
--------------------------------------------------------------------------------
1 | export const decimalToString = (n: number, digitsAfterDot = 2): string =>
2 | n.toFixed(digitsAfterDot).replace(/\.?0+$/, '');
3 |
--------------------------------------------------------------------------------
/src/utils/formatPercentage.ts:
--------------------------------------------------------------------------------
1 | import { decimalToString } from './decimalToString';
2 | import { formatPercentageDelta } from './formatPercentageDelta';
3 | import { i18n } from './i18n';
4 |
5 | const APPROXIMATION_THRESHOLD = 0;
6 |
7 | export const formatPercentage = (
8 | headPercentage: number,
9 | basePercentage: number = headPercentage
10 | ) => {
11 | const delta = headPercentage - basePercentage;
12 |
13 | const isDeltaValid = Math.abs(delta) > APPROXIMATION_THRESHOLD;
14 |
15 | return i18n(
16 | isDeltaValid
17 | ? '{{ percentage }}% {{ delta }}
'
18 | : '{{ percentage }}%',
19 | {
20 | percentage: decimalToString(headPercentage),
21 | baseCoverage:
22 | i18n('baseCoverage') + decimalToString(basePercentage),
23 | delta: isDeltaValid ? formatPercentageDelta(delta) : '',
24 | }
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/utils/formatPercentageDelta.ts:
--------------------------------------------------------------------------------
1 | import { decimalToString } from './decimalToString';
2 | import { i18n } from './i18n';
3 |
4 | export const formatPercentageDelta = (delta: number): string =>
5 | i18n(
6 | delta > 0
7 | ? `(+{{ delta }}% :arrow_up_small:)`
8 | : `({{ delta }}% :small_red_triangle_down:)`,
9 | {
10 | delta: decimalToString(delta),
11 | }
12 | );
13 |
--------------------------------------------------------------------------------
/src/utils/getConsoleLink.ts:
--------------------------------------------------------------------------------
1 | import { context } from '@actions/github';
2 |
3 | export const getConsoleLink = () => {
4 | const repositoryUrl =
5 | context.payload.repository?.html_url ??
6 | `https://github.com/${context.repo.owner}/${context.repo.repo}`;
7 |
8 | return `${repositoryUrl}/actions/runs/${context.runId}`;
9 | };
10 |
--------------------------------------------------------------------------------
/src/utils/getCoverageForDirectory.ts:
--------------------------------------------------------------------------------
1 | import micromatch from 'micromatch';
2 |
3 | import { accumulateCoverageDetails } from './accumulateCoverageDetails';
4 | import { DetailedFileCoverage, FileCoverageMap } from './getFileCoverageMap';
5 |
6 | export const getCoverageForDirectory = (
7 | directory: string,
8 | details: FileCoverageMap
9 | ): DetailedFileCoverage => {
10 | const children = micromatch(Object.keys(details), `${directory}/**`);
11 |
12 | return accumulateCoverageDetails(children.map((child) => details[child]));
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/getFileCoverageMap.ts:
--------------------------------------------------------------------------------
1 | import {
2 | coveredBranchesCounter,
3 | coveredLinesCounter,
4 | standardCoveredCounter,
5 | standardTotalCounter,
6 | totalBranchesCounter,
7 | totalLinesCounter,
8 | } from '../format/counters';
9 | import { JsonReport } from '../typings/JsonReport';
10 |
11 | export type DetailedFileCoverage = {
12 | totalStatements: number;
13 | coveredStatements: number;
14 | totalFunctions: number;
15 | coveredFunctions: number;
16 | totalBranches: number;
17 | coveredBranches: number;
18 | totalLines: number;
19 | coveredLines: number;
20 | };
21 |
22 | export type FileCoverageMap = Record;
23 |
24 | export const getFileCoverageMap = (jsonReport: JsonReport) =>
25 | Object.entries(jsonReport.coverageMap).reduce(
26 | (acc, [filename, fileCoverage]) => {
27 | const normalizedFileCoverage =
28 | 'statementMap' in fileCoverage
29 | ? fileCoverage
30 | : fileCoverage.data;
31 |
32 | acc[filename] = {
33 | totalStatements: standardTotalCounter('s')(
34 | normalizedFileCoverage
35 | ),
36 | coveredStatements: standardCoveredCounter('s')(
37 | normalizedFileCoverage
38 | ),
39 | totalFunctions: standardTotalCounter('f')(
40 | normalizedFileCoverage
41 | ),
42 | coveredFunctions: standardCoveredCounter('f')(
43 | normalizedFileCoverage
44 | ),
45 | totalBranches: totalBranchesCounter(normalizedFileCoverage),
46 | coveredBranches: coveredBranchesCounter(normalizedFileCoverage),
47 | totalLines: totalLinesCounter(normalizedFileCoverage),
48 | coveredLines: coveredLinesCounter(normalizedFileCoverage),
49 | };
50 | return acc;
51 | },
52 | {}
53 | );
54 |
--------------------------------------------------------------------------------
/src/utils/getNormalThreshold.ts:
--------------------------------------------------------------------------------
1 | import isNil from 'lodash/isNil';
2 |
3 | import { tryGetJestThreshold } from './tryGetJestThreshold';
4 | import { JestThreshold } from '../typings/JestThreshold';
5 |
6 | export const getNormalThreshold = async (
7 | workingDirectory: string,
8 | thresholdFromOptions: number | undefined
9 | ): Promise => {
10 | const threshold = await tryGetJestThreshold(workingDirectory);
11 |
12 | // Should be removed in further versions
13 | if (isNil(threshold)) {
14 | return {
15 | global: {
16 | branches: thresholdFromOptions,
17 | functions: thresholdFromOptions,
18 | lines: thresholdFromOptions,
19 | statements: thresholdFromOptions,
20 | },
21 | };
22 | }
23 |
24 | return threshold;
25 | };
26 |
--------------------------------------------------------------------------------
/src/utils/getPrPatch.ts:
--------------------------------------------------------------------------------
1 | import { context, getOctokit } from '@actions/github';
2 | import { Endpoints } from '@octokit/types';
3 |
4 | import { Options } from '../typings/Options';
5 |
6 | type getPullsResponse = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}']['response'];
7 |
8 | export async function getPrPatch(
9 | octokit: ReturnType,
10 | options: Options
11 | ): Promise {
12 | const pullsResponse: getPullsResponse = ((await octokit.rest.pulls.get({
13 | ...context.repo,
14 | pull_number: options.pullRequest!.number,
15 | })) as unknown) as getPullsResponse;
16 |
17 | const headSha = pullsResponse.data.head.sha;
18 | const baseSha = pullsResponse.data.base.sha;
19 |
20 | const compareResponse: {
21 | data: string;
22 | } = ((await octokit.rest.repos.compareCommits({
23 | ...context.repo,
24 | base: baseSha,
25 | head: headSha,
26 | headers: {
27 | accept: 'application/vnd.github.v3.patch',
28 | },
29 | })) as unknown) as { data: string };
30 |
31 | return compareResponse.data;
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/getStatusOfPercents.ts:
--------------------------------------------------------------------------------
1 | import { i18n } from './i18n';
2 |
3 | const DEFAULT_STEP = 20;
4 |
5 | export const getStatusOfPercents = (percentage: number, threshold = 60) => {
6 | let step = DEFAULT_STEP;
7 |
8 | if (threshold > 100 - DEFAULT_STEP * 2) {
9 | step = (100 - threshold) / 2;
10 | }
11 |
12 | if (percentage < threshold) {
13 | return i18n(':red_circle:');
14 | } else if (percentage < threshold + step) {
15 | return i18n(':yellow_circle:');
16 | } else {
17 | return i18n(':green_circle:');
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/src/utils/getTestCommand.ts:
--------------------------------------------------------------------------------
1 | import { exec } from '@actions/exec';
2 | import semver from 'semver';
3 |
4 | import { isOldScript } from './isOldScript';
5 |
6 | const checkPnpmVersion = async () => {
7 | try {
8 | let versionStr = '';
9 | await exec('pnpm -v', undefined, {
10 | listeners: {
11 | stdout: (data) => {
12 | versionStr += data.toString();
13 | },
14 | },
15 | });
16 |
17 | return semver.satisfies(versionStr.trim(), '< 7.0.0');
18 | } catch (error) {
19 | return true;
20 | }
21 | };
22 |
23 | export const getTestCommand = async (
24 | command: string,
25 | outputFile: string,
26 | workingDirectory: string | undefined
27 | ) => {
28 | if (await isOldScript(command, workingDirectory)) {
29 | // TODO: add warning here
30 | return command;
31 | }
32 |
33 | const isNpmStyle =
34 | command.startsWith('npm') ||
35 | (command.startsWith('pnpm') && (await checkPnpmVersion()));
36 |
37 | const hasDoubleHyphen = command.includes(' -- ');
38 |
39 | // building new command
40 | const newCommandBuilder: (string | boolean)[] = [
41 | command,
42 | // add two hypens if it is npm or pnpm package managers and two hyphens don't already exist
43 | isNpmStyle && !hasDoubleHyphen && '--',
44 | // argument which indicates that jest runs in CI environment
45 | '--ci',
46 | // telling jest that output should be in json format
47 | '--json',
48 | // force jest to collect coverage
49 | '--coverage',
50 | // argument which tells jest to include tests' locations in the generated json output
51 | '--testLocationInResults',
52 | // output file
53 | `--outputFile="${outputFile}"`,
54 | ];
55 |
56 | return newCommandBuilder.filter(Boolean).join(' ');
57 | };
58 |
--------------------------------------------------------------------------------
/src/utils/i18n.ts:
--------------------------------------------------------------------------------
1 | import { getInput } from '@actions/core';
2 | import get from 'lodash/get';
3 |
4 | import { insertArgs } from './insertArgs';
5 | import strings from '../format/strings.json';
6 |
7 | const iconRegex = /:(\w+):/g;
8 |
9 | const iconType = getInput('icons');
10 |
11 | const icons = (strings.icons as Record>)[
12 | iconType || 'emoji'
13 | ];
14 |
15 | export const i18n = (key: string, args?: Record) => {
16 | let string = get(strings, key, key) as string | string[];
17 |
18 | if (Array.isArray(string)) {
19 | string = string.join('\n');
20 | }
21 |
22 | const normalizedIconsString = string.replace(
23 | iconRegex,
24 | (initialValue, key) => {
25 | if (key in icons) {
26 | return icons[key];
27 | } else {
28 | return initialValue;
29 | }
30 | }
31 | );
32 |
33 | if (!args) {
34 | return normalizedIconsString;
35 | }
36 |
37 | return insertArgs(normalizedIconsString, args as Record);
38 | };
39 |
--------------------------------------------------------------------------------
/src/utils/insertArgs.ts:
--------------------------------------------------------------------------------
1 | export const insertArgs = (
2 | text: string,
3 | args: Record
4 | ) => {
5 | Object.keys(args).forEach(
6 | (argName) =>
7 | args[argName] !== undefined &&
8 | args[argName] !== null &&
9 | (text = text.replace(`{{ ${argName} }}`, args[argName] as string))
10 | );
11 | return text;
12 | };
13 |
--------------------------------------------------------------------------------
/src/utils/isOldScript.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | import { readFile } from 'fs-extra';
4 |
5 | const packageScriptRegex = /^(?:(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?([\w:-]+))/;
6 |
7 | export const isOldScript = async (
8 | command: string,
9 | workingDirectory: string | undefined
10 | ) => {
11 | if (command.includes('report.json')) {
12 | return true;
13 | }
14 |
15 | const matchResult = command.match(packageScriptRegex);
16 |
17 | if (matchResult) {
18 | const [, scriptName] = matchResult;
19 |
20 | try {
21 | const packageJson = JSON.parse(
22 | (
23 | await readFile(
24 | join(
25 | ...([workingDirectory, 'package.json'].filter(
26 | Boolean
27 | ) as string[])
28 | )
29 | )
30 | ).toString()
31 | );
32 |
33 | const realScript = packageJson.scripts[scriptName];
34 |
35 | if (realScript.includes('report.json')) {
36 | return true;
37 | }
38 | } catch {
39 | /** ignore exceptions */
40 | }
41 | }
42 |
43 | return false;
44 | };
45 |
--------------------------------------------------------------------------------
/src/utils/isValidNumber.ts:
--------------------------------------------------------------------------------
1 | export const isValidNumber = (value: unknown): value is number =>
2 | typeof value === 'number' && !isNaN(value);
3 |
--------------------------------------------------------------------------------
/src/utils/joinPaths.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | export const joinPaths = (...segments: Array) =>
4 | join(...(segments as string[]).filter((segment) => segment !== undefined));
5 |
--------------------------------------------------------------------------------
/src/utils/markdownTable.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArtiomTr/jest-coverage-report-action/262a7bb0b20c4d1d6b6b026af0f008f78da72788/src/utils/markdownTable.ts
--------------------------------------------------------------------------------
/src/utils/parseJestConfig.ts:
--------------------------------------------------------------------------------
1 | import { loadConfig } from 'c12';
2 |
3 | export const parseJestConfig = async (
4 | workingDirectory: string
5 | ): Promise => {
6 | let { config } = await loadConfig({
7 | cwd: workingDirectory,
8 | name: 'jest',
9 | });
10 |
11 | if (!config || Object.keys(config).length === 0) {
12 | const packageJson = await loadConfig({
13 | cwd: workingDirectory,
14 | configFile: 'package.json',
15 | });
16 |
17 | config = packageJson.config?.jest || {};
18 | }
19 |
20 | return config;
21 | };
22 |
--------------------------------------------------------------------------------
/src/utils/removeDirectory.ts:
--------------------------------------------------------------------------------
1 | import { rm, rmdir } from 'fs-extra';
2 | import { satisfies } from 'semver';
3 |
4 | export const removeDirectory = (path: string) => {
5 | if (satisfies(process.version, '>=14.14.0')) {
6 | return rm(path, { force: true, recursive: true });
7 | } else {
8 | return rmdir(path, { recursive: true });
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/utils/runStage.ts:
--------------------------------------------------------------------------------
1 | import { DataCollector } from './DataCollector';
2 | import { i18n } from './i18n';
3 |
4 | export type SuccessfulStageResult = [success: true, output: T];
5 |
6 | export type FailedStageResult = [success: false, output: undefined];
7 |
8 | export type StageResult = SuccessfulStageResult | FailedStageResult;
9 |
10 | const SKIP_SYMBOL = Symbol();
11 |
12 | export const runStage = async (
13 | stage: string,
14 | dataCollector: DataCollector,
15 | action: (skip: () => never) => Promise | T
16 | ): Promise> => {
17 | const stageKey = `stages.${stage}`;
18 | dataCollector.info(
19 | i18n('stages.defaults.begin', {
20 | stage: i18n(stageKey).toLowerCase(),
21 | })
22 | );
23 |
24 | const skip = () => {
25 | throw SKIP_SYMBOL;
26 | };
27 |
28 | try {
29 | const output = await action(skip);
30 | return [true, output];
31 | } catch (error) {
32 | if (error === SKIP_SYMBOL) {
33 | dataCollector.info(
34 | i18n('stages.defaults.skip', {
35 | stage: i18n(stageKey),
36 | })
37 | );
38 | } else {
39 | dataCollector.info(
40 | i18n('stages.defaults.fail', {
41 | stage: i18n(stageKey),
42 | })
43 | );
44 | dataCollector.error(error as Error);
45 | }
46 |
47 | return [false, undefined];
48 | } finally {
49 | dataCollector.info(
50 | i18n('stages.defaults.end', {
51 | stage: i18n(stageKey),
52 | })
53 | );
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/src/utils/tryGetJestThreshold.ts:
--------------------------------------------------------------------------------
1 | import { parseJestConfig } from './parseJestConfig';
2 | import { JestThreshold } from '../typings/JestThreshold';
3 |
4 | export const tryGetJestThreshold = async (
5 | workingDirectory: string
6 | ): Promise => {
7 | try {
8 | const config = (await parseJestConfig(workingDirectory)) as
9 | | { coverageThreshold: JestThreshold }
10 | | undefined;
11 |
12 | return config?.coverageThreshold;
13 | } catch (err) {
14 | console.log(
15 | '[Warning] Failed to parse jest configuration.',
16 | '"coverageThreshold" from config file will be ignored.',
17 | err
18 | );
19 | return undefined;
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/src/utils/upsertCheck.ts:
--------------------------------------------------------------------------------
1 | import { getOctokit } from '@actions/github';
2 | import { RequestError } from '@octokit/request-error';
3 |
4 | import { CreateCheckOptions } from '../format/annotations/CreateCheckOptions';
5 | import { ActionError } from '../typings/ActionError';
6 | import { FailReason } from '../typings/Report';
7 |
8 | export const upsertCheck = async (
9 | octokit: ReturnType,
10 | check: CreateCheckOptions
11 | ) => {
12 | let check_id: number | undefined;
13 |
14 | try {
15 | const checks = await octokit.rest.checks.listForRef({
16 | owner: check.owner as string,
17 | repo: check.repo as string,
18 | ref: check.head_sha as string,
19 | check_name: check.name as string,
20 | });
21 |
22 | if (checks.data.check_runs.length === 1) {
23 | check_id = checks.data.check_runs[0].id;
24 | }
25 | } catch {
26 | console.warn(
27 | 'Missing `checks: read` permission. It is required to prevent bug when manually rerunning action adds check' +
28 | ' twice. You can safely ignore this, and enable permission only if you encounter duplicate checks.'
29 | );
30 | }
31 |
32 | try {
33 | if (check_id === undefined) {
34 | await octokit.rest.checks.create(check);
35 | } else {
36 | await octokit.rest.checks.update({
37 | check_run_id: check_id,
38 | ...check,
39 | head_sha: undefined,
40 | });
41 | }
42 | } catch (error) {
43 | if (error instanceof RequestError && error.status === 403) {
44 | throw new ActionError(FailReason.MISSING_CHECKS_PERMISSION);
45 | }
46 |
47 | throw error;
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/src/utils/withExplanation.ts:
--------------------------------------------------------------------------------
1 | export const withExplanation = (text: string, explanation: string) =>
2 | `${text}:grey_question:
`;
3 |
--------------------------------------------------------------------------------
/tests/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": false,
3 | "rules": {
4 | "@typescript-eslint/no-explicit-any": "off"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tests/annotations/__snapshots__/createFailedTestsAnnotations.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`createFailedTestsAnnotations should create failed tests annotations 1`] = `
4 | Array [
5 | Object {
6 | "annotation_level": "failure",
7 | "end_line": 4,
8 | "message": "",
9 | "path": "tests/format/details/getNewFilesCoverage.test.ts",
10 | "start_line": 4,
11 | "title": "getNewFilesCoverage > should return new file coverage",
12 | },
13 | Object {
14 | "annotation_level": "failure",
15 | "end_line": 29,
16 | "message": "",
17 | "path": "tests/format/details/getNewFilesCoverage.test.ts",
18 | "start_line": 29,
19 | "title": "getNewFilesCoverage > should return empty object, when base details not specified",
20 | },
21 | Object {
22 | "annotation_level": "failure",
23 | "end_line": 23,
24 | "message": "",
25 | "path": "tests/format/summary/getTestRunSummary.test.ts",
26 | "start_line": 23,
27 | "title": "getTestRunSummary > should return failure summary",
28 | },
29 | ]
30 | `;
31 |
--------------------------------------------------------------------------------
/tests/annotations/createCoverageAnnotations.test.ts:
--------------------------------------------------------------------------------
1 | import { relative } from 'path';
2 |
3 | import { createCoverageAnnotations } from '../../src/annotations/createCoverageAnnotations';
4 | import { JsonReport } from '../../src/typings/JsonReport';
5 | import jsonReport from '../mock-data/jsonReport.json';
6 | import jsonReport2 from '../mock-data/jsonReport2.json';
7 | import jsonReport3 from '../mock-data/jsonReport3.json';
8 |
9 | jest.mock('path');
10 |
11 | describe('createCoverageAnnotations', () => {
12 | it('should match snapshot', () => {
13 | (relative as jest.Mock).mockImplementation(
14 | (_, second) => second
15 | );
16 |
17 | expect(createCoverageAnnotations(jsonReport)).toMatchSnapshot();
18 |
19 | expect(
20 | createCoverageAnnotations((jsonReport2 as unknown) as JsonReport)
21 | ).toMatchSnapshot();
22 |
23 | expect(
24 | createCoverageAnnotations((jsonReport3 as unknown) as JsonReport)
25 | ).toMatchSnapshot();
26 |
27 | (relative as jest.Mock).mockClear();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/tests/annotations/createFailedTestsAnnotations.test.ts:
--------------------------------------------------------------------------------
1 | import { relative } from 'path';
2 |
3 | import { createFailedTestsAnnotations } from '../../src/annotations/createFailedTestsAnnotations';
4 | import { JsonReport } from '../../src/typings/JsonReport';
5 | import jsonReport from '../mock-data/jsonReport3.json';
6 |
7 | jest.mock('path');
8 |
9 | describe('createFailedTestsAnnotations', () => {
10 | it('should create failed tests annotations', () => {
11 | (relative as jest.Mock).mockImplementation(
12 | (_, second) => second
13 | );
14 |
15 | expect(
16 | createFailedTestsAnnotations((jsonReport as unknown) as JsonReport)
17 | ).toMatchSnapshot();
18 |
19 | (relative as jest.Mock).mockClear();
20 | });
21 |
22 | it('should return empty array', () => {
23 | expect(createFailedTestsAnnotations({} as JsonReport)).toStrictEqual(
24 | []
25 | );
26 | expect(
27 | createFailedTestsAnnotations(({
28 | testResults: [{}],
29 | } as unknown) as JsonReport)
30 | ).toStrictEqual([]);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/tests/annotations/isAnnotationEnabled.test.ts:
--------------------------------------------------------------------------------
1 | import { isAnnotationEnabled } from '../../src/annotations/isAnnotationEnabled';
2 |
3 | describe('isAnnotationEnabled', () => {
4 | it('should return true', () => {
5 | expect(
6 | isAnnotationEnabled('failed-tests', 'failed-tests')
7 | ).toBeTruthy();
8 | expect(isAnnotationEnabled('all', 'failed-tests')).toBeTruthy();
9 | });
10 |
11 | it('should return false', () => {
12 | expect(isAnnotationEnabled('none', 'failed-tests')).toBeFalsy();
13 | expect(isAnnotationEnabled('coverage', 'failed-tests')).toBeFalsy();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/tests/constants/GITHUB_MESSAGE_SIZE_LIMIT.test.ts:
--------------------------------------------------------------------------------
1 | import { GITHUB_MESSAGE_SIZE_LIMIT } from '../../src/constants/GITHUB_MESSAGE_SIZE_LIMIT';
2 |
3 | describe('GITHUB_MESSAGE_SIZE_LIMIT', () => {
4 | it('should be 65535', () => {
5 | expect(GITHUB_MESSAGE_SIZE_LIMIT).toBe(65535);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/tests/constants/REPORT_PATH.test.ts:
--------------------------------------------------------------------------------
1 | import { REPORT_PATH } from '../../src/constants/REPORT_PATH';
2 |
3 | describe('REPORT_PATH', () => {
4 | it('should be "report.json"', () => {
5 | expect(REPORT_PATH).toBe('report.json');
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/tests/constants/getReportTag.test.ts:
--------------------------------------------------------------------------------
1 | import { getReportTag } from '../../src/constants/getReportTag';
2 |
3 | const REPORT_TAG_REGEX = //;
4 |
5 | describe('getReportTag', () => {
6 | it('should return report tag for full options', () => {
7 | const options = {
8 | workingDirectory: 'directory',
9 | testScript: 'script',
10 | coverageFile: 'coverage',
11 | baseCoverageFile: 'baseCoverage',
12 | customTitle: 'customTitle',
13 | };
14 |
15 | const reportTag = getReportTag(options);
16 |
17 | expect(reportTag).toMatch(REPORT_TAG_REGEX);
18 | });
19 |
20 | it('should return report tag for partial options', () => {
21 | const options = {
22 | testScript: 'script',
23 | };
24 |
25 | const reportTag = getReportTag(options);
26 |
27 | expect(reportTag).toMatch(REPORT_TAG_REGEX);
28 | });
29 |
30 | it('should return different tags for different options', () => {
31 | const options = {
32 | workingDirectory: 'directory',
33 | testScript: 'script',
34 | coverageFile: 'coverage',
35 | baseCoverageFile: 'baseCoverage',
36 | customTitle: 'customTitle',
37 | };
38 |
39 | const reportTag = getReportTag(options);
40 |
41 | [
42 | 'workingDirectory',
43 | 'testScript',
44 | 'coverageFile',
45 | 'baseCoverageFile',
46 | 'customTitle',
47 | ].forEach((option) => {
48 | const changedOptions = { ...options, [option]: 'changed option' };
49 |
50 | const changedReportTag = getReportTag(changedOptions);
51 |
52 | expect(changedReportTag).not.toEqual(reportTag);
53 | });
54 | });
55 |
56 | it('should return same tag if only arbitrary options change', () => {
57 | const options = {
58 | workingDirectory: 'directory',
59 | testScript: 'script',
60 | coverageFile: 'coverage',
61 | baseCoverageFile: 'baseCoverage',
62 | customTitle: 'customTitle',
63 | };
64 |
65 | const reportTag = getReportTag(options);
66 |
67 | [
68 | 'token',
69 | 'iconType',
70 | 'annotations',
71 | 'threshold',
72 | 'packageManager',
73 | 'skipStep',
74 | ].forEach((option) => {
75 | const changedOptions = { ...options, [option]: 'changed option' };
76 |
77 | const changedReportTag = getReportTag(changedOptions);
78 |
79 | expect(changedReportTag).toEqual(reportTag);
80 | });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/tests/filters/onlyChanged.test.ts:
--------------------------------------------------------------------------------
1 | import { Annotation } from '../../src/annotations/Annotation';
2 | import { onlyChanged } from '../../src/filters/onlyChanged';
3 |
4 | const annotations: Annotation[] = [
5 | {
6 | start_line: 5,
7 | end_line: 5,
8 | start_column: 4,
9 | end_column: 21,
10 | path: 'jest/examples/typescript/memory.ts',
11 | annotation_level: 'warning',
12 | title: '🧾 Statement is not covered',
13 | message: 'Warning! Not covered statement',
14 | },
15 | {
16 | start_line: 9,
17 | end_line: 9,
18 | start_column: 4,
19 | end_column: 26,
20 | path: 'jest/examples/typescript/memory.ts',
21 | annotation_level: 'warning',
22 | title: '🧾 Statement is not covered',
23 | message: 'Warning! Not covered statement',
24 | },
25 | {
26 | start_line: 11,
27 | end_line: 11,
28 | start_column: 4,
29 | end_column: 24,
30 | path: 'jest/examples/typescript/memory.ts',
31 | annotation_level: 'warning',
32 | title: '🧾 Statement is not covered',
33 | message: 'Warning! Not covered statement',
34 | },
35 | {
36 | start_line: 15,
37 | end_line: 15,
38 | start_column: 4,
39 | end_column: 26,
40 | path: 'jest/examples/typescript/memory.ts',
41 | annotation_level: 'warning',
42 | title: '🧾 Statement is not covered',
43 | message: 'Warning! Not covered statement',
44 | },
45 | ];
46 | const patchContent = `
47 | From 13c48d4f21f92ea950ab47d4761ec8a7422851c6 Mon Sep 17 00:00:00 2001
48 | From: Florent Jaby
49 | Date: Tue, 16 Aug 2022 10:52:26 +0200
50 | Subject: Adding subtle fixtures inside a test
51 |
52 | ---
53 | jest/examples/typescript/memory.ts | 22 ++++++++++++++++++----
54 | 1 file changed, 18 insertions(+), 4 deletions(-)
55 |
56 | diff --git a/jest/examples/typescript/memory.ts b/jest/examples/typescript/memory.ts
57 | index ba11b9eb..14e13857 100644
58 | --- a/jest/examples/typescript/memory.ts
59 | +++ b/jest/examples/typescript/memory.ts
60 | @@ -7,5 +7,5 @@ name: Some commit name
61 | redoSomeStuff();
62 | if (untestedCondition) {
63 | - didUncoverStatement();
64 | + stillDoUncoverStatement();
65 | }
66 | // continue file
67 | @@ -13,4 +13,5 @@ name: add line
68 | // call this all the time
69 | + callSomeOtherStuff()
70 | if (false) thisWasHereBefore()
71 | return
72 | `;
73 |
74 | describe('onlyChanged(annotations, patchContent)', () => {
75 | it('filters out annotations on unchanged files', () => {
76 | // Given
77 | const expected: Annotation[] = [annotations[1]];
78 | // When
79 | const actual = onlyChanged(annotations, patchContent);
80 | // Then
81 | expect(actual).toEqual(expected);
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/tests/format/__snapshots__/formatCoverage.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`formatCoverage should display warning if hiding details 1`] = `
4 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
5 | | :-: | :- | :- | :-: |
6 | | 🟢 | Statements | 81.82% | 27/33 |
7 | | 🟢 | Branches | 100% | 8/8 |
8 | | 🟢 | Functions | 63.64% | 7/11 |
9 | | 🟢 | Lines | 80.65% | 25/31 |
10 | > :warning: Details were not displayed: the report size has exceeded the limit."
11 | `;
12 |
13 | exports[`formatCoverage should format standard coverage 1`] = `
14 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
15 | | :-: | :- | :- | :-: |
16 | | 🟢 | Statements | 81.82% | 27/33 |
17 | | 🟢 | Branches | 100% | 8/8 |
18 | | 🟢 | Functions | 63.64% | 7/11 |
19 | | 🟢 | Lines | 80.65% | 25/31 |
20 |
21 | "
22 | `;
23 |
24 | exports[`formatCoverage should format standard coverage 2`] = `
25 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
26 | | :-: | :- | :- | :-: |
27 | | 🟢 | Statements | 81.82% | 27/33 |
28 | | 🟢 | Branches | 100% | 8/8 |
29 | | 🟢 | Functions | 63.64% | 7/11 |
30 | | 🟢 | Lines | 80.65% | 25/31 |
31 |
32 | "
33 | `;
34 |
35 | exports[`formatCoverage should format standard coverage 3`] = `
36 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
37 | | :-: | :- | :- | :-: |
38 | | 🟢 | Statements | 81.82% | 27/33 |
39 | | 🟢 | Branches | 100% | 8/8 |
40 | | 🟡 | Functions | 63.64% | 7/11 |
41 | | 🟢 | Lines | 80.65% | 25/31 |
42 |
43 | "
44 | `;
45 |
--------------------------------------------------------------------------------
/tests/format/__snapshots__/formatRunReport.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`formatRunReport should generate summary as markdown (failure tests) 1`] = `
4 | "## Tests Failed
5 |
6 | 2 Tests Passed, 1 Test Failed
7 |
8 |
9 | \`\`\`
10 | fail 1
11 | \`\`\`
12 | ---
13 | \`\`\`
14 | fail 2
15 | \`\`\`
16 |
17 |
18 | "
19 | `;
20 |
21 | exports[`formatRunReport should generate summary as markdown (successful tests) 1`] = `
22 | "## Tests Passed
23 | 2 Tests Passed"
24 | `;
25 |
--------------------------------------------------------------------------------
/tests/format/__snapshots__/getFormattedCoverage.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`getFormattedCoverage should match snapshots 1`] = `
4 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
5 | | :-: | :- | :- | :-: |
6 | | 🟢 | Statements | 81.82% | 27/33 |
7 | | 🟢 | Branches | 100% | 8/8 |
8 | | 🟡 | Functions | 63.64% | 7/11 |
9 | | 🟢 | Lines | 80.65% | 25/31 |
10 |
11 | "
12 | `;
13 |
--------------------------------------------------------------------------------
/tests/format/annotations/getFailedAnnotationsSummary.test.ts:
--------------------------------------------------------------------------------
1 | import { getFailedAnnotationsSummary } from '../../../src/format/annotations/getFailedAnnotationsSummary';
2 | import { JsonReport } from '../../../src/typings/JsonReport';
3 |
4 | describe('getFailedAnnotationsSummary', () => {
5 | it("should return succeeded report's summary", () => {
6 | expect(
7 | getFailedAnnotationsSummary(({
8 | numPassedTests: 10,
9 | numPassedTestSuites: 3,
10 | success: true,
11 | } as unknown) as JsonReport)
12 | ).toBe('10 tests passing in 3 suites.');
13 |
14 | expect(
15 | getFailedAnnotationsSummary(({
16 | numPassedTests: 10,
17 | numPassedTestSuites: 1,
18 | success: true,
19 | } as unknown) as JsonReport)
20 | ).toBe('10 tests passing in 1 suite.');
21 | });
22 |
23 | it("should return failed report's summary", () => {
24 | expect(
25 | getFailedAnnotationsSummary(({
26 | numFailedTests: 10,
27 | numTotalTests: 32,
28 | numFailedTestSuites: 3,
29 | numTotalTestSuites: 10,
30 | success: false,
31 | } as unknown) as JsonReport)
32 | ).toBe('Failed tests: 10/32. Failed suites: 3/10.');
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/tests/format/details/__snapshots__/formatCoverageDetails.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`formatCoverageDetails should match snapshots 1`] = `
4 | "
5 | Show new covered files 🐣
6 |
7 |
8 | | St.:grey_question:
| File | Statements | Branches | Functions | Lines |
9 | | :-: | :- | :- | :- | :- | :- |
10 | | 🔴 | newFile.ts | 50% | 50% | 50% | 50% |
11 |
12 |
13 |
14 |
15 | Show files with reduced coverage 🔻
16 |
17 |
18 | | St.:grey_question:
| File | Statements | Branches | Functions | Lines |
19 | | :-: | :- | :- | :- | :- | :- |
20 | | 🟡 | decreased.ts | 70% (-10% 🔻)
| 50% (-10% 🔻)
| 50% (-10% 🔻)
| 80% (-10% 🔻)
|
21 |
22 |
23 | "
24 | `;
25 |
--------------------------------------------------------------------------------
/tests/format/details/__snapshots__/getFileCoverageDetailRow.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`getFileCoverageDetailRow should return formatted detail row (decreased coverage) 1`] = `
4 | Array [
5 | "🔴",
6 | "hello.ts",
7 | "50% (-10% 🔻)
",
8 | "100%",
9 | "50% (-10% 🔻)
",
10 | "50% (-10% 🔻)
",
11 | ]
12 | `;
13 |
14 | exports[`getFileCoverageDetailRow should return formatted detail row (increased coverage) 1`] = `
15 | Array [
16 | "🔴",
17 | "hello.ts",
18 | "50% (+10% 🔼)
",
19 | "100%",
20 | "50% (+10% 🔼)
",
21 | "50% (+10% 🔼)
",
22 | ]
23 | `;
24 |
25 | exports[`getFileCoverageDetailRow should return formatted detail row (no base details) 1`] = `
26 | Array [
27 | "🔴",
28 | "hello.ts",
29 | "50%",
30 | "100%",
31 | "50%",
32 | "50%",
33 | ]
34 | `;
35 |
--------------------------------------------------------------------------------
/tests/format/details/findCommonPath.test.ts:
--------------------------------------------------------------------------------
1 | import { findCommonPath } from '../../../src/format/details/findCommonPath';
2 |
3 | describe('findCommonPath', () => {
4 | it('should find common filepath', () => {
5 | expect(
6 | findCommonPath([
7 | 'src/details/a.ts',
8 | 'src/details/b.ts',
9 | 'src/format/hello.ts',
10 | ])
11 | ).toBe('src/');
12 |
13 | expect(
14 | findCommonPath([
15 | 'src/details/hello/world/a.ts',
16 | 'src/details/hello/world/b.ts',
17 | 'src/format/hello.ts',
18 | ])
19 | ).toBe('src/');
20 |
21 | expect(
22 | findCommonPath([
23 | 'src/details/hello/world/a.ts',
24 | 'src/details/hello/world/b.ts',
25 | 'src/details/hello/world/hello.ts',
26 | ])
27 | ).toBe('src/details/hello/world/');
28 | });
29 |
30 | it('should return empty string', () => {
31 | expect(findCommonPath([])).toBe('');
32 | expect(
33 | findCommonPath([
34 | 'src/details/hello/world/a.ts',
35 | 'src/details/hello/world/b.ts',
36 | 'src/details/hello/world/hello.ts',
37 | 'hello',
38 | ])
39 | ).toBe('');
40 | });
41 |
42 | it('should return common folder (not part of filename)', () => {
43 | expect(
44 | findCommonPath([
45 | 'src/details/hello/world/hello.ts',
46 | 'src/details/hello/world/helloTest.ts',
47 | 'src/details/hello/world/helloasd',
48 | ])
49 | ).toBe('src/details/hello/world/');
50 |
51 | expect(
52 | findCommonPath([
53 | 'src/details/hello/world/hello.ts',
54 | 'src/details/helloasd.ts',
55 | 'src/details/hello.ts',
56 | ])
57 | ).toBe('src/details/');
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/tests/format/details/formatCoverageDetails.test.ts:
--------------------------------------------------------------------------------
1 | import { formatCoverageDetails } from '../../../src/format/details/formatCoverageDetails';
2 |
3 | describe('formatCoverageDetails', () => {
4 | it('should match snapshots', () => {
5 | expect(
6 | formatCoverageDetails(
7 | {
8 | 'increased.ts': {
9 | lines: 80,
10 | statements: 70,
11 | branches: 50,
12 | functions: 50,
13 | filename: 'increased.ts',
14 | },
15 | 'decreased.ts': {
16 | lines: 80,
17 | statements: 70,
18 | branches: 50,
19 | functions: 50,
20 | filename: 'decreased.ts',
21 | },
22 | 'newFile.ts': {
23 | lines: 50,
24 | statements: 50,
25 | branches: 50,
26 | functions: 50,
27 | filename: 'newFile.ts',
28 | },
29 | },
30 | {
31 | 'increased.ts': {
32 | lines: 50,
33 | statements: 50,
34 | branches: 30,
35 | functions: 30,
36 | filename: 'increased.ts',
37 | },
38 | 'decreased.ts': {
39 | lines: 90,
40 | statements: 80,
41 | branches: 60,
42 | functions: 60,
43 | filename: 'decreased.ts',
44 | },
45 | },
46 | 70
47 | )
48 | ).toMatchSnapshot();
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/tests/format/details/getFileCoverageDetailRow.test.ts:
--------------------------------------------------------------------------------
1 | import { getFileCoverageDetailRow } from '../../../src/format/details/getFileCoverageDetailRow';
2 |
3 | describe('getFileCoverageDetailRow', () => {
4 | it('should return formatted detail row (decreased coverage)', () => {
5 | expect(
6 | getFileCoverageDetailRow(
7 | 'hello.ts',
8 | {
9 | filename: 'hello.ts',
10 | lines: 50,
11 | branches: 100,
12 | functions: 50,
13 | statements: 50,
14 | },
15 | {
16 | filename: 'hello.ts',
17 | lines: 60,
18 | branches: 100,
19 | functions: 60,
20 | statements: 60,
21 | },
22 | 70
23 | )
24 | ).toMatchSnapshot();
25 | });
26 |
27 | it('should return formatted detail row (increased coverage)', () => {
28 | expect(
29 | getFileCoverageDetailRow(
30 | 'hello.ts',
31 | {
32 | filename: 'hello.ts',
33 | lines: 50,
34 | branches: 100,
35 | functions: 50,
36 | statements: 50,
37 | },
38 | {
39 | filename: 'hello.ts',
40 | lines: 40,
41 | branches: 100,
42 | functions: 40,
43 | statements: 40,
44 | },
45 | 70
46 | )
47 | ).toMatchSnapshot();
48 | });
49 |
50 | it('should return formatted detail row (no base details)', () => {
51 | expect(
52 | getFileCoverageDetailRow(
53 | 'hello.ts',
54 | {
55 | filename: 'hello.ts',
56 | lines: 50,
57 | branches: 100,
58 | functions: 50,
59 | statements: 50,
60 | },
61 | undefined,
62 | 70
63 | )
64 | ).toMatchSnapshot();
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/tests/format/details/getNewFilesCoverage.test.ts:
--------------------------------------------------------------------------------
1 | import { getNewFilesCoverage } from '../../../src/format/details/getNewFilesCoverage';
2 |
3 | describe('getNewFilesCoverage', () => {
4 | it('should return new file coverage', () => {
5 | expect(
6 | getNewFilesCoverage(
7 | {
8 | 'hello.ts': {
9 | filename: 'hello.ts',
10 | functions: 50,
11 | branches: 50,
12 | lines: 50,
13 | statements: 50,
14 | },
15 | },
16 | {}
17 | )
18 | ).toStrictEqual({
19 | 'hello.ts': {
20 | filename: 'hello.ts',
21 | functions: 50,
22 | branches: 50,
23 | lines: 50,
24 | statements: 50,
25 | },
26 | });
27 | });
28 |
29 | it('should return empty object, when base details not specified', () => {
30 | expect(
31 | getNewFilesCoverage(
32 | {
33 | 'hello.ts': {
34 | filename: 'hello.ts',
35 | functions: 50,
36 | branches: 50,
37 | lines: 50,
38 | statements: 50,
39 | },
40 | },
41 | undefined
42 | )
43 | ).toStrictEqual({});
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/tests/format/details/parseDetails.test.ts:
--------------------------------------------------------------------------------
1 | import { parseDetails } from '../../../src/format/details/parseDetails';
2 | import { JsonReport } from '../../../src/typings/JsonReport';
3 | import report from '../../mock-data/jsonReport.json';
4 | import report2 from '../../mock-data/jsonReport2.json';
5 |
6 | describe('parseDetails', () => {
7 | it('should match snapshots', () => {
8 | expect(parseDetails(report)).toMatchSnapshot();
9 | expect(
10 | parseDetails((report2 as unknown) as JsonReport)
11 | ).toMatchSnapshot();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/tests/format/details/shrinkLongPath.test.ts:
--------------------------------------------------------------------------------
1 | import { shrinkLongPath } from '../../../src/format/details/shrinkLongPath';
2 |
3 | describe('shrinkLongPath', () => {
4 | it('should shrink long paths', () => {
5 | expect(shrinkLongPath('hello/world/this/is/a.ts')).toBe(
6 | '`...` / a.ts
'
7 | );
8 | expect(shrinkLongPath('hello/asdfasdfasdfasdfasdfasdfa.ts')).toBe(
9 | '`...` / asdfasdfasdfasdfasdfasdfa.ts
'
10 | );
11 | });
12 |
13 | it('should not touch short paths', () => {
14 | expect(shrinkLongPath('hello/a.ts')).toBe('hello/a.ts');
15 | expect(shrinkLongPath('asdf/d.ts')).toBe('asdf/d.ts');
16 | });
17 |
18 | it('should not trim long paths without directory', () => {
19 | expect(shrinkLongPath('asdfasdfasdfasdfasdfasdfasdfasdfasdfa.ts')).toBe(
20 | 'asdfasdfasdfasdfasdfasdfasdfasdfasdfa.ts'
21 | );
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/tests/format/formatCoverage.test.ts:
--------------------------------------------------------------------------------
1 | import { formatCoverage } from '../../src/format/formatCoverage';
2 | import jsonReport from '../mock-data/jsonReport.json';
3 |
4 | describe('formatCoverage', () => {
5 | it('should format standard coverage', () => {
6 | expect(
7 | formatCoverage(jsonReport, jsonReport, 0.3, false)
8 | ).toMatchSnapshot();
9 | expect(
10 | formatCoverage(jsonReport, undefined, 0.3, false)
11 | ).toMatchSnapshot();
12 | expect(
13 | formatCoverage(jsonReport, undefined, undefined, false)
14 | ).toMatchSnapshot();
15 | });
16 |
17 | it('should display warning if hiding details', () => {
18 | expect(
19 | formatCoverage(jsonReport, jsonReport, 0.3, true)
20 | ).toMatchSnapshot();
21 | });
22 |
23 | it('should return empty string if no reports specified', () => {
24 | expect(formatCoverage(undefined, undefined, 0.3, false)).toBe('');
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/tests/format/formatErrors.test.ts:
--------------------------------------------------------------------------------
1 | import * as all from '@actions/github';
2 |
3 | import { formatErrors } from '../../src/format/formatErrors';
4 | import { ActionError } from '../../src/typings/ActionError';
5 | import { FailReason } from '../../src/typings/Report';
6 |
7 | const { mockContext, clearContextMock } = all as any;
8 |
9 | describe('formatErrors', () => {
10 | it('should return empty string, if no errors specified', () => {
11 | expect(formatErrors([], false, [])).toBe('');
12 | });
13 |
14 | it('should format 1 error', () => {
15 | expect(
16 | formatErrors([new ActionError(FailReason.TESTS_FAILED)], false, [])
17 | ).toMatchSnapshot();
18 |
19 | mockContext({
20 | payload: {},
21 | repo: {
22 | owner: 'bot',
23 | repo: 'test-repo',
24 | },
25 | runId: 10,
26 | });
27 |
28 | expect(
29 | formatErrors([new Error('This is unexpected error')], false, [])
30 | ).toMatchSnapshot();
31 |
32 | clearContextMock();
33 | });
34 |
35 | it('should format multiple errors', () => {
36 | expect(
37 | formatErrors(
38 | [
39 | new ActionError(FailReason.TESTS_FAILED),
40 | new Error('hello'),
41 | new ActionError(FailReason.TESTS_FAILED),
42 | new EvalError('aaa'),
43 | ],
44 | false,
45 | []
46 | )
47 | ).toMatchSnapshot();
48 |
49 | expect(
50 | formatErrors(
51 | new Array(40).fill(0).map(() => new Error('error')),
52 | false,
53 | []
54 | )
55 | ).toMatchSnapshot();
56 |
57 | expect(
58 | formatErrors(
59 | new Array(100).fill(0).map(() => new Error('error')),
60 | false,
61 | []
62 | )
63 | ).toMatchSnapshot();
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/tests/format/formatRunReport.test.ts:
--------------------------------------------------------------------------------
1 | import { formatRunReport } from '../../src/format/formatRunReport';
2 | import { getFailureDetails } from '../../src/format/getFailureDetails';
3 | import { JsonReport } from '../../src/typings/JsonReport';
4 |
5 | describe('formatRunReport', () => {
6 | it('should generate summary as markdown (successful tests)', () => {
7 | expect(
8 | formatRunReport({
9 | success: true,
10 | summary: '2 Tests Passed',
11 | title: 'Tests Passed',
12 | })
13 | ).toMatchSnapshot();
14 | });
15 | it('should generate summary as markdown (failure tests)', () => {
16 | expect(
17 | formatRunReport({
18 | success: false,
19 | summary: '2 Tests Passed, 1 Test Failed',
20 | title: 'Tests Failed',
21 | failures: getFailureDetails({
22 | testResults: [
23 | {
24 | message: 'fail 1',
25 | },
26 | {
27 | message: 'fail 2',
28 | },
29 | ],
30 | } as JsonReport),
31 | })
32 | ).toMatchSnapshot();
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/tests/format/getFailureDetails.test.ts:
--------------------------------------------------------------------------------
1 | import { getFailureDetails } from '../../src/format/getFailureDetails';
2 | import { JsonReport } from '../../src/typings/JsonReport';
3 |
4 | describe('getFormattedFailures', () => {
5 | it('should return empty string, if no test results (or all passed)', () => {
6 | expect(getFailureDetails(({} as unknown) as JsonReport)).toBe('');
7 |
8 | expect(
9 | getFailureDetails(({
10 | testResults: [],
11 | } as unknown) as JsonReport)
12 | ).toBe('');
13 |
14 | expect(
15 | getFailureDetails(({
16 | testResults: [
17 | {
18 | status: 'passed',
19 | message: '',
20 | },
21 | ],
22 | } as unknown) as JsonReport)
23 | ).toBe('');
24 | });
25 |
26 | it('should format failure details', () => {
27 | expect(
28 | getFailureDetails({
29 | testResults: [
30 | {
31 | status: 'passed',
32 | message: 'asdfasdfasdf',
33 | },
34 | {
35 | status: 'failed',
36 | message: 'This is simple error message',
37 | },
38 | {
39 | status: 'failed',
40 | message: '',
41 | },
42 | {
43 | status: 'failed',
44 | message: 'Another error message',
45 | },
46 | {
47 | status: 'passed',
48 | message: 'Ignored message, because test is passed',
49 | },
50 | ],
51 | } as JsonReport)
52 | ).toBe(
53 | '```\n' +
54 | 'This is simple error message\n' +
55 | '```\n' +
56 | '---\n' +
57 | '```\n' +
58 | 'Another error message\n' +
59 | '```'
60 | );
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/tests/format/getFormattedCoverage.test.ts:
--------------------------------------------------------------------------------
1 | import { parseDetails } from '../../src/format/details/parseDetails';
2 | import { getFormattedCoverage } from '../../src/format/getFormattedCoverage';
3 | import { parseSummary } from '../../src/format/summary/parseSummary';
4 | import jsonReport from '../mock-data/jsonReport.json';
5 |
6 | describe('getFormattedCoverage', () => {
7 | it('should match snapshots', () => {
8 | expect(
9 | getFormattedCoverage(
10 | parseSummary(jsonReport),
11 | undefined,
12 | parseDetails(jsonReport),
13 | undefined,
14 | undefined,
15 | false
16 | )
17 | ).toMatchSnapshot();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/tests/format/getPercents.test.ts:
--------------------------------------------------------------------------------
1 | import { getPercents } from '../../src/format/getPercents';
2 |
3 | describe('getPercents', () => {
4 | it('should return percents', () => {
5 | expect(getPercents(5, 10)).toBe(50);
6 | expect(getPercents(10, 10)).toBe(100);
7 | expect(getPercents(0, 10)).toBe(0);
8 | });
9 |
10 | it('should return 100 when total is 0', () => {
11 | expect(getPercents(0, 0)).toBe(100);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/tests/format/summary/__snapshots__/formatCoverageSummary.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`formatCoverageSummary should format summary when head and base coverage specified (with threshold) 1`] = `
4 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
5 | | :-: | :- | :- | :-: |
6 | | 🟡 | Statements | 76.67% (+26.67% 🔼)
| 23/30 |"
7 | `;
8 |
9 | exports[`formatCoverageSummary should format summary when head and base coverage specified (with threshold) 2`] = `
10 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
11 | | :-: | :- | :- | :-: |
12 | | 🔴 | Statements | 76.67% (-20% 🔻)
| 23/30 |"
13 | `;
14 |
15 | exports[`formatCoverageSummary should format summary when head and base coverage specified (with threshold) 3`] = `
16 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
17 | | :-: | :- | :- | :-: |
18 | | 🔴 | Statements | 76.67% | 23/30 |"
19 | `;
20 |
21 | exports[`formatCoverageSummary should format summary when head and base coverage specified (with threshold) 4`] = `
22 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
23 | | :-: | :- | :- | :-: |
24 | | 🟢 | Statements | 96.67% | 29/30 |"
25 | `;
26 |
27 | exports[`formatCoverageSummary should format summary when head and base coverage specified 1`] = `
28 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
29 | | :-: | :- | :- | :-: |
30 | | 🟡 | Statements | 76.67% (+26.67% 🔼)
| 23/30 |"
31 | `;
32 |
33 | exports[`formatCoverageSummary should format summary when head and base coverage specified 2`] = `
34 | "| St.:grey_question:
| Category | Percentage | Covered / Total |
35 | | :-: | :- | :- | :-: |
36 | | 🟡 | Statements | 76.67% (-20% 🔻)
| 23/30 |"
37 | `;
38 |
--------------------------------------------------------------------------------
/tests/format/summary/__snapshots__/parseSummary.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`parseSummary should parse summary from mock data 1`] = `
4 | Array [
5 | Object {
6 | "covered": 27,
7 | "percentage": 81.81818181818183,
8 | "title": "Statements",
9 | "total": 33,
10 | },
11 | Object {
12 | "covered": 8,
13 | "percentage": 100,
14 | "title": "Branches",
15 | "total": 8,
16 | },
17 | Object {
18 | "covered": 7,
19 | "percentage": 63.63636363636363,
20 | "title": "Functions",
21 | "total": 11,
22 | },
23 | Object {
24 | "covered": 25,
25 | "percentage": 80.64516129032258,
26 | "title": "Lines",
27 | "total": 31,
28 | },
29 | ]
30 | `;
31 |
--------------------------------------------------------------------------------
/tests/format/summary/getSummary.test.ts:
--------------------------------------------------------------------------------
1 | import { getSummary } from '../../../src/format/summary/getSummary';
2 | import { coverageMap } from '../../mock-data/jsonReport.json';
3 |
4 | describe('getSummary', () => {
5 | it('should calculate summary', () => {
6 | const counter = jest.fn(() => 1);
7 |
8 | expect(
9 | getSummary(coverageMap, counter, counter, 'Title')
10 | ).toStrictEqual({
11 | title: 'Title',
12 | total: 6,
13 | covered: 6,
14 | percentage: 100,
15 | });
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/tests/format/summary/parseSummary.test.ts:
--------------------------------------------------------------------------------
1 | import { parseSummary } from '../../../src/format/summary/parseSummary';
2 | import report from '../../mock-data/jsonReport.json';
3 |
4 | describe('parseSummary', () => {
5 | it('should parse summary from mock data', () => {
6 | expect(parseSummary(report)).toMatchSnapshot();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/tests/report/generateCommitReport.test.ts:
--------------------------------------------------------------------------------
1 | import type { getOctokit } from '@actions/github';
2 | import * as all from '@actions/github';
3 |
4 | import { generateCommitReport } from '../../src/report/generateCommitReport';
5 |
6 | const { mockContext, clearContextMock } = all as any;
7 |
8 | describe('generateCommitReport', () => {
9 | it('should generate commit report', async () => {
10 | const createCommitComment = jest.fn();
11 |
12 | mockContext({
13 | sha: '123456',
14 | });
15 |
16 | await generateCommitReport(
17 | 'Report body',
18 | {
19 | owner: 'bot',
20 | repo: 'test-repository',
21 | },
22 | ({
23 | rest: {
24 | repos: {
25 | createCommitComment,
26 | },
27 | },
28 | } as unknown) as ReturnType
29 | );
30 |
31 | expect(createCommitComment).toBeCalledWith({
32 | owner: 'bot',
33 | repo: 'test-repository',
34 | body: 'Report body',
35 | commit_sha: '123456',
36 | });
37 |
38 | clearContextMock();
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/tests/stages/collectCoverage.test.ts:
--------------------------------------------------------------------------------
1 | import { sep } from 'path';
2 |
3 | import { readFile } from 'fs-extra';
4 |
5 | import { collectCoverage } from '../../src/stages/collectCoverage';
6 | import { ActionError } from '../../src/typings/ActionError';
7 | import { FailReason } from '../../src/typings/Report';
8 | import { createDataCollector } from '../../src/utils/DataCollector';
9 |
10 | const clearMocks = () => {
11 | (readFile as jest.Mock).mockClear();
12 | };
13 |
14 | beforeEach(clearMocks);
15 |
16 | describe('collectCoverage', () => {
17 | it('should read report.json by default', async () => {
18 | const dataCollector = createDataCollector();
19 |
20 | (readFile as jest.Mock).mockImplementationOnce(() => 'Value');
21 |
22 | await expect(collectCoverage(dataCollector)).resolves.toBe('Value');
23 | expect(readFile).toBeCalledWith('report.json');
24 | });
25 |
26 | it('should read report.json from correct path when working directory is provided', async () => {
27 | const dataCollector = createDataCollector();
28 |
29 | (readFile as jest.Mock).mockImplementationOnce(
30 | () => 'New value'
31 | );
32 |
33 | await expect(
34 | collectCoverage(dataCollector, 'customFolder')
35 | ).resolves.toBe('New value');
36 | expect(readFile).toBeCalledWith(`customFolder${sep}report.json`);
37 | });
38 |
39 | it('should read report from correct path when working directory and custom report path is provided', async () => {
40 | const dataCollector = createDataCollector();
41 |
42 | (readFile as jest.Mock).mockImplementationOnce(
43 | () => 'New value'
44 | );
45 |
46 | await expect(
47 | collectCoverage(
48 | dataCollector,
49 | 'customFolder',
50 | './customReport.json'
51 | )
52 | ).resolves.toBe('New value');
53 | expect(readFile).toBeCalledWith(`customFolder${sep}customReport.json`);
54 | });
55 |
56 | it('should throw error if report not found', async () => {
57 | const dataCollector = createDataCollector();
58 |
59 | (readFile as jest.Mock).mockImplementationOnce(() => {
60 | throw {
61 | code: 'ENOENT',
62 | };
63 | });
64 |
65 | await expect(collectCoverage(dataCollector)).rejects.toStrictEqual(
66 | new ActionError(FailReason.REPORT_NOT_FOUND, {
67 | coveragePath: 'report.json',
68 | })
69 | );
70 | });
71 |
72 | it('should throw unknown error', async () => {
73 | const dataCollector = createDataCollector();
74 |
75 | (readFile as jest.Mock).mockImplementationOnce(() => {
76 | throw new Error('Custom error');
77 | });
78 |
79 | await expect(collectCoverage(dataCollector)).rejects.not.toStrictEqual(
80 | new ActionError(FailReason.REPORT_NOT_FOUND, {
81 | coveragePath: 'report.json',
82 | })
83 | );
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/tests/stages/installDependencies.test.ts:
--------------------------------------------------------------------------------
1 | import { sep } from 'path';
2 |
3 | import { exec } from '@actions/exec';
4 | import { mocked } from 'ts-jest/utils';
5 |
6 | import { installDependencies } from '../../src/stages/installDependencies';
7 | import { removeDirectory } from '../../src/utils/removeDirectory';
8 |
9 | jest.mock('../../src/utils/removeDirectory');
10 |
11 | const clearMocks = () => {
12 | mocked(exec).mockClear();
13 | mocked(removeDirectory).mockClear();
14 | };
15 |
16 | beforeEach(clearMocks);
17 |
18 | describe('installDependencies', () => {
19 | it('should remove "node_modules" directory', async () => {
20 | await installDependencies();
21 |
22 | expect(removeDirectory).toBeCalledWith('node_modules');
23 | });
24 |
25 | it('should remove "node_modules" directory, which is under specified working directory', async () => {
26 | await installDependencies(undefined, 'workingDir');
27 |
28 | expect(removeDirectory).toBeCalledWith(`workingDir${sep}node_modules`);
29 | });
30 |
31 | it('should install dependencies', async () => {
32 | await installDependencies();
33 |
34 | expect(exec).toBeCalledWith('npm install', undefined, {
35 | cwd: undefined,
36 | });
37 | });
38 |
39 | it('should install dependencies using npm', async () => {
40 | await installDependencies('npm');
41 |
42 | expect(exec).toBeCalledWith('npm install', undefined, {
43 | cwd: undefined,
44 | });
45 | });
46 |
47 | it('should install dependencies using yarn', async () => {
48 | await installDependencies('yarn');
49 |
50 | expect(exec).toBeCalledWith('yarn install', undefined, {
51 | cwd: undefined,
52 | });
53 | });
54 |
55 | it('should install dependencies using pnpm', async () => {
56 | await installDependencies('pnpm');
57 |
58 | expect(exec).toBeCalledWith('pnpm install', undefined, {
59 | cwd: undefined,
60 | });
61 | });
62 |
63 | it('should install dependencies using bun', async () => {
64 | await installDependencies('bun');
65 |
66 | expect(exec).toBeCalledWith('bun install', undefined, {
67 | cwd: undefined,
68 | });
69 | });
70 |
71 | it('should install dependencies under specified working directory', async () => {
72 | await installDependencies(undefined, 'workingDir');
73 |
74 | expect(exec).toBeCalledWith('npm install', undefined, {
75 | cwd: 'workingDir',
76 | });
77 | });
78 |
79 | it("shouldn't install dependencies, if node_modules directory deletion failed", async () => {
80 | try {
81 | mocked(removeDirectory).mockImplementationOnce(() => {
82 | throw 0;
83 | });
84 | await installDependencies();
85 | } catch {
86 | /** ignore error */
87 | }
88 |
89 | expect(exec).not.toBeCalled();
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/tests/stages/parseCoverage.test.ts:
--------------------------------------------------------------------------------
1 | import { parseCoverage } from '../../src/stages/parseCoverage';
2 | import { ActionError } from '../../src/typings/ActionError';
3 | import { FailReason } from '../../src/typings/Report';
4 |
5 | describe('parseCoverage', () => {
6 | it('should parse valid JSON', () => {
7 | expect(parseCoverage('{ "data": "I\'m valid JSON!" }')).toStrictEqual({
8 | data: "I'm valid JSON!",
9 | });
10 | });
11 |
12 | it('should throw error if JSON is not valid', () => {
13 | expect(() => parseCoverage('not valid json')).toThrowError(
14 | new ActionError(FailReason.INVALID_COVERAGE_FORMAT)
15 | );
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/tests/stages/runTest.test.ts:
--------------------------------------------------------------------------------
1 | import { exec } from '@actions/exec';
2 |
3 | import { runTest } from '../../src/stages/runTest';
4 |
5 | const clearMocks = () => {
6 | (exec as jest.Mock).mockClear();
7 | };
8 |
9 | beforeEach(clearMocks);
10 |
11 | describe('runTest', () => {
12 | it('should run test script', async () => {
13 | await runTest('npm run test');
14 |
15 | expect(exec).toBeCalledWith(
16 | 'npm run test -- --ci --json --coverage --testLocationInResults --outputFile="report.json"',
17 | [],
18 | {
19 | cwd: undefined,
20 | }
21 | );
22 | });
23 |
24 | it('should run test script in custom working directory', async () => {
25 | await runTest('npm run test', 'custom cwd');
26 |
27 | expect(exec).toBeCalledWith(expect.any(String), [], {
28 | cwd: 'custom cwd',
29 | });
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/tests/typings/Report.test.ts:
--------------------------------------------------------------------------------
1 | import { FailReason } from '../../src/typings/Report';
2 |
3 | describe('FailReason', () => {
4 | it('should be defined', () => {
5 | expect(FailReason).toBeDefined();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/tests/utils/DataCollector.test.ts:
--------------------------------------------------------------------------------
1 | import * as core from '@actions/core';
2 |
3 | import { ActionError } from '../../src/typings/ActionError';
4 | import { FailReason } from '../../src/typings/Report';
5 | import { createDataCollector } from '../../src/utils/DataCollector';
6 |
7 | describe('DataCollector', () => {
8 | beforeEach(() => {
9 | (core.error as jest.Mock).mockClear();
10 | (core.info as jest.Mock).mockClear();
11 | });
12 |
13 | it('should collect data', () => {
14 | const dataCollector = createDataCollector();
15 |
16 | dataCollector.add('hello');
17 | dataCollector.add('world');
18 | dataCollector.add('this');
19 |
20 | expect(core.error).not.toHaveBeenCalled();
21 | expect(core.info).not.toHaveBeenCalled();
22 |
23 | expect(dataCollector.get().data).toStrictEqual([
24 | 'hello',
25 | 'world',
26 | 'this',
27 | ]);
28 | });
29 |
30 | it('should collect errors', () => {
31 | const dataCollector = createDataCollector();
32 |
33 | dataCollector.error(new ActionError(FailReason.TESTS_FAILED));
34 | dataCollector.error(new Error('world'));
35 | dataCollector.error(
36 | new ActionError(FailReason.REPORT_NOT_FOUND, {
37 | coveragePath: 'somepath',
38 | })
39 | );
40 |
41 | expect(core.error).toHaveBeenCalledTimes(3);
42 | expect(core.info).not.toHaveBeenCalled();
43 |
44 | expect(dataCollector.get().errors).toStrictEqual([
45 | new ActionError(FailReason.TESTS_FAILED),
46 | new Error('world'),
47 | new ActionError(FailReason.REPORT_NOT_FOUND, {
48 | coveragePath: 'somepath',
49 | }),
50 | ]);
51 | });
52 |
53 | it('should collect info', () => {
54 | const dataCollector = createDataCollector();
55 |
56 | dataCollector.info('hello');
57 | dataCollector.info('world');
58 | dataCollector.info('this');
59 |
60 | expect(core.error).not.toHaveBeenCalled();
61 | expect(core.info).toHaveBeenCalledTimes(3);
62 |
63 | expect(dataCollector.get().messages).toStrictEqual([
64 | 'hello',
65 | 'world',
66 | 'this',
67 | ]);
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/tests/utils/createMarkdownSpoiler.test.ts:
--------------------------------------------------------------------------------
1 | import { createMarkdownSpoiler } from '../../src/utils/createMarkdownSpoiler';
2 |
3 | describe('createMarkdownSpoiler', () => {
4 | it('should create markdown spoiler', () => {
5 | expect(
6 | createMarkdownSpoiler({
7 | body: 'This is body',
8 | summary: 'This is summary',
9 | })
10 | ).toBe(`
11 | This is summary
12 |
13 |
14 | This is body
15 |
16 |
17 | `);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/tests/utils/decimalToString.test.ts:
--------------------------------------------------------------------------------
1 | import { decimalToString } from '../../src/utils/decimalToString';
2 |
3 | describe('decimalToString', () => {
4 | it('should convert integers to string', () => {
5 | expect(decimalToString(5)).toBe('5');
6 | expect(decimalToString(0)).toBe('0');
7 | expect(decimalToString(0.0)).toBe('0');
8 | expect(decimalToString(1000)).toBe('1000');
9 | });
10 |
11 | it('should convert decimals to string', () => {
12 | expect(decimalToString(1.15)).toBe('1.15');
13 | expect(decimalToString(4.01)).toBe('4.01');
14 | expect(decimalToString(4.1)).toBe('4.1');
15 | expect(decimalToString(1 / 3)).toBe('0.33');
16 | expect(decimalToString(2 / 3)).toBe('0.67');
17 | });
18 |
19 | it('should convert decimals to string (custom digits after dot)', () => {
20 | expect(decimalToString(1.15, 1)).toBe('1.1');
21 | expect(decimalToString(4.01, 3)).toBe('4.01');
22 | expect(decimalToString(4.1, 0)).toBe('4');
23 | expect(decimalToString(1 / 3, 4)).toBe('0.3333');
24 | expect(decimalToString(2 / 3, 5)).toBe('0.66667');
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/tests/utils/formatPercentage.test.ts:
--------------------------------------------------------------------------------
1 | import { formatPercentage } from '../../src/utils/formatPercentage';
2 |
3 | describe('formatPercentage', () => {
4 | it('only head coverage provided', () => {
5 | expect(formatPercentage(10)).toBe('10%');
6 | expect(formatPercentage(60.01)).toBe('60.01%');
7 | expect(formatPercentage(1 / 3)).toBe('0.33%');
8 | });
9 |
10 | it('coverage reduced', () => {
11 | expect(formatPercentage(10, 20)).toBe(
12 | '10% (-10% 🔻)
'
13 | );
14 | expect(formatPercentage(1.15, 20.15)).toBe(
15 | '1.15% (-19% 🔻)
'
16 | );
17 | expect(formatPercentage(1.15, 20)).toBe(
18 | '1.15% (-18.85% 🔻)
'
19 | );
20 | expect(formatPercentage(1.15, 20.1)).toBe(
21 | '1.15% (-18.95% 🔻)
'
22 | );
23 | });
24 |
25 | it('coverage increased', () => {
26 | expect(formatPercentage(20, 10)).toBe(
27 | '20% (+10% 🔼)
'
28 | );
29 | expect(formatPercentage(20.15, 1.15)).toBe(
30 | '20.15% (+19% 🔼)
'
31 | );
32 | expect(formatPercentage(20, 1.15)).toBe(
33 | '20% (+18.85% 🔼)
'
34 | );
35 | expect(formatPercentage(20.1, 1.15)).toBe(
36 | '20.1% (+18.95% 🔼)
'
37 | );
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/tests/utils/formatPercentageDelta.test.ts:
--------------------------------------------------------------------------------
1 | import { formatPercentageDelta } from '../../src/utils/formatPercentageDelta';
2 |
3 | describe('formatPercentageDelta', () => {
4 | it('negative delta', () => {
5 | expect(formatPercentageDelta(-1)).toBe('(-1% 🔻)');
6 | expect(formatPercentageDelta(-1.00001)).toBe('(-1% 🔻)');
7 | expect(formatPercentageDelta(-1 / 3)).toBe('(-0.33% 🔻)');
8 | expect(formatPercentageDelta(-52.11)).toBe('(-52.11% 🔻)');
9 | });
10 |
11 | it('positive delta', () => {
12 | expect(formatPercentageDelta(1)).toBe('(+1% 🔼)');
13 | expect(formatPercentageDelta(1.00001)).toBe('(+1% 🔼)');
14 | expect(formatPercentageDelta(1 / 3)).toBe('(+0.33% 🔼)');
15 | expect(formatPercentageDelta(52.11)).toBe('(+52.11% 🔼)');
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/tests/utils/getConsoleLink.test.ts:
--------------------------------------------------------------------------------
1 | import * as all from '@actions/github';
2 |
3 | import { getConsoleLink } from '../../src/utils/getConsoleLink';
4 |
5 | const { mockContext, clearContextMock } = all as any;
6 |
7 | describe('getConsoleLink', () => {
8 | it('should return link (from payload)', () => {
9 | mockContext({
10 | payload: {
11 | repository: {
12 | html_url: 'https://github.com/test/test-repo',
13 | },
14 | },
15 | runId: 111,
16 | });
17 | expect(getConsoleLink()).toBe(
18 | 'https://github.com/test/test-repo/actions/runs/111'
19 | );
20 | clearContextMock();
21 | });
22 |
23 | it('should return link (built from context)', () => {
24 | mockContext({
25 | payload: {},
26 | repo: {
27 | owner: 'test',
28 | repo: 'test-repo',
29 | },
30 | runId: 111,
31 | });
32 | expect(getConsoleLink()).toBe(
33 | 'https://github.com/test/test-repo/actions/runs/111'
34 | );
35 | clearContextMock();
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/tests/utils/getStatusOfPercents.test.ts:
--------------------------------------------------------------------------------
1 | import { getStatusOfPercents } from '../../src/utils/getStatusOfPercents';
2 |
3 | describe('getStatusOfPercents', () => {
4 | it('should handle threshold by default', () => {
5 | expect(getStatusOfPercents(10)).toBe('🔴');
6 | expect(getStatusOfPercents(33)).toBe('🔴');
7 | expect(getStatusOfPercents(59.99)).toBe('🔴');
8 | expect(getStatusOfPercents(60)).toBe('🟡');
9 | expect(getStatusOfPercents(73.33333)).toBe('🟡');
10 | expect(getStatusOfPercents(79.99)).toBe('🟡');
11 | expect(getStatusOfPercents(80)).toBe('🟢');
12 | expect(getStatusOfPercents(85.15)).toBe('🟢');
13 | expect(getStatusOfPercents(100)).toBe('🟢');
14 | expect(getStatusOfPercents(120)).toBe('🟢');
15 | });
16 |
17 | it('should handle custom threshold', () => {
18 | expect(getStatusOfPercents(10, 80)).toBe('🔴');
19 | expect(getStatusOfPercents(33, 80)).toBe('🔴');
20 | expect(getStatusOfPercents(79.99, 80)).toBe('🔴');
21 | expect(getStatusOfPercents(80, 80)).toBe('🟡');
22 | expect(getStatusOfPercents(85.334534, 80)).toBe('🟡');
23 | expect(getStatusOfPercents(89.999, 80)).toBe('🟡');
24 | expect(getStatusOfPercents(90, 80)).toBe('🟢');
25 | expect(getStatusOfPercents(93.33, 80)).toBe('🟢');
26 | expect(getStatusOfPercents(100, 80)).toBe('🟢');
27 | expect(getStatusOfPercents(120, 80)).toBe('🟢');
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/tests/utils/i18n.test.ts:
--------------------------------------------------------------------------------
1 | import { i18n } from '../../src/utils/i18n';
2 |
3 | describe('i18n', () => {
4 | it('should insert arguments & icons', () => {
5 | expect(i18n('{{ a }}', { a: 'hello' })).toBe('hello');
6 | expect(i18n(':x: {{ a }}', { a: 'hello' })).toBe('❌ hello');
7 | expect(i18n(':not_specified_icon:')).toBe(':not_specified_icon:');
8 | });
9 |
10 | it('should read string from strings.json', () => {
11 | expect(i18n('status')).toBe('St.');
12 | expect(i18n('errors.multiple')).toBe('Multiple errors occurred');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/tests/utils/insertArgs.test.ts:
--------------------------------------------------------------------------------
1 | import { insertArgs } from '../../src/utils/insertArgs';
2 |
3 | describe('insertArgs', () => {
4 | it('should insert arguments', () => {
5 | expect(insertArgs('{{ arg }}', { arg: 'hello' })).toBe('hello');
6 | expect(insertArgs('Hello, {{ name }}!', { name: 'world' })).toBe(
7 | 'Hello, world!'
8 | );
9 | expect(
10 | insertArgs('First: {{ arg1 }}, second: {{ arg2 }}', {
11 | arg1: 'hello',
12 | arg2: 2,
13 | })
14 | ).toBe('First: hello, second: 2');
15 | });
16 |
17 | it('should skip arguments, which are not provided', () => {
18 | expect(insertArgs('{{ notGivenArgument }}', {})).toBe(
19 | '{{ notGivenArgument }}'
20 | );
21 | expect(
22 | insertArgs('{{ given }}, {{ notGivenArgument }}', { given: 'a' })
23 | ).toBe('a, {{ notGivenArgument }}');
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/tests/utils/joinPaths.test.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | import { joinPaths } from '../../src/utils/joinPaths';
4 |
5 | describe('joinPaths', () => {
6 | it('should filter undefined paths', () => {
7 | expect(
8 | joinPaths('hello', undefined, 'a', undefined, undefined, 'b')
9 | ).toBe(join('hello', 'a', 'b'));
10 |
11 | expect(joinPaths('asdf', undefined, 'bcc')).toBe(join('asdf', 'bcc'));
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/tests/utils/removeDirectory.test.ts:
--------------------------------------------------------------------------------
1 | import { rm, rmdir } from 'fs-extra';
2 | import { mocked } from 'ts-jest/utils';
3 |
4 | import { removeDirectory } from '../../src/utils/removeDirectory';
5 |
6 | const originalProcess = process;
7 |
8 | beforeEach(() => {
9 | mocked(rm).mockClear();
10 | mocked(rmdir).mockClear();
11 | });
12 |
13 | afterEach(() => {
14 | global.process = originalProcess;
15 | });
16 |
17 | describe('removeDirectory', () => {
18 | it('should call rmdir when node version is < 14.14.0', async () => {
19 | global.process = {
20 | ...originalProcess,
21 | version: 'v14.3.9',
22 | };
23 | await removeDirectory('asdf');
24 | expect(rm).not.toBeCalled();
25 | expect(rmdir).toBeCalled();
26 | });
27 |
28 | it('should call rm when node version is >=14.14.0', async () => {
29 | global.process = {
30 | ...originalProcess,
31 | version: 'v14.14.0',
32 | };
33 | await removeDirectory('asdf');
34 | expect(rm).toBeCalled();
35 | expect(rmdir).not.toBeCalled();
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/tests/utils/runStage.test.ts:
--------------------------------------------------------------------------------
1 | import { createDataCollector } from '../../src/utils/DataCollector';
2 | import { runStage } from '../../src/utils/runStage';
3 |
4 | describe('runStage', () => {
5 | it('should run successfully', async () => {
6 | const dataCollector = createDataCollector();
7 | const [succeed, result] = await runStage(
8 | 'initialize',
9 | dataCollector,
10 | () => {
11 | return Promise.resolve('some result');
12 | }
13 | );
14 |
15 | expect(succeed).toBeTruthy();
16 | expect(result).toBe('some result');
17 | expect(dataCollector.get().messages).toStrictEqual([
18 | 'Begin initialization stage...',
19 | 'Initialization stage ended',
20 | ]);
21 | });
22 |
23 | it('should skip', async () => {
24 | const dataCollector = createDataCollector();
25 | const [succeed, result] = await runStage(
26 | 'initialize',
27 | dataCollector,
28 | (skip) => {
29 | skip();
30 | }
31 | );
32 |
33 | expect(succeed).toBeFalsy();
34 | expect(result).toBe(undefined);
35 | expect(dataCollector.get().messages).toStrictEqual([
36 | 'Begin initialization stage...',
37 | 'Initialization stage skipped',
38 | 'Initialization stage ended',
39 | ]);
40 | });
41 |
42 | it('should fail', async () => {
43 | const dataCollector = createDataCollector();
44 | const [succeed, result] = await runStage(
45 | 'initialize',
46 | dataCollector,
47 | () => {
48 | return Promise.reject('New error');
49 | }
50 | );
51 |
52 | expect(succeed).toBeFalsy();
53 | expect(result).toBe(undefined);
54 | expect(dataCollector.get().messages).toStrictEqual([
55 | 'Begin initialization stage...',
56 | 'Initialization stage failed',
57 | 'Initialization stage ended',
58 | ]);
59 | expect(dataCollector.get().errors).toStrictEqual(['New error']);
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/tests/utils/withExplanation.test.ts:
--------------------------------------------------------------------------------
1 | import { withExplanation } from '../../src/utils/withExplanation';
2 |
3 | describe('withExplanation', () => {
4 | it('should add explanation', () => {
5 | expect(withExplanation('test', 'This is simple explanation')).toBe(
6 | 'test:grey_question:
'
7 | );
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2019",
4 | "module": "commonjs",
5 | "declaration": false,
6 | "outDir": "./dist",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "lib": ["ES2019", "DOM" /* dirty hack, otherwise typescript doesn't find URL */],
10 | "rootDir": "src",
11 | "resolveJsonModule": true
12 | },
13 | "include": ["src", "index.d.ts"],
14 | "exclude": ["node_modules", "tests", "example"]
15 | }
16 |
--------------------------------------------------------------------------------