Skip to content

playwright-coverage-threshold has never gated: warning-only, not line coverage, and zero scenarios scores 100% #189

Description

@rubenvdlinde

Summary

playwright-coverage-threshold is documented as an enforced minimum. It has never been able to fail a run. Separately, the number it compares against the threshold does not measure what its name says, and it returns a perfect score for a repo with no spec scenarios at all.

Three defects, in .github/workflows/quality.yml@main, "Generate spec-to-test coverage report" step.

1. The threshold cannot fail anything

Lines 2659-2661:

if (report.coverage < ${{ inputs.playwright-coverage-threshold }}) {
  console.log('::warning::Spec-to-test coverage ' + report.coverage + '% is below threshold ' + ... + '%');
}

::warning:: and no non-zero exit. The branch is taken, a warning is annotated, and the job continues green.

Measured: pipelinq sets playwright-coverage-threshold: 75 and its Code Quality run 31082256226 printed

Spec-to-test coverage: 28%
##[warning]Spec-to-test coverage 28% is below threshold 75%

28 against a declared floor of 75, and the setting contributed nothing to the verdict. A repo can set any threshold it likes; the value is decorative.

The input description (line 146) reads "Minimum line coverage percentage for Playwright tests (0-100). Only enforced when enable-playwright-coverage is true." — which tells a reader that enabling the flag turns enforcement on. It does not.

2. It is not line coverage

Line 2647:

coverage: scenarios.length > 0 ? Math.round((tests.length / scenarios.length) * 100) : 100,

That is count(test(...) calls) / count(spec scenarios) — a ratio of two independent totals. It is not line coverage, and it is not coverage in any sense: it never checks that any test corresponds to any scenario. Adding ten unrelated test() calls raises the number exactly as much as covering ten scenarios does. The metric can exceed 100% while covering nothing.

The two sides are also collected by regex over source text — scenarios by /^###?\s+(S\d+|Scenario[:\s]|REQ-)[^\n]*/gm and tests by /test\([']([^']+)/g — so a commented-out test( counts, and a scenario heading that does not match the pattern does not.

3. Zero scenarios scores 100%

The ternary's else branch is : 100. A repo whose openspec/specs/ and openspec/changes/ yield no matching headings — because the directory is absent, empty, or its headings use a different form — reports 100% coverage. The worst-covered possible repo and the best-covered one produce the same number, and the worse one produces it without running anything.

This is the standard dead-gate shape: the failure mode and the success mode are indistinguishable in the output.

Suggested fix

  1. Decide whether this gate enforces. If it does, ::error:: and a non-zero exit; if it does not, remove playwright-coverage-threshold rather than shipping a knob that does nothing, and reword the description.
  2. Rename the metric to what it is (tests-per-scenario), or make it real by matching tests to scenarios — gate-19 (@e2e traceability) already does per-scenario matching and could supply the mapping.
  3. scenarios.length === 0 must not score 100. Report it as "no scenarios found" and, if the gate enforces, treat it as a failure to measure rather than a pass.

Whatever is chosen, it needs a test that shows the gate CAN fail — a fixture with coverage below the threshold that turns the job red. Right now no such run can exist.

Found while burning down E2E failures across pipelinq / decidesk / openbuild / nldesign. Related: #188 (Playwright trace and globalTimeout instrument defects).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions