skip to content

What does Playwright's forbidOnly config option protect a CI run from?

level: middleimportance: must knowfreq 66%

answer

  1. Guards against a committed focus marker
  2. Checked before any test executes
  3. Usually enabled only under CI
  4. Refuses the run, does not repair it
  5. Says nothing about skip or grep

basics

~20 s

Playwright's forbidOnly makes a run fail when any test or group is marked with test.only. It stops a focused marker committed by accident from silently shrinking a CI suite to a single test that then reports green.

solid answer

~40 s

`test.only` is a local debugging tool: it tells the runner to execute that test and skip its neighbours. Committed by accident, it turns a thousand-test payroll suite into a one-test run that still exits zero, so the pipeline reports a green stage on almost no coverage. `forbidOnly: true` closes that hole — Playwright checks the collected tests before executing anything, and if a `test.only` or `test.describe.only` is present it errors out and fails the run instead of honouring the focus. The generated config wires it to the environment, `forbidOnly: !!process.env.CI`, so developers keep `.only` locally while CI refuses it. There is a `--forbid-only` CLI flag for the same check on demand.

code

typescript · 7 lines
typescript
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests/payroll',
  // Focusing a test locally is fine; committing the marker is not.
  forbidOnly: !!process.env.CI,
});

go deeper

for a junior

Know what test.only does first: it runs that test and skips the rest. forbidOnly is the config option that makes a run fail when such a marker is committed, and it is normally switched on for CI only.

for a middle

Explain the mechanics: the check runs after collection and before execution, so nothing launches, and the run exits non-zero with the offending file named. Note that it does not touch skip, fixme or grep filters.

for a senior

Frame the risk it removes: without it a stray marker leaves a green stage over one executed test. Pair it with attention to test counts in the report, since focus is only one of several ways a run can silently shrink.

for a principal

Decide the guard policy across suites: which environments refuse focus, what else is enforced at collection time, and how the team notices coverage collapse that no single option catches.

`forbidOnly` exists because of one specific accident: a focused test that reaches the main branch. It is a small option that guards a large failure mode, which is why it appears in almost every serious Playwright config review. ## The failure mode it prevents `test.only(...)` and `test.describe.only(...)` tell the runner to run *that* test or group and skip everything else in the run. Locally that is exactly what you want while iterating on one payroll calculation. Committed, it is silent damage: - The suite still runs. The pipeline stage still passes. The exit code is still zero. - But the run executed one test, not the thousand that stage was supposed to defend. - Nothing in a green check tells a reviewer that coverage collapsed — the only clue is a test count nobody reads. A regression suite that quietly stops regressing is worse than no suite, because the team keeps trusting it. ## How Playwright enforces it With `forbidOnly` enabled, the runner collects the test files first and inspects the resulting tree **before** executing anything. If any focused test or group survives collection, it does not run the focused test: it reports an error identifying the offending item and fails the run with a non-zero exit code. Two consequences follow from the check happening at collection time: 1. The failure is fast and cheap — no browser is launched, so the guard costs nothing on a healthy branch. 2. The failure is *about the code*, not about a test outcome. Reading the error, you look for a stray `.only`, not for a bug in payroll. ## Where it is set | Form | Scope | Typical use | |---|---|---| | `forbidOnly: true` | config, every run | Suites where `.only` must never appear | | `forbidOnly: !!process.env.CI` | config, CI only | The scaffolded default: allow focus locally, forbid it in CI | | `--forbid-only` | one invocation | Checking a branch on demand, or a lint-style job | The environment-driven form is the one the `npm init playwright` scaffold generates, and it is the shape most teams keep: focusing a test is a legitimate local workflow, so banning it outright makes developers fight the tool. ## What it does not cover `forbidOnly` is narrow on purpose. It says nothing about the other ways a suite can shrink: - `test.skip` and `test.fixme` remove tests from the run, and `forbidOnly` ignores both — they are deliberate annotations, not accidents. - A `--grep` filter or a `grep` config entry can select a tiny subset; that is a selection decision, not a focus marker. - A file that fails to match `testMatch` is never collected at all, so there is nothing to forbid. If the worry is "did this run actually execute the tests we think it did?", the honest check is the test count in the report, not this option alone. ## Reviewing for it Interviewers often push toward the belt-and-braces answer, so be ready with the layered version: - Keep `forbidOnly` on in CI so a focused marker fails loudly rather than passing quietly. - Expect the failure message to name the file and line, which makes the fix a one-line revert. - Remember the flag form, `--forbid-only`, for a job that wants the check without changing the shared config. ## Common misreadings - Believing `forbidOnly` deletes or ignores the `.only` marker and runs the full suite anyway. It does not repair the run; it refuses it. - Believing it fails only after the focused test has run. The check precedes execution. - Assuming it also catches `.skip`. Those are separate annotations with separate intent.

  • Why is forbidOnly usually tied to process.env.CI instead of being on everywhere?
    Because `test.only` is a legitimate local workflow: it is how you iterate on one test without paying for the suite. Enabling the guard everywhere makes developers work around the tool. Tying it to the CI variable keeps focus available where it helps and refused where it does harm.
  • Does forbidOnly also catch tests marked with test.skip or test.fixme?
    No. It only rejects focus markers, `test.only` and `test.describe.only`. Skip and fixme are deliberate annotations that remove a test from the run on purpose, so the option ignores them. If you want to police those, that is a review or reporting concern, not this flag.

saying these in an interview costs you the question

  • Thinks forbidOnly ignores the marker and runs everything
  • Says the run fails only after the focused test executes
  • Believes it also rejects skipped tests
  • Claims a focused test makes CI fail on its own
  • Enables it locally and blocks normal debugging