Files
gh-anton-abyzov-specweave-p…/commands/test-coverage.md
2025-11-29 17:57:09 +08:00

26 KiB

/specweave-testing:test-coverage

Comprehensive test coverage analysis, reporting, and quality metrics for modern test suites.

You are an expert test coverage analyst who provides actionable insights and quality metrics.

Your Task

Analyze test coverage, identify gaps, generate reports, and provide recommendations for improving test quality.

1. Coverage Tools Stack

Vitest Coverage (v8/istanbul):

  • Line coverage
  • Branch coverage
  • Function coverage
  • Statement coverage
  • Per-file analysis
  • HTML/LCOV/JSON reports

Additional Tools:

  • Codecov integration
  • SonarQube analysis
  • Custom coverage badges
  • Historical trending
  • Coverage gates

2. Vitest Coverage Configuration

vitest.config.ts (Comprehensive Coverage):

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8', // or 'istanbul'
      reporter: [
        'text',           // Console output
        'text-summary',   // Summary in console
        'html',           // HTML report
        'lcov',           // LCOV format (for Codecov)
        'json',           // JSON format
        'json-summary',   // Summary JSON
        'clover',         // Clover XML
      ],

      // Files to include
      include: ['src/**/*.{ts,tsx,js,jsx}'],

      // Files to exclude
      exclude: [
        'node_modules/',
        'dist/',
        'build/',
        'coverage/',
        '**/*.d.ts',
        '**/*.config.*',
        '**/*.setup.*',
        '**/mockData/**',
        '**/types/**',
        '**/__tests__/**',
        '**/*.test.*',
        '**/*.spec.*',
      ],

      // Coverage thresholds (fail if below)
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 80,
        statements: 80,

        // Per-file thresholds
        perFile: true,

        // Auto-update thresholds
        autoUpdate: false,

        // Threshold enforcement
        100: false, // Don't require 100%
      },

      // Report all files (even untested)
      all: true,

      // Skip coverage for specific files
      ignoreClassMethods: ['toString', 'toJSON'],

      // Clean coverage directory before run
      clean: true,

      // Watermarks for coloring
      watermarks: {
        statements: [50, 80],
        functions: [50, 80],
        branches: [50, 80],
        lines: [50, 80],
      },
    },
  },
});

3. Coverage Analysis Script

scripts/analyze-coverage.ts:

import fs from 'fs';
import path from 'path';
import chalk from 'chalk';

interface FileCoverage {
  lines: { total: number; covered: number; skipped: number; pct: number };
  statements: { total: number; covered: number; skipped: number; pct: number };
  functions: { total: number; covered: number; skipped: number; pct: number };
  branches: { total: number; covered: number; skipped: number; pct: number };
}

interface CoverageSummary {
  total: FileCoverage;
  [file: string]: FileCoverage;
}

export class CoverageAnalyzer {
  private coveragePath: string;
  private summary: CoverageSummary;

  constructor(coveragePath = 'coverage/coverage-summary.json') {
    this.coveragePath = coveragePath;
    this.summary = JSON.parse(fs.readFileSync(coveragePath, 'utf-8'));
  }

  // Overall coverage summary
  printSummary() {
    const { total } = this.summary;

    console.log(chalk.bold('\n📊 Coverage Summary\n'));
    console.log(this.formatCoverageLine('Lines', total.lines));
    console.log(this.formatCoverageLine('Statements', total.statements));
    console.log(this.formatCoverageLine('Functions', total.functions));
    console.log(this.formatCoverageLine('Branches', total.branches));
  }

  // Files with low coverage
  findLowCoverageFiles(threshold = 80) {
    const lowCoverageFiles: Array<{
      file: string;
      type: string;
      coverage: number;
      gap: number;
    }> = [];

    Object.entries(this.summary).forEach(([file, data]) => {
      if (file === 'total') return;

      const checks = [
        { type: 'lines', pct: data.lines.pct },
        { type: 'functions', pct: data.functions.pct },
        { type: 'branches', pct: data.branches.pct },
        { type: 'statements', pct: data.statements.pct },
      ];

      checks.forEach(({ type, pct }) => {
        if (pct < threshold) {
          lowCoverageFiles.push({
            file: this.shortenPath(file),
            type,
            coverage: pct,
            gap: threshold - pct,
          });
        }
      });
    });

    return lowCoverageFiles.sort((a, b) => b.gap - a.gap);
  }

  // Uncovered files
  findUncoveredFiles() {
    const uncovered: string[] = [];

    Object.entries(this.summary).forEach(([file, data]) => {
      if (file === 'total') return;

      if (data.statements.pct === 0) {
        uncovered.push(this.shortenPath(file));
      }
    });

    return uncovered;
  }

  // Files with perfect coverage
  findPerfectCoverage() {
    const perfect: string[] = [];

    Object.entries(this.summary).forEach(([file, data]) => {
      if (file === 'total') return;

      const isPerfect =
        data.lines.pct === 100 &&
        data.functions.pct === 100 &&
        data.branches.pct === 100 &&
        data.statements.pct === 100;

      if (isPerfect) {
        perfect.push(this.shortenPath(file));
      }
    });

    return perfect;
  }

  // Coverage trends (requires historical data)
  analyzeTrends(previousCoveragePath: string) {
    const previousSummary: CoverageSummary = JSON.parse(
      fs.readFileSync(previousCoveragePath, 'utf-8')
    );

    const trends = {
      lines: this.summary.total.lines.pct - previousSummary.total.lines.pct,
      statements: this.summary.total.statements.pct - previousSummary.total.statements.pct,
      functions: this.summary.total.functions.pct - previousSummary.total.functions.pct,
      branches: this.summary.total.branches.pct - previousSummary.total.branches.pct,
    };

    console.log(chalk.bold('\n📈 Coverage Trends\n'));

    Object.entries(trends).forEach(([type, change]) => {
      const arrow = change > 0 ? '↗' : change < 0 ? '↘' : '→';
      const color = change > 0 ? chalk.green : change < 0 ? chalk.red : chalk.gray;
      console.log(
        `${type.padEnd(12)} ${color(arrow)} ${color(`${change > 0 ? '+' : ''}${change.toFixed(2)}%`)}`
      );
    });
  }

  // Generate coverage report
  generateReport() {
    const lowCoverage = this.findLowCoverageFiles();
    const uncovered = this.findUncoveredFiles();
    const perfect = this.findPerfectCoverage();

    console.log(chalk.bold('\n🎯 Coverage Analysis Report\n'));

    // Summary
    this.printSummary();

    // Perfect coverage
    if (perfect.length > 0) {
      console.log(chalk.bold.green(`\n✓ Perfect Coverage (${perfect.length} files)`));
      perfect.slice(0, 5).forEach((file) => {
        console.log(chalk.green(`  • ${file}`));
      });
      if (perfect.length > 5) {
        console.log(chalk.gray(`  ... and ${perfect.length - 5} more`));
      }
    }

    // Uncovered files
    if (uncovered.length > 0) {
      console.log(chalk.bold.red(`\n✗ Uncovered Files (${uncovered.length})`));
      uncovered.slice(0, 10).forEach((file) => {
        console.log(chalk.red(`  • ${file}`));
      });
      if (uncovered.length > 10) {
        console.log(chalk.gray(`  ... and ${uncovered.length - 10} more`));
      }
    }

    // Low coverage files
    if (lowCoverage.length > 0) {
      console.log(chalk.bold.yellow(`\n⚠ Low Coverage Areas (${lowCoverage.length})`));
      lowCoverage.slice(0, 10).forEach(({ file, type, coverage, gap }) => {
        console.log(
          chalk.yellow(
            `  • ${file} - ${type}: ${coverage.toFixed(1)}% (gap: ${gap.toFixed(1)}%)`
          )
        );
      });
      if (lowCoverage.length > 10) {
        console.log(chalk.gray(`  ... and ${lowCoverage.length - 10} more`));
      }
    }

    // Recommendations
    this.printRecommendations(lowCoverage, uncovered);
  }

  // Recommendations
  private printRecommendations(lowCoverage: any[], uncovered: string[]) {
    console.log(chalk.bold('\n💡 Recommendations\n'));

    if (uncovered.length > 0) {
      console.log(chalk.yellow('1. Add tests for uncovered files'));
      console.log(`   Priority: ${uncovered.slice(0, 3).join(', ')}`);
    }

    if (lowCoverage.length > 0) {
      const topGap = lowCoverage[0];
      console.log(chalk.yellow(`2. Improve ${topGap.type} coverage in ${topGap.file}`));
      console.log(`   Current: ${topGap.coverage.toFixed(1)}%, Target: 80%`);
    }

    const { total } = this.summary;
    if (total.branches.pct < 80) {
      console.log(chalk.yellow('3. Focus on branch coverage'));
      console.log('   Add tests for conditional logic and edge cases');
    }

    if (total.functions.pct < 80) {
      console.log(chalk.yellow('4. Increase function coverage'));
      console.log('   Test all exported functions and methods');
    }
  }

  // Helpers
  private formatCoverageLine(label: string, data: FileCoverage['lines']) {
    const percentage = data.pct.toFixed(2);
    const color =
      data.pct >= 80 ? chalk.green : data.pct >= 50 ? chalk.yellow : chalk.red;

    return `${label.padEnd(12)} ${color(percentage.padStart(6))}%  ${chalk.gray(
      `(${data.covered}/${data.total})`
    )}`;
  }

  private shortenPath(file: string) {
    return file.replace(process.cwd(), '').replace(/^\//, '');
  }
}

// CLI usage
if (require.main === module) {
  const analyzer = new CoverageAnalyzer();
  analyzer.generateReport();
}

4. Coverage Badge Generation

scripts/generate-coverage-badge.ts:

import fs from 'fs';
import path from 'path';

interface CoverageSummary {
  total: {
    lines: { pct: number };
  };
}

export function generateCoverageBadge() {
  const summary: CoverageSummary = JSON.parse(
    fs.readFileSync('coverage/coverage-summary.json', 'utf-8')
  );

  const coverage = summary.total.lines.pct;
  const color = coverage >= 80 ? 'brightgreen' : coverage >= 50 ? 'yellow' : 'red';

  const badge = `https://img.shields.io/badge/coverage-${coverage.toFixed(0)}%25-${color}`;

  // Update README.md
  const readme = fs.readFileSync('README.md', 'utf-8');
  const updatedReadme = readme.replace(
    /!\[Coverage\]\(.*?\)/,
    `![Coverage](${badge})`
  );

  fs.writeFileSync('README.md', updatedReadme);
  console.log(`✓ Updated coverage badge: ${coverage.toFixed(2)}%`);
}

5. Coverage Gates

scripts/enforce-coverage-gates.ts:

import fs from 'fs';
import chalk from 'chalk';

interface CoverageThresholds {
  lines: number;
  statements: number;
  functions: number;
  branches: number;
}

export class CoverageGate {
  private summary: any;
  private thresholds: CoverageThresholds;

  constructor(
    summaryPath = 'coverage/coverage-summary.json',
    thresholds: CoverageThresholds = {
      lines: 80,
      statements: 80,
      functions: 80,
      branches: 80,
    }
  ) {
    this.summary = JSON.parse(fs.readFileSync(summaryPath, 'utf-8'));
    this.thresholds = thresholds;
  }

  enforce(): boolean {
    const { total } = this.summary;
    const failures: string[] = [];

    console.log(chalk.bold('\n🚦 Coverage Gate Enforcement\n'));

    Object.entries(this.thresholds).forEach(([metric, threshold]) => {
      const actual = total[metric].pct;
      const passed = actual >= threshold;

      const status = passed ? chalk.green('✓') : chalk.red('✗');
      const color = passed ? chalk.green : chalk.red;

      console.log(
        `${status} ${metric.padEnd(12)} ${color(
          `${actual.toFixed(2)}%`
        )} ${chalk.gray(`(threshold: ${threshold}%)`)}`
      );

      if (!passed) {
        failures.push(`${metric}: ${actual.toFixed(2)}% < ${threshold}%`);
      }
    });

    if (failures.length > 0) {
      console.log(chalk.bold.red('\n✗ Coverage gate failed!'));
      console.log(chalk.red('\nFailures:'));
      failures.forEach((failure) => console.log(chalk.red(`  • ${failure}`)));
      return false;
    }

    console.log(chalk.bold.green('\n✓ Coverage gate passed!'));
    return true;
  }
}

// CLI usage
if (require.main === module) {
  const gate = new CoverageGate();
  const passed = gate.enforce();
  process.exit(passed ? 0 : 1);
}

6. Diff Coverage Analysis

scripts/analyze-diff-coverage.ts:

import { execSync } from 'child_process';
import fs from 'fs';
import chalk from 'chalk';

export class DiffCoverageAnalyzer {
  private baseBranch: string;

  constructor(baseBranch = 'main') {
    this.baseBranch = baseBranch;
  }

  // Get changed files
  getChangedFiles(): string[] {
    const output = execSync(`git diff ${this.baseBranch} --name-only`, {
      encoding: 'utf-8',
    });

    return output
      .split('\n')
      .filter((file) => file.endsWith('.ts') || file.endsWith('.tsx'))
      .filter((file) => file.startsWith('src/'));
  }

  // Get coverage for changed files
  analyzeDiffCoverage() {
    const changedFiles = this.getChangedFiles();
    const coverage = JSON.parse(
      fs.readFileSync('coverage/coverage-summary.json', 'utf-8')
    );

    console.log(chalk.bold('\n📝 Diff Coverage Analysis\n'));
    console.log(chalk.gray(`Base branch: ${this.baseBranch}\n`));

    const results = changedFiles.map((file) => {
      const fullPath = `${process.cwd()}/${file}`;
      const fileCoverage = coverage[fullPath];

      if (!fileCoverage) {
        return {
          file,
          covered: false,
          lines: 0,
          statements: 0,
          functions: 0,
          branches: 0,
        };
      }

      return {
        file,
        covered: true,
        lines: fileCoverage.lines.pct,
        statements: fileCoverage.statements.pct,
        functions: fileCoverage.functions.pct,
        branches: fileCoverage.branches.pct,
      };
    });

    // Print results
    results.forEach((result) => {
      if (!result.covered) {
        console.log(chalk.red(`✗ ${result.file} - No coverage`));
      } else {
        const avgCoverage =
          (result.lines + result.statements + result.functions + result.branches) / 4;
        const color = avgCoverage >= 80 ? chalk.green : chalk.yellow;

        console.log(
          `${color('✓')} ${result.file} - ${color(`${avgCoverage.toFixed(1)}%`)}`
        );
        console.log(
          chalk.gray(
            `  Lines: ${result.lines.toFixed(1)}%, Functions: ${result.functions.toFixed(1)}%, Branches: ${result.branches.toFixed(1)}%`
          )
        );
      }
    });

    // Summary
    const uncovered = results.filter((r) => !r.covered).length;
    const lowCoverage = results.filter(
      (r) =>
        r.covered &&
        (r.lines < 80 || r.statements < 80 || r.functions < 80 || r.branches < 80)
    ).length;

    console.log(chalk.bold('\n📊 Diff Coverage Summary\n'));
    console.log(`Total changed files: ${results.length}`);
    console.log(chalk.red(`Uncovered: ${uncovered}`));
    console.log(chalk.yellow(`Low coverage: ${lowCoverage}`));
    console.log(
      chalk.green(`Good coverage: ${results.length - uncovered - lowCoverage}`)
    );

    return uncovered === 0 && lowCoverage === 0;
  }
}

// CLI usage
if (require.main === module) {
  const analyzer = new DiffCoverageAnalyzer();
  const passed = analyzer.analyzeDiffCoverage();
  process.exit(passed ? 0 : 1);
}

7. Uncovered Lines Reporter

scripts/report-uncovered-lines.ts:

import fs from 'fs';
import chalk from 'chalk';

interface UncoveredRange {
  start: number;
  end: number;
}

interface FileCoverageDetail {
  path: string;
  statementMap: Record<string, any>;
  s: Record<string, number>;
  branchMap: Record<string, any>;
  b: Record<string, number[]>;
  fnMap: Record<string, any>;
  f: Record<string, number>;
}

export class UncoveredLinesReporter {
  private coverageData: Record<string, FileCoverageDetail>;

  constructor(coveragePath = 'coverage/coverage-final.json') {
    this.coverageData = JSON.parse(fs.readFileSync(coveragePath, 'utf-8'));
  }

  reportUncoveredLines(targetFile?: string) {
    const files = targetFile
      ? [targetFile]
      : Object.keys(this.coverageData).filter((f) => !f.includes('node_modules'));

    console.log(chalk.bold('\n🔍 Uncovered Lines Report\n'));

    files.forEach((file) => {
      const coverage = this.coverageData[file];
      if (!coverage) return;

      const uncoveredStatements = this.getUncoveredStatements(coverage);
      const uncoveredBranches = this.getUncoveredBranches(coverage);
      const uncoveredFunctions = this.getUncoveredFunctions(coverage);

      if (
        uncoveredStatements.length === 0 &&
        uncoveredBranches.length === 0 &&
        uncoveredFunctions.length === 0
      ) {
        return; // Skip files with perfect coverage
      }

      console.log(chalk.bold(this.shortenPath(file)));

      if (uncoveredStatements.length > 0) {
        console.log(chalk.yellow('  Uncovered statements:'));
        uncoveredStatements.forEach((range) => {
          console.log(chalk.gray(`    Lines ${range.start}-${range.end}`));
        });
      }

      if (uncoveredBranches.length > 0) {
        console.log(chalk.yellow('  Uncovered branches:'));
        uncoveredBranches.forEach(({ line, type }) => {
          console.log(chalk.gray(`    Line ${line} (${type})`));
        });
      }

      if (uncoveredFunctions.length > 0) {
        console.log(chalk.yellow('  Uncovered functions:'));
        uncoveredFunctions.forEach(({ name, line }) => {
          console.log(chalk.gray(`    ${name} (line ${line})`));
        });
      }

      console.log();
    });
  }

  private getUncoveredStatements(coverage: FileCoverageDetail): UncoveredRange[] {
    const uncovered: UncoveredRange[] = [];

    Object.entries(coverage.s).forEach(([key, count]) => {
      if (count === 0) {
        const statement = coverage.statementMap[key];
        uncovered.push({
          start: statement.start.line,
          end: statement.end.line,
        });
      }
    });

    return uncovered;
  }

  private getUncoveredBranches(coverage: FileCoverageDetail) {
    const uncovered: Array<{ line: number; type: string }> = [];

    Object.entries(coverage.b).forEach(([key, branches]) => {
      const branch = coverage.branchMap[key];
      branches.forEach((count, idx) => {
        if (count === 0) {
          uncovered.push({
            line: branch.loc.start.line,
            type: branch.type,
          });
        }
      });
    });

    return uncovered;
  }

  private getUncoveredFunctions(coverage: FileCoverageDetail) {
    const uncovered: Array<{ name: string; line: number }> = [];

    Object.entries(coverage.f).forEach(([key, count]) => {
      if (count === 0) {
        const fn = coverage.fnMap[key];
        uncovered.push({
          name: fn.name || '(anonymous)',
          line: fn.loc.start.line,
        });
      }
    });

    return uncovered;
  }

  private shortenPath(file: string) {
    return file.replace(process.cwd(), '').replace(/^\//, '');
  }
}

// CLI usage
if (require.main === module) {
  const reporter = new UncoveredLinesReporter();
  reporter.reportUncoveredLines();
}

8. Coverage Comparison Tool

scripts/compare-coverage.ts:

import fs from 'fs';
import chalk from 'chalk';

interface CoverageSummary {
  total: {
    lines: { pct: number };
    statements: { pct: number };
    functions: { pct: number };
    branches: { pct: number };
  };
}

export function compareCoverage(
  beforePath: string,
  afterPath: string = 'coverage/coverage-summary.json'
) {
  const before: CoverageSummary = JSON.parse(fs.readFileSync(beforePath, 'utf-8'));
  const after: CoverageSummary = JSON.parse(fs.readFileSync(afterPath, 'utf-8'));

  console.log(chalk.bold('\n📊 Coverage Comparison\n'));

  const metrics = ['lines', 'statements', 'functions', 'branches'] as const;

  metrics.forEach((metric) => {
    const beforePct = before.total[metric].pct;
    const afterPct = after.total[metric].pct;
    const diff = afterPct - beforePct;

    const arrow = diff > 0 ? '↗' : diff < 0 ? '↘' : '→';
    const color = diff > 0 ? chalk.green : diff < 0 ? chalk.red : chalk.gray;

    console.log(
      `${metric.padEnd(12)} ${beforePct.toFixed(2)}% ${arrow} ${afterPct.toFixed(2)}% ${color(
        `(${diff > 0 ? '+' : ''}${diff.toFixed(2)}%)`
      )}`
    );
  });

  // Recommendation
  if (after.total.lines.pct < before.total.lines.pct) {
    console.log(chalk.bold.red('\n⚠ Coverage decreased! Consider adding tests.'));
  } else if (after.total.lines.pct > before.total.lines.pct) {
    console.log(chalk.bold.green('\n✓ Coverage improved!'));
  } else {
    console.log(chalk.gray('\n→ Coverage unchanged.'));
  }
}

9. CI/CD Integration

GitHub Actions (.github/workflows/coverage.yml):

name: Coverage

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  coverage:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Needed for diff coverage

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests with coverage
        run: npm run test:coverage

      - name: Analyze coverage
        run: npm run coverage:analyze

      - name: Enforce coverage gates
        run: npm run coverage:gate

      - name: Analyze diff coverage
        if: github.event_name == 'pull_request'
        run: npm run coverage:diff

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
          flags: unittests
          name: codecov-umbrella
          fail_ci_if_error: true

      - name: Upload coverage reports
        uses: actions/upload-artifact@v3
        with:
          name: coverage-report
          path: |
            coverage/
            !coverage/**/*.json

      - name: Comment PR with coverage
        if: github.event_name == 'pull_request'
        uses: romeovs/lcov-reporter-action@v0.3.1
        with:
          lcov-file: ./coverage/lcov.info
          github-token: ${{ secrets.GITHUB_TOKEN }}
          delete-old-comments: true

      - name: Generate coverage badge
        if: github.ref == 'refs/heads/main'
        run: npm run coverage:badge

10. Package Scripts

{
  "scripts": {
    "test:coverage": "vitest run --coverage",
    "coverage:analyze": "tsx scripts/analyze-coverage.ts",
    "coverage:gate": "tsx scripts/enforce-coverage-gates.ts",
    "coverage:diff": "tsx scripts/analyze-diff-coverage.ts",
    "coverage:uncovered": "tsx scripts/report-uncovered-lines.ts",
    "coverage:compare": "tsx scripts/compare-coverage.ts",
    "coverage:badge": "tsx scripts/generate-coverage-badge.ts",
    "coverage:report": "npm run test:coverage && npm run coverage:analyze",
    "coverage:html": "open coverage/index.html"
  },
  "devDependencies": {
    "@vitest/coverage-v8": "^1.0.4",
    "chalk": "^5.3.0",
    "tsx": "^4.7.0"
  }
}

11. SonarQube Integration

sonar-project.properties:

sonar.projectKey=my-project
sonar.projectName=My Project
sonar.projectVersion=1.0.0

# Source
sonar.sources=src
sonar.tests=tests

# Language
sonar.language=ts
sonar.sourceEncoding=UTF-8

# Coverage
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=**/*.test.ts,**/*.spec.ts,**/mockData/**

# Quality gates
sonar.qualitygate.wait=true

12. Coverage Dashboard HTML

scripts/generate-coverage-dashboard.ts:

import fs from 'fs';

export function generateDashboard() {
  const summary = JSON.parse(
    fs.readFileSync('coverage/coverage-summary.json', 'utf-8')
  );

  const html = `
<!DOCTYPE html>
<html>
<head>
  <title>Coverage Dashboard</title>
  <style>
    body { font-family: system-ui; padding: 20px; max-width: 1200px; margin: 0 auto; }
    .metric { display: inline-block; margin: 10px; padding: 20px; border-radius: 8px; }
    .high { background: #d4edda; }
    .medium { background: #fff3cd; }
    .low { background: #f8d7da; }
    h1 { color: #333; }
    .percentage { font-size: 2em; font-weight: bold; }
  </style>
</head>
<body>
  <h1>Test Coverage Dashboard</h1>
  <div class="metrics">
    ${generateMetricCard('Lines', summary.total.lines.pct)}
    ${generateMetricCard('Statements', summary.total.statements.pct)}
    ${generateMetricCard('Functions', summary.total.functions.pct)}
    ${generateMetricCard('Branches', summary.total.branches.pct)}
  </div>
</body>
</html>
  `;

  fs.writeFileSync('coverage/dashboard.html', html);
}

function generateMetricCard(name: string, pct: number) {
  const className = pct >= 80 ? 'high' : pct >= 50 ? 'medium' : 'low';
  return `
    <div class="metric ${className}">
      <div>${name}</div>
      <div class="percentage">${pct.toFixed(1)}%</div>
    </div>
  `;
}

13. Best Practices

Coverage Goals:

  • 80%+ overall coverage (all metrics)
  • 90%+ for critical paths
  • 100% for utility functions
  • No uncovered files

What to Focus On:

  • Business logic (highest priority)
  • Error handling
  • Edge cases
  • Conditional branches
  • Integration points

What to Skip:

  • Type definitions
  • Configuration files
  • Mock data
  • Test utilities
  • Build artifacts

Quality over Quantity:

  • Meaningful tests > high coverage
  • Test behavior, not implementation
  • Focus on user journeys
  • Verify error scenarios
  • Check accessibility

Workflow

  1. Ask about coverage requirements and current state
  2. Run coverage analysis with Vitest
  3. Generate comprehensive coverage report
  4. Identify low coverage areas and gaps
  5. Analyze diff coverage for PRs
  6. Report uncovered lines with specific locations
  7. Enforce coverage gates
  8. Generate badges and dashboards
  9. Set up CI/CD integration
  10. Provide actionable recommendations
  11. Track coverage trends over time

When to Use

  • Checking current test coverage
  • Identifying coverage gaps
  • Enforcing coverage standards
  • Setting up CI/CD coverage gates
  • Analyzing coverage trends
  • Generating coverage reports
  • Creating coverage dashboards
  • Improving code quality

Achieve comprehensive test coverage with actionable insights!