mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
fix: remove docs/tests/comments, extract find-workflow-run action, consolidate scripts
- Remove feasibility doc (one-time analysis, not reference) - Remove unit tests for CI scripts (change-detector tests) - Remove organizational comments from scripts - Extract find-workflow-run composite action from pr-report.yaml - Remove exports from coverage-slack-notify.ts (were only for tests) - Remove VITEST guard from coverage-slack-notify.ts - Fix toLocaleString() in coverage-report.js (locale-dependent in CI)
This commit is contained in:
61
.github/actions/find-workflow-run/action.yaml
vendored
Normal file
61
.github/actions/find-workflow-run/action.yaml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Find Workflow Run
|
||||
description: Finds a workflow run for a given commit SHA and outputs its status and run ID.
|
||||
|
||||
inputs:
|
||||
workflow-id:
|
||||
description: The workflow filename (e.g., 'ci-size-data.yaml')
|
||||
required: true
|
||||
head-sha:
|
||||
description: The commit SHA to find runs for
|
||||
required: true
|
||||
not-found-status:
|
||||
description: Status to output when no run exists
|
||||
required: false
|
||||
default: pending
|
||||
token:
|
||||
description: GitHub token for API access
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
status:
|
||||
description: One of 'ready', 'pending', 'failed', or the not-found-status value
|
||||
value: ${{ steps.find.outputs.status }}
|
||||
run-id:
|
||||
description: The workflow run ID (only set when status is 'ready')
|
||||
value: ${{ steps.find.outputs.run-id }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Find workflow run
|
||||
id: find
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ inputs.token }}
|
||||
script: |
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: '${{ inputs.workflow-id }}',
|
||||
head_sha: '${{ inputs.head-sha }}',
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
const run = runs.workflow_runs[0];
|
||||
if (!run) {
|
||||
core.setOutput('status', '${{ inputs.not-found-status }}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.status !== 'completed') {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.conclusion !== 'success') {
|
||||
core.setOutput('status', 'failed');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('status', 'ready');
|
||||
core.setOutput('run-id', String(run.id));
|
||||
106
.github/workflows/pr-report.yaml
vendored
106
.github/workflows/pr-report.yaml
vendored
@@ -67,73 +67,23 @@ jobs:
|
||||
core.setOutput('base', livePr.base.ref);
|
||||
core.setOutput('head-sha', livePr.head.sha);
|
||||
|
||||
- name: Find size workflow run for this commit
|
||||
- name: Find size workflow run
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
id: find-size
|
||||
uses: actions/github-script@v8
|
||||
uses: ./.github/actions/find-workflow-run
|
||||
with:
|
||||
script: |
|
||||
const headSha = '${{ steps.pr-meta.outputs.head-sha }}';
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'ci-size-data.yaml',
|
||||
head_sha: headSha,
|
||||
per_page: 1,
|
||||
});
|
||||
workflow-id: ci-size-data.yaml
|
||||
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
const run = runs.workflow_runs[0];
|
||||
if (!run) {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.status !== 'completed') {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.conclusion !== 'success') {
|
||||
core.setOutput('status', 'failed');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('status', 'ready');
|
||||
core.setOutput('run-id', String(run.id));
|
||||
|
||||
- name: Find perf workflow run for this commit
|
||||
- name: Find perf workflow run
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
id: find-perf
|
||||
uses: actions/github-script@v8
|
||||
uses: ./.github/actions/find-workflow-run
|
||||
with:
|
||||
script: |
|
||||
const headSha = '${{ steps.pr-meta.outputs.head-sha }}';
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'ci-perf-report.yaml',
|
||||
head_sha: headSha,
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
const run = runs.workflow_runs[0];
|
||||
if (!run) {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.status !== 'completed') {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.conclusion !== 'success') {
|
||||
core.setOutput('status', 'failed');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('status', 'ready');
|
||||
core.setOutput('run-id', String(run.id));
|
||||
workflow-id: ci-perf-report.yaml
|
||||
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download size data (current)
|
||||
if: steps.pr-meta.outputs.skip != 'true' && steps.find-size.outputs.status == 'ready'
|
||||
@@ -154,39 +104,15 @@ jobs:
|
||||
path: temp/size-prev
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Find coverage workflow run for this commit
|
||||
- name: Find coverage workflow run
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
id: find-coverage
|
||||
uses: actions/github-script@v8
|
||||
uses: ./.github/actions/find-workflow-run
|
||||
with:
|
||||
script: |
|
||||
const headSha = '${{ steps.pr-meta.outputs.head-sha }}';
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'ci-tests-e2e-coverage.yaml',
|
||||
head_sha: headSha,
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
const run = runs.workflow_runs[0];
|
||||
if (!run) {
|
||||
core.setOutput('status', 'skip');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.status !== 'completed') {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.conclusion !== 'success') {
|
||||
core.setOutput('status', 'failed');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('status', 'ready');
|
||||
core.setOutput('run-id', String(run.id));
|
||||
workflow-id: ci-tests-e2e-coverage.yaml
|
||||
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
|
||||
not-found-status: skip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download coverage data
|
||||
if: steps.pr-meta.outputs.skip != 'true' && steps.find-coverage.outputs.status == 'ready'
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
# Playwright Code Coverage Feasibility Summary
|
||||
|
||||
## Objective
|
||||
|
||||
Evaluate approaches for collecting code coverage data from Playwright E2E tests in the ComfyUI frontend project, enabling the team to identify untested code paths exercised during browser-level integration tests.
|
||||
|
||||
## Current Project Context
|
||||
|
||||
| Aspect | Details |
|
||||
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Bundler** | Vite (via `vitest/config`'s `defineConfig`) |
|
||||
| **Unit test coverage** | `@vitest/coverage-v8` already in devDependencies |
|
||||
| **Playwright tests** | 59 spec files in `browser_tests/tests/` |
|
||||
| **Playwright fixture** | Custom `comfyPageFixture` extending `@playwright/test`'s `base.extend` |
|
||||
| **Browser targets** | Chromium-only (Desktop Chrome, Mobile Chrome, 2x/0.5x scale variants). Firefox/WebKit are commented out in `playwright.config.ts` |
|
||||
| **Test runner** | `pnpm test:browser` / `pnpm test:browser:local` |
|
||||
| **Source maps** | Enabled by default (`GENERATE_SOURCEMAP !== 'false'`) |
|
||||
| **Build configuration** | Complex — conditional plugins for Sentry, DevTools, font exclusion, meta injection, etc. |
|
||||
| **Global setup/teardown** | Backs up/restores ComfyUI user data; writes perf reports on teardown |
|
||||
| **Vitest coverage config** | `coverage: { reporter: ['text', 'json', 'html'] }` in `vite.config.mts` |
|
||||
|
||||
## Approach 1: V8 Coverage via `page.coverage` API
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Call `page.coverage.startJSCoverage()` before each test navigates to the app.
|
||||
2. Call `page.coverage.stopJSCoverage()` after each test, receiving raw V8 coverage entries (byte offsets, function ranges).
|
||||
3. Convert V8 format to Istanbul/lcov using the `v8-to-istanbul` npm package.
|
||||
4. Merge per-test coverage files and generate reports with `nyc report` or `istanbul`.
|
||||
|
||||
### Wrapper Package: `@bgotink/playwright-coverage`
|
||||
|
||||
This package wraps the above workflow into a Playwright fixture and reporter:
|
||||
|
||||
- Provides a custom `test.extend()` fixture that auto-starts/stops coverage.
|
||||
- Includes a Playwright reporter that merges and outputs Istanbul-format `.json` files.
|
||||
- Handles source map resolution for mapping bundled code back to source files.
|
||||
|
||||
### Integration Point
|
||||
|
||||
The `comfyPageFixture` already extends `base.extend<{ comfyPage: ComfyPage }>`. A coverage fixture would either:
|
||||
|
||||
- Wrap `comfyPageFixture` with an additional fixture layer, or
|
||||
- Be added as a separate fixture composed alongside `comfyPage`.
|
||||
|
||||
### Pros
|
||||
|
||||
- **No build modifications** — works with the existing production build.
|
||||
- **Lower runtime overhead** — V8 coverage is built into the engine; no instrumentation step.
|
||||
- **Simpler setup** — no conditional Vite plugin configuration.
|
||||
- **Familiar tooling** — team already uses `@vitest/coverage-v8` (same V8 engine).
|
||||
|
||||
### Cons
|
||||
|
||||
- **Chromium-only** — `page.coverage` is a CDP (Chrome DevTools Protocol) API. If Firefox/WebKit projects are ever enabled, coverage won't work there.
|
||||
- **Source map accuracy** — V8 reports coverage against bundled code. Source map resolution can produce imprecise mappings, especially with heavily transformed code (Vue SFCs, TypeScript, Tailwind).
|
||||
- **Bundle-level granularity** — coverage is per-bundle-chunk, not per-source-file. Vendor chunks and code-split modules may produce noisy data.
|
||||
- **Vue SFC blind spots** — template compilation and `<script setup>` transforms can cause missed or phantom coverage lines.
|
||||
|
||||
### Estimated Effort
|
||||
|
||||
**1–2 days** to integrate `@bgotink/playwright-coverage` into the existing fixture, configure source map resolution, and generate initial reports.
|
||||
|
||||
---
|
||||
|
||||
## Approach 2: Istanbul Instrumentation via `vite-plugin-istanbul`
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Add `vite-plugin-istanbul` to the Vite plugin chain, gated behind an environment variable (e.g., `INSTRUMENT_COVERAGE=true`).
|
||||
2. The plugin instruments source files at build/serve time with Istanbul counters, exposing `window.__coverage__` at runtime.
|
||||
3. After each test, collect `window.__coverage__` via `page.evaluate(() => window.__coverage__)` and write it to `.nyc_output/`.
|
||||
4. Run `nyc report --reporter=html --reporter=lcov` to generate coverage reports.
|
||||
|
||||
### Reference Implementation
|
||||
|
||||
[mxschmitt/playwright-test-coverage](https://github.com/mxschmitt/playwright-test-coverage) demonstrates this exact pattern with Vite + `vite-plugin-istanbul` + Playwright.
|
||||
|
||||
### Integration Point
|
||||
|
||||
Add a Playwright fixture that:
|
||||
|
||||
```typescript
|
||||
// coverage-fixture.ts (simplified)
|
||||
import { test as base } from '@playwright/test'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import crypto from 'crypto'
|
||||
|
||||
export const test = base.extend({
|
||||
context: async ({ context }, use) => {
|
||||
await context.addInitScript(() => {
|
||||
// Reset coverage for each context if needed
|
||||
})
|
||||
await use(context)
|
||||
},
|
||||
page: async ({ page }, use) => {
|
||||
await use(page)
|
||||
// Collect coverage after test
|
||||
const coverage = await page.evaluate(() => (window as any).__coverage__)
|
||||
if (coverage) {
|
||||
const outputDir = path.join(process.cwd(), '.nyc_output')
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
const id = crypto.randomUUID()
|
||||
fs.writeFileSync(
|
||||
path.join(outputDir, `${id}.json`),
|
||||
JSON.stringify(coverage)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Vite Configuration Change
|
||||
|
||||
```typescript
|
||||
// In vite.config.mts — add conditionally
|
||||
import istanbul from 'vite-plugin-istanbul'
|
||||
|
||||
const INSTRUMENT_COVERAGE = process.env.INSTRUMENT_COVERAGE === 'true'
|
||||
|
||||
// Add to plugins array:
|
||||
...(INSTRUMENT_COVERAGE
|
||||
? [istanbul({
|
||||
include: 'src/*',
|
||||
exclude: ['node_modules', 'browser_tests', 'tests-ui'],
|
||||
extension: ['.ts', '.vue'],
|
||||
requireEnv: true,
|
||||
forceBuildInstrument: true // Also instrument production builds
|
||||
})]
|
||||
: [])
|
||||
```
|
||||
|
||||
### Pros
|
||||
|
||||
- **Cross-browser** — works on any browser Playwright supports (if Firefox/WebKit are ever re-enabled).
|
||||
- **Source-level accuracy** — instrumentation happens before bundling, so coverage maps directly to original `.ts` and `.vue` files.
|
||||
- **Vue SFC support** — instruments the compiled output of `<script setup>`, capturing template-driven code paths accurately.
|
||||
- **Proven pattern** — well-documented reference implementations exist. Standard Istanbul tooling (`nyc`, `istanbul`) for reporting.
|
||||
- **Mergeable with unit coverage** — both Istanbul and V8 can produce lcov format, enabling merged reports across Vitest unit tests and Playwright E2E tests.
|
||||
|
||||
### Cons
|
||||
|
||||
- **Build modification required** — adds a conditional Vite plugin, increasing config complexity in an already complex `vite.config.mts`.
|
||||
- **Performance overhead** — Istanbul instrumentation adds ~10–30% runtime overhead and 2–3× bundle size increase due to injected counter code.
|
||||
- **Dev server impact** — if used with `pnpm dev`, instrumented code is slower. Must be gated behind an env var.
|
||||
- **Maintenance burden** — `vite-plugin-istanbul` must stay compatible with Vite version upgrades.
|
||||
- **CI build time** — instrumented builds take longer; may need a separate CI step or matrix entry.
|
||||
|
||||
### Estimated Effort
|
||||
|
||||
**2–3 days** to add `vite-plugin-istanbul` with conditional gating, create the coverage collection fixture, integrate with `nyc report`, and verify source mapping accuracy for Vue SFCs.
|
||||
|
||||
---
|
||||
|
||||
## Side-by-Side Comparison
|
||||
|
||||
| Criteria | V8 (`@bgotink/playwright-coverage`) | Istanbul (`vite-plugin-istanbul`) |
|
||||
| ----------------------------- | ---------------------------------------- | ---------------------------------- |
|
||||
| **Browser support** | Chromium only | All browsers |
|
||||
| **Build changes** | None | Conditional Vite plugin |
|
||||
| **Source map accuracy** | Good (post-bundle) | Excellent (pre-bundle) |
|
||||
| **Vue SFC coverage** | Partial (template compilation artifacts) | Full (instruments compiled output) |
|
||||
| **Runtime overhead** | Low (~5%) | Medium (~10–30%) |
|
||||
| **Bundle size impact** | None | 2–3× increase when instrumented |
|
||||
| **Setup complexity** | Low | Medium |
|
||||
| **Maintenance** | Low (Playwright built-in API) | Medium (plugin compatibility) |
|
||||
| **Mergeable with Vitest** | Yes (via lcov) | Yes (via lcov) |
|
||||
| **Existing team familiarity** | High (`@vitest/coverage-v8`) | Low (new tooling) |
|
||||
| **Estimated effort** | 1–2 days | 2–3 days |
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### V8 Approach Risks
|
||||
|
||||
1. **Source map resolution failures** — the project uses multiple Vite transforms (Vue SFCs, TypeScript, Tailwind, custom plugins). V8 coverage relies on source maps being accurate after all transforms. Inaccurate maps → misleading coverage data.
|
||||
2. **Code splitting noise** — the project uses aggressive code splitting (15+ vendor chunks via `rolldownOptions.output.codeSplitting`). Coverage for vendor code will pollute reports unless carefully filtered.
|
||||
3. **Low risk of breakage** — `page.coverage` is a stable Playwright/CDP API.
|
||||
|
||||
### Istanbul Approach Risks
|
||||
|
||||
1. **Build config complexity** — `vite.config.mts` is already ~650 lines with many conditional plugins. Adding another conditional plugin increases the surface area for config bugs.
|
||||
2. **Plugin compatibility** — the project recently migrated to Rolldown (`rolldownOptions`). `vite-plugin-istanbul` may not yet fully support Rolldown's output format. **This requires validation before committing to this approach.**
|
||||
3. **CI pipeline impact** — instrumented builds will need separate caching and possibly a dedicated CI job to avoid slowing down the main test pipeline.
|
||||
|
||||
## Recommendation
|
||||
|
||||
### Primary: Istanbul via `vite-plugin-istanbul`
|
||||
|
||||
The Istanbul approach is recommended as the primary path for the following reasons:
|
||||
|
||||
1. **Source-level accuracy matters** — the project is a large Vue 3 + TypeScript codebase with complex SFC compilation. Pre-bundle instrumentation produces the most trustworthy coverage data.
|
||||
2. **Vite-native integration** — as a Vite-based project, `vite-plugin-istanbul` integrates naturally with the existing build pipeline.
|
||||
3. **Future-proof** — if Firefox/WebKit projects are ever re-enabled (they're commented out in `playwright.config.ts`), coverage will continue to work without changes.
|
||||
4. **Merged reporting** — Istanbul output can be combined with Vitest's existing `@vitest/coverage-v8` output to produce a unified coverage view of unit + E2E tests.
|
||||
|
||||
### Fallback: V8 via `@bgotink/playwright-coverage`
|
||||
|
||||
If the Istanbul approach encounters blocking issues (e.g., `vite-plugin-istanbul` incompatibility with Rolldown), the V8 approach is a viable fallback:
|
||||
|
||||
- Zero build changes needed.
|
||||
- Quick to prototype (1–2 days).
|
||||
- Good enough for Chromium-only coverage (which is the current test target).
|
||||
|
||||
### Recommended Implementation Path
|
||||
|
||||
1. **Validate Rolldown compatibility** — install `vite-plugin-istanbul` and confirm it works with the project's Rolldown-based build. If it doesn't, fall back to V8.
|
||||
2. **Add conditional instrumentation** — gate behind `INSTRUMENT_COVERAGE=true` env var in `vite.config.mts`.
|
||||
3. **Create coverage fixture** — extend `comfyPageFixture` to collect `window.__coverage__` after each test and write to `.nyc_output/`.
|
||||
4. **Add `nyc` report generation** — add a `pnpm test:browser:coverage` script that runs tests with instrumentation, then generates HTML/lcov reports.
|
||||
5. **CI integration** — add an optional CI job (not on the critical path) that runs instrumented tests and uploads coverage reports.
|
||||
6. **Merge with unit coverage (stretch goal)** — combine Playwright lcov output with Vitest lcov output for a unified coverage dashboard.
|
||||
|
||||
### Suggested Package Additions
|
||||
|
||||
```jsonc
|
||||
// devDependencies
|
||||
{
|
||||
"vite-plugin-istanbul": "^6.0.2",
|
||||
"nyc": "^17.1.0"
|
||||
// OR for V8 fallback:
|
||||
// "@bgotink/playwright-coverage": "^0.3.0",
|
||||
// "v8-to-istanbul": "^9.3.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Suggested Script Additions
|
||||
|
||||
```jsonc
|
||||
// package.json scripts
|
||||
{
|
||||
"test:browser:coverage": "INSTRUMENT_COVERAGE=true pnpm test:browser && nyc report --reporter=html --reporter=lcov --temp-dir=.nyc_output --report-dir=coverage/playwright"
|
||||
}
|
||||
```
|
||||
@@ -1,11 +1,6 @@
|
||||
// @ts-check
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
|
||||
/**
|
||||
* Generates a markdown coverage report from lcov data.
|
||||
* Output format matches the unified PR report style (size + perf sections).
|
||||
*/
|
||||
|
||||
const lcovPath = process.argv[2] || 'coverage/playwright/coverage.lcov'
|
||||
|
||||
if (!existsSync(lcovPath)) {
|
||||
@@ -17,7 +12,6 @@ if (!existsSync(lcovPath)) {
|
||||
|
||||
const lcov = readFileSync(lcovPath, 'utf-8')
|
||||
|
||||
// Parse lcov summary
|
||||
let totalLines = 0
|
||||
let coveredLines = 0
|
||||
let totalFunctions = 0
|
||||
@@ -76,16 +70,15 @@ lines.push('')
|
||||
lines.push('| Metric | Covered | Total | Pct | |')
|
||||
lines.push('|---|--:|--:|--:|---|')
|
||||
lines.push(
|
||||
`| Lines | ${coveredLines.toLocaleString()} | ${totalLines.toLocaleString()} | ${pct(coveredLines, totalLines)} | ${bar(coveredLines, totalLines)} |`
|
||||
`| Lines | ${coveredLines} | ${totalLines} | ${pct(coveredLines, totalLines)} | ${bar(coveredLines, totalLines)} |`
|
||||
)
|
||||
lines.push(
|
||||
`| Functions | ${coveredFunctions.toLocaleString()} | ${totalFunctions.toLocaleString()} | ${pct(coveredFunctions, totalFunctions)} | ${bar(coveredFunctions, totalFunctions)} |`
|
||||
`| Functions | ${coveredFunctions} | ${totalFunctions} | ${pct(coveredFunctions, totalFunctions)} | ${bar(coveredFunctions, totalFunctions)} |`
|
||||
)
|
||||
lines.push(
|
||||
`| Branches | ${coveredBranches.toLocaleString()} | ${totalBranches.toLocaleString()} | ${pct(coveredBranches, totalBranches)} | ${bar(coveredBranches, totalBranches)} |`
|
||||
`| Branches | ${coveredBranches} | ${totalBranches} | ${pct(coveredBranches, totalBranches)} | ${bar(coveredBranches, totalBranches)} |`
|
||||
)
|
||||
|
||||
// Top uncovered files
|
||||
const uncovered = [...fileStats.entries()]
|
||||
.filter(([, s]) => s.lines > 0)
|
||||
.map(([file, s]) => ({
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
buildMilestoneBlock,
|
||||
crossedMilestone,
|
||||
formatCoverageRow,
|
||||
formatDelta,
|
||||
formatPct,
|
||||
parseArgs,
|
||||
parseLcovContent,
|
||||
progressBar
|
||||
} from './coverage-slack-notify'
|
||||
|
||||
describe('parseLcovContent', () => {
|
||||
it('parses valid lcov content', () => {
|
||||
const content = [
|
||||
'SF:src/foo.ts',
|
||||
'LF:100',
|
||||
'LH:75',
|
||||
'end_of_record',
|
||||
'SF:src/bar.ts',
|
||||
'LF:200',
|
||||
'LH:150',
|
||||
'end_of_record'
|
||||
].join('\n')
|
||||
|
||||
const result = parseLcovContent(content)
|
||||
expect(result).toEqual({
|
||||
totalLines: 300,
|
||||
coveredLines: 225,
|
||||
percentage: 75
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null for empty content', () => {
|
||||
expect(parseLcovContent('')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when total lines is zero', () => {
|
||||
expect(parseLcovContent('SF:src/foo.ts\nend_of_record')).toBeNull()
|
||||
})
|
||||
|
||||
it('handles malformed LF/LH values gracefully', () => {
|
||||
const content = 'LF:abc\nLH:50\n'
|
||||
expect(parseLcovContent(content)).toBeNull()
|
||||
})
|
||||
|
||||
it('handles NaN in LH with valid LF', () => {
|
||||
const content = 'LF:100\nLH:xyz\n'
|
||||
const result = parseLcovContent(content)
|
||||
expect(result).toEqual({
|
||||
totalLines: 100,
|
||||
coveredLines: 0,
|
||||
percentage: 0
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('progressBar', () => {
|
||||
it('returns all filled for 100%', () => {
|
||||
expect(progressBar(100)).toBe('████████████████████')
|
||||
})
|
||||
|
||||
it('returns all empty for 0%', () => {
|
||||
expect(progressBar(0)).toBe('░░░░░░░░░░░░░░░░░░░░')
|
||||
})
|
||||
|
||||
it('returns half filled for 50%', () => {
|
||||
const bar = progressBar(50)
|
||||
expect(bar).toBe('██████████░░░░░░░░░░')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatPct', () => {
|
||||
it('formats with one decimal place', () => {
|
||||
expect(formatPct(75.123)).toBe('75.1%')
|
||||
})
|
||||
|
||||
it('formats zero', () => {
|
||||
expect(formatPct(0)).toBe('0.0%')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatDelta', () => {
|
||||
it('adds + sign for positive delta', () => {
|
||||
expect(formatDelta(2.5)).toBe('+2.5%')
|
||||
})
|
||||
|
||||
it('adds - sign for negative delta', () => {
|
||||
expect(formatDelta(-1.3)).toBe('-1.3%')
|
||||
})
|
||||
|
||||
it('adds + sign for zero', () => {
|
||||
expect(formatDelta(0)).toBe('+0.0%')
|
||||
})
|
||||
})
|
||||
|
||||
describe('crossedMilestone', () => {
|
||||
it('detects crossing from 14.9 to 15.1', () => {
|
||||
expect(crossedMilestone(14.9, 15.1)).toBe(15)
|
||||
})
|
||||
|
||||
it('detects crossing from 79.9 to 80.1', () => {
|
||||
expect(crossedMilestone(79.9, 80.1)).toBe(80)
|
||||
})
|
||||
|
||||
it('returns null when no milestone crossed', () => {
|
||||
expect(crossedMilestone(16, 18)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns highest milestone when crossing multiple', () => {
|
||||
expect(crossedMilestone(14, 26)).toBe(25)
|
||||
})
|
||||
|
||||
it('detects exact boundary crossing', () => {
|
||||
expect(crossedMilestone(14.999, 15.0)).toBe(15)
|
||||
})
|
||||
|
||||
it('returns null when staying in same bucket', () => {
|
||||
expect(crossedMilestone(10.0, 14.9)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildMilestoneBlock', () => {
|
||||
it('returns goal-reached block at target', () => {
|
||||
const block = buildMilestoneBlock('Unit test', 80)
|
||||
expect(block).not.toBeNull()
|
||||
expect(block!.text.text).toContain('GOAL REACHED')
|
||||
})
|
||||
|
||||
it('returns milestone block below target', () => {
|
||||
const block = buildMilestoneBlock('Unit test', 25)
|
||||
expect(block).not.toBeNull()
|
||||
expect(block!.text.text).toContain('MILESTONE')
|
||||
expect(block!.text.text).toContain('55 percentage points to go')
|
||||
})
|
||||
|
||||
it('uses singular for 1 percentage point', () => {
|
||||
const block = buildMilestoneBlock('Unit test', 79)
|
||||
expect(block!.text.text).toContain('1 percentage point to go')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseArgs', () => {
|
||||
it('parses all arguments', () => {
|
||||
const result = parseArgs([
|
||||
'--pr-url=https://github.com/foo/bar/pull/1',
|
||||
'--pr-number=42',
|
||||
'--author=alice'
|
||||
])
|
||||
expect(result).toEqual({
|
||||
prUrl: 'https://github.com/foo/bar/pull/1',
|
||||
prNumber: '42',
|
||||
author: 'alice'
|
||||
})
|
||||
})
|
||||
|
||||
it('returns empty strings for missing args', () => {
|
||||
expect(parseArgs([])).toEqual({
|
||||
prUrl: '',
|
||||
prNumber: '',
|
||||
author: ''
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatCoverageRow', () => {
|
||||
it('formats a coverage row with delta', () => {
|
||||
const current = { percentage: 50, totalLines: 200, coveredLines: 100 }
|
||||
const baseline = { percentage: 45, totalLines: 200, coveredLines: 90 }
|
||||
|
||||
const row = formatCoverageRow('Unit', current, baseline)
|
||||
expect(row).toBe('*Unit:* 45.0% → 50.0% (+5.0%)')
|
||||
})
|
||||
|
||||
it('formats negative delta', () => {
|
||||
const current = { percentage: 40, totalLines: 200, coveredLines: 80 }
|
||||
const baseline = { percentage: 45, totalLines: 200, coveredLines: 90 }
|
||||
|
||||
const row = formatCoverageRow('E2E', current, baseline)
|
||||
expect(row).toContain('-5.0%')
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ const TARGET = 80
|
||||
const MILESTONE_STEP = 5
|
||||
const BAR_WIDTH = 20
|
||||
|
||||
export interface CoverageData {
|
||||
interface CoverageData {
|
||||
percentage: number
|
||||
totalLines: number
|
||||
coveredLines: number
|
||||
@@ -18,7 +18,7 @@ interface SlackBlock {
|
||||
}
|
||||
}
|
||||
|
||||
export function parseLcovContent(content: string): CoverageData | null {
|
||||
function parseLcovContent(content: string): CoverageData | null {
|
||||
let totalLines = 0
|
||||
let coveredLines = 0
|
||||
|
||||
@@ -44,22 +44,22 @@ function parseLcov(filePath: string): CoverageData | null {
|
||||
return parseLcovContent(readFileSync(filePath, 'utf-8'))
|
||||
}
|
||||
|
||||
export function progressBar(percentage: number): string {
|
||||
function progressBar(percentage: number): string {
|
||||
const filled = Math.round((percentage / 100) * BAR_WIDTH)
|
||||
const empty = BAR_WIDTH - filled
|
||||
return '█'.repeat(filled) + '░'.repeat(empty)
|
||||
}
|
||||
|
||||
export function formatPct(value: number): string {
|
||||
function formatPct(value: number): string {
|
||||
return value.toFixed(1) + '%'
|
||||
}
|
||||
|
||||
export function formatDelta(delta: number): string {
|
||||
function formatDelta(delta: number): string {
|
||||
const sign = delta >= 0 ? '+' : ''
|
||||
return sign + delta.toFixed(1) + '%'
|
||||
}
|
||||
|
||||
export function crossedMilestone(prev: number, curr: number): number | null {
|
||||
function crossedMilestone(prev: number, curr: number): number | null {
|
||||
const prevBucket = Math.floor(prev / MILESTONE_STEP)
|
||||
const currBucket = Math.floor(curr / MILESTONE_STEP)
|
||||
|
||||
@@ -69,7 +69,7 @@ export function crossedMilestone(prev: number, curr: number): number | null {
|
||||
return null
|
||||
}
|
||||
|
||||
export function buildMilestoneBlock(
|
||||
function buildMilestoneBlock(
|
||||
label: string,
|
||||
milestone: number
|
||||
): SlackBlock | null {
|
||||
@@ -101,7 +101,7 @@ export function buildMilestoneBlock(
|
||||
}
|
||||
}
|
||||
|
||||
export function parseArgs(argv: string[]): {
|
||||
function parseArgs(argv: string[]): {
|
||||
prUrl: string
|
||||
prNumber: string
|
||||
author: string
|
||||
@@ -120,7 +120,7 @@ export function parseArgs(argv: string[]): {
|
||||
return { prUrl, prNumber, author }
|
||||
}
|
||||
|
||||
export function formatCoverageRow(
|
||||
function formatCoverageRow(
|
||||
label: string,
|
||||
current: CoverageData,
|
||||
baseline: CoverageData
|
||||
@@ -214,6 +214,4 @@ function main() {
|
||||
process.stdout.write(JSON.stringify(payload))
|
||||
}
|
||||
|
||||
if (process.env.VITEST !== 'true') {
|
||||
main()
|
||||
}
|
||||
main()
|
||||
|
||||
@@ -18,7 +18,6 @@ const coverageStatus = getArg('coverage-status') ?? 'skip'
|
||||
/** @type {string[]} */
|
||||
const lines = []
|
||||
|
||||
// --- Size section ---
|
||||
if (sizeStatus === 'ready') {
|
||||
try {
|
||||
const sizeReport = execFileSync('node', ['scripts/size-report.js'], {
|
||||
@@ -44,7 +43,6 @@ if (sizeStatus === 'ready') {
|
||||
|
||||
lines.push('')
|
||||
|
||||
// --- Perf section ---
|
||||
if (perfStatus === 'ready' && existsSync('test-results/perf-metrics.json')) {
|
||||
try {
|
||||
const perfReport = execFileSync(
|
||||
@@ -73,7 +71,6 @@ if (perfStatus === 'ready' && existsSync('test-results/perf-metrics.json')) {
|
||||
lines.push('> ⏳ Performance tests in progress…')
|
||||
}
|
||||
|
||||
// --- Coverage section ---
|
||||
if (coverageStatus === 'ready' && existsSync('temp/coverage/coverage.lcov')) {
|
||||
try {
|
||||
const coverageReport = execFileSync(
|
||||
@@ -97,6 +94,5 @@ if (coverageStatus === 'ready' && existsSync('temp/coverage/coverage.lcov')) {
|
||||
lines.push('')
|
||||
lines.push('> ⚠️ Coverage collection failed. Check the CI workflow logs.')
|
||||
}
|
||||
// coverageStatus === 'skip' (default) — don't show section at all
|
||||
|
||||
process.stdout.write(lines.join('\n') + '\n')
|
||||
|
||||
Reference in New Issue
Block a user