Skip to content

Commit

Permalink
feat: Add trend indicator (#323)
Browse files Browse the repository at this point in the history
* refactor: Adds folders for more clarity

* feat: Adds feature to show trend indicator by comparing json summaries

* feat: Adjusts target in report

* chore: adjusts output design and adds test export

* tests: Fixes tests

* docs: Includes docs for the feature
  • Loading branch information
davelosert authored Feb 26, 2024
1 parent c624e06 commit 698d54e
Show file tree
Hide file tree
Showing 27 changed files with 474 additions and 110 deletions.
82 changes: 73 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,16 @@ This action requires the `pull-request: write` permission to add a comment to yo

### Options

| Option | Description | Default |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `working-directory` | The main path to search for coverage- and configuration files (adjusting this is especially useful in monorepos). | `./` |
| `json-summary-path` | The path to the json summary file. | `${working-directory}/coverage/coverage-summary.json` |
| `json-final-path` | The path to the json final file. | `${working-directory}/coverage/coverage-final.json` |
| `vite-config-path` | The path to the vite config file. Will check the same paths as vite and vitest | Checks pattern `${working-directory}/vite[st].config.{t\|mt\|ct\|j\|mj\|cj}s` |
| `github-token` | A GitHub access token with permissions to write to issues (defaults to `secrets.GITHUB_TOKEN`). | `${{ github.token }}` |
| `file-coverage-mode` | Defines how file-based coverage is reported. Possible values are `all`, `changes` or `none`. | `changes` |
| `name` | Give the report a custom name. This is useful if you want multiple reports for different test suites within the same PR. Needs to be unique. | '' |
| Option | Description | Default |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `working-directory` | The main path to search for coverage- and configuration files (adjusting this is especially useful in monorepos). | `./` |
| `json-summary-path` | The path to the json summary file. | `${working-directory}/coverage/coverage-summary.json` |
| `json-final-path` | The path to the json final file. | `${working-directory}/coverage/coverage-final.json` |
| `vite-config-path` | The path to the vite config file. Will check the same paths as vite and vitest | Checks pattern `${working-directory}/vite[st].config.{t\|mt\|ct\|j\|mj\|cj}s` |
| `github-token` | A GitHub access token with permissions to write to issues (defaults to `secrets.GITHUB_TOKEN`). | `${{ github.token }}` |
| `file-coverage-mode` | Defines how file-based coverage is reported. Possible values are `all`, `changes` or `none`. | `changes` |
| `name` | Give the report a custom name. This is useful if you want multiple reports for different test suites within the same PR. Needs to be unique. | '' |
| `json-summary-compare-path` | The path to the json summary file to compare against. If given, will display a trend indicator and the difference in the summary. Respects the `working-directory` option. | undefined |

#### File Coverage Mode

Expand Down Expand Up @@ -142,6 +143,69 @@ With the above configuration, the report would appear as follows:

If no thresholds are defined, the status will display as '🔵'.

### Coverage Trend Indicator

By using the `json-summary-compare-path` option, the action will display both a trend indicator and the coverage difference in the summary. This feature is particularly useful for tracking changes between the main branch and a previous run.

![Screenshot of the action-result showcasing the trend indicator](./docs/coverage-report-trend-indicator.png)

The most straightforward method to obtain the comparison file within a pull request is to run the tests and generate the coverage for the target branch within a matrix job:

```yml
name: "Test"
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
branch:
- ${{ github.head_ref }}
- "main"
permissions:
# Required to checkout the code
contents: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ matrix.branch }}
- name: "Install Node"
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: "Install Deps"
run: npm install
- name: "Test"
run: npx vitest --coverage.enabled true
- name: "Upload Coverage"
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.branch }}
path: coverage
report-coverage:
needs: test
runs-on: ubuntu-latest
steps:
- name: "Download Coverage Artifacts"
uses: actions/download-artifact@v4
with:
name: coverage-${{ github.head_ref }}
path: coverage
- uses: actions/download-artifact@v4
with:
name: coverage-main
path: coverage-main
- name: "Report Coverage"
uses: davelosert/vitest-coverage-report-action@v2
with:
json-summary-compare-path: coverage-main/coverage-summary.json
```

### Workspaces

If you're using a monorepo with [Vitest Workspaces](https://vitest.dev/guide/workspace.html) and running Vitest from your project's root, Vitest will disregard the `coverage` property in individual project-level Vite configuration files. This is because some [configuration options](https://vitest.dev/guide/workspace.html#configuration), such as coverage, apply to the entire workspace and are not allowed in a project config.
Expand Down
9 changes: 6 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ inputs:
required: false
description: 'The path to the json summary file. Uses "coverage/coverage-summary.json" by default.'
default: coverage/coverage-summary.json
file-coverage-mode:
json-summary-compare-path:
required: false
description: 'How to show summary for files coverage. Uses "changes" by default.'
default: changes
description: 'The path to the json summary file of the previous run to get trend indicators.'
json-final-path:
required: false
description: 'The path to the json final file. Uses "coverage/coverage-final.json" by default.'
default: coverage/coverage-final.json
file-coverage-mode:
required: false
description: 'How to show summary for files coverage. Uses "changes" by default.'
default: changes
working-directory:
required: false
description: 'Custom working directory'
Expand Down
Binary file added docs/coverage-report-trend-indicator.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"test": "vitest",
"test:coverage": "vitest run --coverage.enabled true",
"test:export": "npx ts-node test/exportTestTable.ts",
"build": "rm -rf dist && node esbuild.cjs",
"start": "npm run build && node dist/index.js",
"typecheck": "npx tsc --noEmit"
Expand Down
56 changes: 0 additions & 56 deletions src/generateSummaryTableHtml.ts

This file was deleted.

4 changes: 4 additions & 0 deletions src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ const icons = {
red: '🔴',
green: '🟢',
blue: '🔵',
increase: '⬆️',
decrease: '⬇️',
equal: '🟰',
target: '🎯'
}

export {
Expand Down
25 changes: 16 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import { generateSummaryTableHtml } from './generateSummaryTableHtml.js';
import { parseVitestJsonFinal, parseVitestJsonSummary } from './parseJsonReports.js';
import { FileCoverageMode } from './inputs/FileCoverageMode.js'
import { generateFileCoverageHtml } from './report/generateFileCoverageHtml.js';
import { generateHeadline } from './report/generateHeadline.js';
import { generateSummaryTableHtml } from './report/generateSummaryTableHtml.js';
import { getPullChanges } from './inputs/getPullChanges.js';
import { parseVitestJsonFinal, parseVitestJsonSummary } from './inputs/parseJsonReports.js';
import { readOptions } from './inputs/options.js';
import { RequestError } from '@octokit/request-error'
import { writeSummaryToPR } from './writeSummaryToPR.js';
import * as core from '@actions/core';
import * as github from '@actions/github';
import { RequestError } from '@octokit/request-error'
import { generateFileCoverageHtml } from './generateFileCoverageHtml.js';
import { getPullChanges } from './getPullChanges.js';
import { FileCoverageMode } from './FileCoverageMode.js'
import { readOptions } from './options.js';
import { generateHeadline } from './generateHeadline.js';

const run = async () => {
const {
fileCoverageMode,
jsonFinalPath,
jsonSummaryPath,
jsonSummaryComparePath,
name,
thresholds,
workingDirectory
} = await readOptions();

const jsonSummary = await parseVitestJsonSummary(jsonSummaryPath);
const tableData = generateSummaryTableHtml(jsonSummary.total, thresholds);

let jsonSummaryCompare;
if(jsonSummaryComparePath) {
jsonSummaryCompare = await parseVitestJsonSummary(jsonSummaryComparePath);
}

const tableData = generateSummaryTableHtml(jsonSummary.total, thresholds, jsonSummaryCompare?.total);
const summary = core.summary
.addHeading(generateHeadline({ workingDirectory, name }), 2)
.addRaw(tableData)
Expand Down
File renamed without changes.
File renamed without changes.
1 change: 0 additions & 1 deletion src/getPullChanges.ts → src/inputs/getPullChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,3 @@ export async function getPullChanges(fileCoverageMode: FileCoverageMode): Promis
core.endGroup()
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe("getViteConfigPath", () => {
const mockWorkingDirectory = path.resolve(
__dirname,
"..",
'..',
"test",
"mockConfig"
);
Expand Down
File renamed without changes.
10 changes: 9 additions & 1 deletion src/options.ts → src/inputs/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ async function readOptions() {
const jsonSummaryPath = path.resolve(workingDirectory, core.getInput('json-summary-path'));
const jsonFinalPath = path.resolve(workingDirectory, core.getInput('json-final-path'));


const jsonSummaryCompareInput = core.getInput('json-summary-compare-path');
let jsonSummaryComparePath;
if(jsonSummaryCompareInput) {
jsonSummaryComparePath = path.resolve(workingDirectory, jsonSummaryCompareInput);
}

const name = core.getInput('name');

// ViteConfig is optional, as it is only required for thresholds. If no vite config is provided, we will not include thresholds in the final report.
Expand All @@ -24,9 +31,10 @@ async function readOptions() {
fileCoverageMode,
jsonFinalPath,
jsonSummaryPath,
jsonSummaryComparePath,
name,
thresholds,
workingDirectory
workingDirectory,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ vi.mock('@actions/core', async (importOriginal) => ({
}));

describe('generateTableData', () => {
const mockConfigPath = path.resolve(__dirname, '..', 'test', 'mockConfig');
const mockConfigPath = path.resolve(__dirname, '..', '..', 'test', 'mockConfig');
const getConfig = (configName: string) => path.resolve(mockConfigPath, configName)

it('returns no thresholds if config file can not be found.', async (): Promise<void> => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'node:path';
import * as core from '@actions/core';
import { promises as fs } from 'fs';
import { Thresholds } from './types/Threshold';
import { Thresholds } from '../types/Threshold';

const regex100 = /100"?\s*:\s*true/;
const regexStatements = /statements\s*:\s*(\d+)/;
Expand Down
4 changes: 2 additions & 2 deletions src/parseJsonReports.ts → src/inputs/parseJsonReports.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { readFile } from 'node:fs/promises';
import * as core from '@actions/core';
import path from 'node:path';
import { JsonFinal } from './types/JsonFinal';
import { JsonSummary } from './types/JsonSummary';
import { JsonFinal } from '../types/JsonFinal';
import { JsonSummary } from '../types/JsonSummary';
import { stripIndent } from 'common-tags';

const parseVitestCoverageReport = async <type extends JsonSummary | JsonFinal>(jsonPath: string): Promise<type> => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createJsonFinalEntry } from './types/JsonFinalMockFactory';
import { createJsonFinalEntry } from '../types/JsonFinalMockFactory';
import { generateFileCoverageHtml } from './generateFileCoverageHtml';
import { getTableLine } from '../test/queryHelper';
import { JsonFinal } from './types/JsonFinal';
import { JsonSummary } from './types/JsonSummary';
import { createMockCoverageReport, createMockJsonSummary, createMockReportNumbers } from './types/JsonSummaryMockFactory';
import { getTableLine } from '../../test/queryHelper';
import { JsonFinal } from '../types/JsonFinal';
import { JsonSummary } from '../types/JsonSummary';
import { createMockCoverageReport, createMockJsonSummary, createMockReportNumbers } from '../types/JsonSummaryMockFactory';
import { describe, it, expect } from 'vitest';
import { FileCoverageMode } from './FileCoverageMode';
import { FileCoverageMode } from '../inputs/FileCoverageMode';
import * as path from 'path';

const workspacePath = process.cwd();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as path from 'path'
import * as core from '@actions/core'
import { generateBlobFileUrl } from './generateFileUrl'
import { LineRange, getUncoveredLinesFromStatements } from './getUncoveredLinesFromStatements'
import { JsonFinal } from './types/JsonFinal'
import { JsonSummary } from './types/JsonSummary'
import { JsonFinal } from '../types/JsonFinal'
import { JsonSummary } from '../types/JsonSummary'
import { oneLine } from 'common-tags'
import { FileCoverageMode } from './FileCoverageMode'
import { FileCoverageMode } from '../inputs/FileCoverageMode'

type FileCoverageInputs = {
jsonSummary: JsonSummary;
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 698d54e

Please sign in to comment.