In Playwright, how does expect(locator).toBeHidden() differ from expect(locator).not.toBeVisible()?
answer
- Both are retrying assertions
- Think about the empty locator result
- One reads positive, one reads negated
- Compare the two failure lines
- What neither of them proves
basics
~20 sIn outcome they are the same. Both retry until the element is hidden or absent from the DOM, and both pass when it never existed at all. The difference is readability and the failure message, not semantics.
solid answer
~40 sThey are two spellings of the same web-first check. Both poll the locator until the `expect` timeout expires, and both treat an empty result as success: Playwright special-cases the missing element so that `toBeHidden()` passes and `not.toBeVisible()` passes when the locator matches nothing. What differs is the report — `toBeHidden()` fails with an expected/received pair phrased around hidden versus visible, the negated form with an expectation phrased as *not visible* — and readability. On a bank statement page, `await expect(page.getByTestId('export-spinner')).toBeHidden()` beats the double negative. The point worth making in an interview is what neither matcher tells you: that the spinner was ever there. Both are satisfied by a page that failed to render, so pair either one with a positive anchor.
code
typescript · 10 linesimport { expect, test } from '@playwright/test';
test('export spinner clears once the CSV is ready', async ({ page }) => {
await page.goto('/statements/2026-08');
await expect(page.getByRole('table', { name: 'Transactions' })).toBeVisible();
await page.getByRole('button', { name: 'Export CSV' }).click();
await expect(page.getByTestId('export-spinner')).toBeVisible();
await expect(page.getByTestId('export-spinner')).toBeHidden();
});go deeper
Remember that both spellings pass for a hidden element and for one that is missing entirely, and that Playwright retries them rather than checking once. Being able to say that plainly is enough at this stage.
Explain the empty-result rule: on each poll the locator is resolved, and an empty result is reported as hidden, so the check succeeds without the element ever having existed. Mention that only the failure messages differ.
Show that you standardise on one spelling across the suite and that you never let an absence check stand alone, because a page that failed to render satisfies it. Name the positive anchor you would put in front of it.
Own the convention rather than the trivia. A review rule that every absence assertion sits next to a positive one costs far less than triaging a suite that has been green for months for the wrong reason.
## Two spellings of one check `expect(locator).toBeHidden()` and `expect(locator).not.toBeVisible()` are both **web-first assertions**. Playwright re-resolves the locator and re-evaluates the condition in a polling loop until it holds or the `expect` timeout expires (5 seconds unless the project config raises it). Neither takes a single snapshot of the page and judges it once, which is why neither needs a manual wait in front of it, and why a failure arrives as a timeout with a call log rather than as an instant mismatch. The condition they share is Playwright's own definition of visibility, which is **not** the CSS `visibility` property. An element is visible when it is attached to the document, has a non-empty bounding box, and is not `visibility: hidden`. Everything else counts as hidden: - `display: none` on the element or on any ancestor - `visibility: hidden`, set directly or inherited - a bounding box of zero width or zero height, such as an empty inline `<span>` - **nothing matching the locator at all** Two things deliberately do *not* make an element hidden: `opacity: 0`, and being scrolled outside the viewport. Both are visible as far as these matchers are concerned, so an export spinner faded to zero opacity will fail an absence check that its author expected to pass. ## The empty-result rule That last bullet decides most interview answers. On every poll Playwright resolves the locator, and when the result is empty there is no element to read a property from — so each matcher needs a defined answer for the empty case. The visibility and attachment family has one, and it is the intuitive one: nothing is hidden, and nothing is not visible. | Assertion | Element missing | Element attached but hidden | |---|---|---| | `toBeHidden()` | passes | passes | | `not.toBeVisible()` | passes | passes | | `not.toBeAttached()` | passes | fails after retrying | | `toHaveCount(0)` | passes | fails after retrying | | `not.toHaveText('Pending')` | fails after retrying | compares the text | The first two rows are identical in every column. `toBeHidden()` and `not.toBeVisible()` are interchangeable in outcome: there is no page state where one passes and the other fails. ## What actually differs 1. **Direction of the report.** `toBeHidden()` fails with an expected/received pair phrased around hidden versus visible, while the negated form phrases the expectation as *not visible*. In a long CI log the positive phrasing is quicker to scan. 2. **Readability under review.** `not.toBeVisible()` is a double negative in a test that is already about something disappearing, and double negatives are exactly what a reviewer skims past. 3. **Greppability.** A suite that mixes both spellings — plus the third one, `toBeVisible({ visible: false })` — cannot be audited for absence checks with a single search. None of those differences are behavioural. Pick one spelling per suite and hold the line in review; `toBeHidden()` is the usual choice because it reads as a statement rather than a denial. ## The trap both of them share Because an empty result passes, an absence check says nothing about history. On a bank statement page, `await expect(page.getByTestId('export-spinner')).toBeHidden()` is satisfied by all of these: - a spinner that appeared while the CSV was generated and then finished — the intended case - a spinner that never appeared because the export button was never wired to anything - a request that returned 500, so the app rendered an error shell instead of the statement - a typo in the test id, which matches nothing on any page in the application Three of those four are defects, and the assertion is green for all four. This is the failure mode the family is known for: the check passes in about a millisecond on a page that rendered nothing at all. ## Closing the gap The fix is ordering, not matcher choice. Put a **positive anchor** in front of every absence check — an assertion that can only hold if the page reached the state you are about to watch it leave: - assert the container rendered: `await expect(page.getByRole('table', { name: 'Transactions' })).toBeVisible()` - assert the thing you expect to vanish first appeared: `await expect(spinner).toBeVisible()` before `await expect(spinner).toBeHidden()` - or replace the negation with a number: `await expect(rows).toHaveCount(12)` fails on an unrendered page, because zero is not twelve And if the requirement is *removal from the DOM* rather than *not shown to the user*, neither of these two matchers is the right tool. `not.toBeAttached()` is, because it keeps failing while the node is still mounted, however invisible it happens to be.
- How would you assert that the balance widget is present in the DOM but not shown to the user?Two assertions on the same locator: `await expect(balance).toBeAttached()` and `await expect(balance).toBeHidden()`. The first proves the node exists, the second proves it is not rendered. Collapsing them into a single negation loses the distinction, because a missing node satisfies `toBeHidden()` on its own.
- Why is toBeHidden() usually the better spelling to standardise on?It states intent positively and fails with a message phrased in the same direction, which is faster to scan in a CI log. Double negatives are easy to misread in review, and a suite that mixes both spellings cannot be audited for absence checks with one search.
saying these in an interview costs you the question
- Claims not.toBeVisible fails when the element is missing entirely.
- Thinks toBeHidden only checks the CSS visibility property.
- Believes toBeHidden proves the element existed and then went away.
- Treats an element scrolled out of the viewport as hidden.
- Assumes the two matchers use different waiting mechanisms.