# Share a Flutter Test and Coverage Report

Canonical: https://commareports.com/share-flutter-test-report
Published: 2026-08-31

> flutter test --coverage writes lcov.info, which is not a report. Convert with genhtml, publish the folder, and get annotated Dart source at a URL with threads on the uncovered branch.

# Share a Flutter test and coverage report

`flutter test --coverage` gives you `coverage/lcov.info`. That is not a
report — it is a machine format that a tool is supposed to read. Yet it is
routinely what gets uploaded as a CI artifact and then never opened, since
opening it accomplishes nothing.

The readable artifact is one conversion away.

## Convert it

```bash
flutter test --coverage || true

# strip generated code first — it dominates the numbers otherwise
lcov --remove coverage/lcov.info \
  '*.g.dart' '*.freezed.dart' '*.mocks.dart' \
  -o coverage/filtered.info

genhtml coverage/filtered.info -o coverage/html
# → coverage/html/index.html
```

`genhtml` ships with lcov (`brew install lcov`, `apt install lcov`). The
filtering step is not optional in practice: a Flutter project with
`json_serializable` and `freezed` generates a great deal of Dart, and
including it produces a coverage number that means nothing.

The output is a **directory** — `index.html`, a page per source file,
plus stylesheets — so publish the folder.

## Drop the folder in

Drag `coverage/html/` (or a zip) into [Comma](https://commareports.com/):

- `index.html` becomes the **report body** — the page carrying the comment
  layer.
- The per-file pages and CSS upload alongside it, with relative references
  rewritten, so the drill-down from summary to directory to annotated Dart
  resolves.
- Scripts run inside a sandboxed iframe (`allow-scripts`, no
  `allow-same-origin`), so the sortable columns keep working.

## From CI

```bash
curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html coverage/html/index.html \
        --arg title "Coverage — $(git rev-parse --short HEAD)" \
        '{title: $title, html: $html}')"
```

The API is JSON-only, so the per-file pages go up through
`POST /api/v1/reports/$REPORT_ID/assets` as base64 — one call per file,
worth scripting once. See the [API reference](/docs/api).

The `|| true` on the test command and an `if: always()` on the step keep
the publish alive when a test fails, which is when the report matters. Use
a [scoped token](/docs/api-tokens) (`reports:write`) from CI secrets and
`PATCH` a saved report id. See [publishing from CI](/ci).

## Test results, if you want them too

`flutter test --machine` emits a JSON event stream that JUnit converters
consume, which is the route if your pipeline wants a results view
alongside coverage:

```bash
flutter test --machine | tojunit --output junit.xml
```

See [share a JUnit report](/share-junit-report). For integration tests
driving a real device, the shape is closer to the
[Android](/share-android-test-report) and
[Xcode](/share-xcode-test-report) reports.

## What the URL changes

- **The PR links the coverage.** Reviewers click instead of running the
  suite and `genhtml` locally.
- **Review lands on the widget.** Select the uncovered branch, leave a
  thread — "this is the error state, it has no test" — and it stays
  anchored across runs. See [commenting on HTML](/comment-on-html).
- **Revision diffs beat the percentage**, especially in Flutter where the
  number swings whenever the generated-code filter changes.

## Who can see it

The coverage report **contains your Dart source**. Access is per report —
private, your team, any signed-in user with the link, or public — and team
or private is the right default for a private repo. Domain-gating is
Enterprise. See [sharing & access control](/docs/sharing).

## Limits

- **Entry HTML: 5 MB.** Assets: 25 MB per file, 250 MB and 500 files
  total. A large app can exceed 500 source pages — publish the feature
  directory under review rather than the whole tree.
- **Scripts run, sandboxed** — no same-origin access.
- **60 requests/minute per token.**

## Try it

Comma is free — unlimited reports, unlimited commenters, unlimited
revision history.

**[Publish a coverage report →](https://commareports.com/)**

### Related

- [Share an lcov report](/share-lcov-report) · [Share a coverage report](/share-coverage-report)
- [Share an Android test report](/share-android-test-report) · [Share an Xcode test report](/share-xcode-test-report)
- [Share a JUnit report](/share-junit-report) · [Publish from CI](/ci)
