skip to content

What does Playwright's test.fixme() do to a test that is known to be broken?

level: middleimportance: should knowfreq 36%

answer

  1. Marks a test as known broken
  2. Reported as skipped, never failed
  3. Three forms including a conditional one
  4. Spends none of the failure budget
  5. Different intent from not-applicable

basics

~20 s

In Playwright, test.fixme() stops the test from running and reports it as skipped rather than failed, so a known-broken payroll test keeps its code and its name in the suite without turning the run red.

solid answer

~40 s

`test.fixme()` marks a test as broken and not worth running yet. Declared as `test.fixme('title', fn)` the body never executes; called *inside* a body, execution stops at that line and the test is marked fixme from there on; and the conditional form `test.fixme(condition, description)` applies it only when the condition holds, which is how a test is parked for one browser or one environment. Reporters count it in the skipped bucket, so it does not fail the run or consume a failure budget. The difference from `test.skip()` is intent rather than mechanics: skip says *not applicable here*, fixme says *this should pass and currently does not*. Both keep the test compiling and visible, unlike commenting it out.

code

typescript · 14 lines
typescript
import { test, expect } from '@playwright/test';

// Declaration form: the body never runs.
test.fixme('payroll exports a P60 PDF', async ({ page }) => {
  await page.goto('/payroll/year-end');
  await expect(page.getByRole('link', { name: 'P60' })).toBeVisible();
});

test('payslip totals reconcile', async ({ page, browserName }) => {
  // Conditional form: parked on WebKit only, with a stated reason.
  test.fixme(browserName === 'webkit', 'currency formatting differs, see payroll backlog');
  await page.goto('/payroll/payslips');
  await expect(page.getByTestId('net-total')).toHaveText('1,240.00');
});

go deeper

for a junior

Know that the annotation stops the test from running and reports it as skipped rather than failed, and that the test stays in the file with its name intact instead of being commented out.

for a middle

Explain the three forms, declaration, inline and conditional, and what each does to execution. Be able to say why the report shows skipped, and contrast the intent behind fixme with that behind skip.

for a senior

Show the operational angle: an un-annotated broken test can eat a small failure budget and truncate a run, while an annotated one is silent and therefore invisible in a green check. Always record the reason in the description.

for a principal

Own the consequence rather than the syntax: parked tests are coverage the team has stopped paying for, and a green run over many of them misrepresents confidence unless somebody is accountable for the list.

`test.fixme()` is the annotation for a test you believe in but cannot currently make pass. It removes the test from the run without removing it from the codebase, and — importantly for a fail-fast discussion — without spending any of the run's failure budget. ## The three forms 1. **Declaration form** — `test.fixme('payroll exports a P60', async ({ page }) => { ... })`. The body never runs at all. Use it when you already know the test is broken before the run starts. 2. **Imperative form** — calling `test.fixme()` inside a running test. Execution stops at that statement; nothing after it runs, and the test is reported as fixme. Use it when the decision depends on something you only learn at runtime. 3. **Conditional form** — `test.fixme(condition, 'description')`, which applies the annotation only when the condition is true. This is how a test is parked for one browser or one environment while still running everywhere else. ## What the report shows - A fixme test lands in the **skipped** bucket of the reporters, not the failed one. - Because it is not a failure, it does not count towards a `maxFailures` budget and cannot trigger a fail-fast abort. - The run's exit code is unaffected by it, so a suite full of fixme tests still exits zero. - The test keeps its title, so it stays visible in every report rather than vanishing the way a commented-out block does. That last point is the argument for the annotation over deletion-by-comment: the name still appears, the code still compiles, and a rename or refactor of the page objects it uses still touches it. ## fixme, skip and slow | Annotation | Meaning | Effect on the run | |---|---|---| | `test.fixme()` | Should pass, currently does not | Not executed; reported as skipped | | `test.skip()` | Not applicable in this configuration | Not executed; reported as skipped | | `test.slow()` | Legitimately long-running | Executed, with a widened time allowance | The first two are mechanically similar and differ in intent, which is exactly what an interviewer is probing: a candidate who uses `skip` for everything erases the distinction between "this does not apply to Firefox" and "this is broken and someone owes it a fix". `test.slow()` belongs in the same conversation for the opposite reason. A slow payroll report test does not need parking; it needs room. Marking it slow widens its own allowance instead of pushing the whole suite's limits up, which keeps the run-level budget meaningful. Note that it applies to a test, so it does not belong in a `beforeAll` or `afterAll` hook. ## Why it matters to a fail-fast discussion Fail-fast controls count failures. Annotations decide what counts as one: - A genuinely broken test left un-annotated fails on every run, and with a small failure budget it can truncate the run before the tests you actually wanted to see have executed. - The same test marked fixme is silent, and the budget is spent on real regressions. - The cost is that the annotation is invisible in a green check — the run is green precisely because a real test is not running. ## Practical notes - Prefer the conditional form over duplicating a test for one environment; one definition with a condition is easier to unpark. - Put the reason in the description argument. "fixme" with no reason ages into a mystery that nobody dares delete. - Do not reach for it as a way to make a red pipeline green under pressure without a decision behind it; that decision — who owns the parked test and what brings it back — is a suite-management question, not a flag. ## Common misreadings - Believing a fixme test is retried, or is reported as flaky. It is not executed. - Believing it fails the run. It does not, which is the whole point. - Believing statements after an inline `test.fixme()` still run. They do not.

  • How does test.fixme() differ from test.skip() if both leave the test unexecuted?
    Mechanically they are close: neither runs, and both land in the skipped bucket. The difference is intent. `skip` says the test does not apply to this browser, project or environment. `fixme` says it should pass and currently does not, so somebody owes it a fix. Losing that distinction makes a report unreadable.
  • What happens to the statements after an inline test.fixme() call inside a test body?
    They do not run. The call stops execution at that point and the test is reported as fixme, so anything after it is dead code for that run. That is why the imperative form is placed as early as the decision allows, usually right after the value it depends on becomes known.

saying these in an interview costs you the question

  • Thinks a fixme test is retried before being skipped
  • Says it fails the run and exits non-zero
  • Believes code after an inline fixme still executes
  • Uses it interchangeably with skip, erasing intent
  • Assumes it counts against a failure budget
  • Marks a slow test fixme instead of giving it room