Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Add trend indicator #323

Merged
merged 7 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading