Visibility vs. Presence: The Wait Condition Bug

Presence and visibility are not the same wait condition. Learn the difference in Selenium, Playwright and Cypress, with a hands-on practice page.

Your test passes on one page and fails on the next — same framework, same "wait for element." Here is why.

A suite passes on staging, fails on production. Same framework, same "wait for the element" call — different result. Nine times out of ten, the cause is the same: the test is waiting for the wrong thing. There are two distinct conditions hiding behind the phrase "wait for the element," and mixing them up is one of the most common sources of flaky UI tests.

Presence vs. visibility

Presence means the element exists somewhere in the DOM. It might be invisible — display: none, visibility: hidden, zero-size, or just off-screen — but a query for it will find it and a presence-based wait will resolve immediately.

Visibility means the element is present and actually rendered in a way a real user could see and click: non-zero size, not hidden by CSS, not covered. A visibility-based wait keeps polling until all of that is true.

The two conditions produce identical results for elements that are simply appended to the DOM already visible. They diverge — and break tests — for the pattern where a spinner finishes and a hidden element becomes visible, versus the pattern where nothing existed until the operation completed and the element is inserted fresh. A test written against the first pattern and run against the second (or vice versa) waits for the wrong signal and either times out or races the page and passes by luck.

What it looks like per framework

Selenium keeps the two as separate, explicitly-named expected conditions:

# Presence: element exists in the DOM, may still be invisible
WebDriverWait(driver, 5).until(
    EC.presence_of_element_located((By.ID, "dynamic-loading-finish-1"))
)

# Visibility: element exists AND has non-zero size / no hidden styling
WebDriverWait(driver, 5).until(
    EC.visibility_of_element_located((By.ID, "dynamic-loading-finish-1"))
)

Playwright's locator actions auto-wait for visible by default, which quietly does the right thing most of the time — the trap is reaching for state: 'attached' out of habit and getting presence when you meant visibility:

// waitFor() defaults to 'visible' — this is what most locator actions
// (.click(), .fill()) wait for automatically before acting.
await page.locator('#dynamic-loading-finish-1').waitFor({ state: 'visible' });

// Ask for presence only when you explicitly don't care about visibility —
// e.g. asserting something was inserted into the DOM at all.
await page.locator('#dynamic-loading-finish-2').waitFor({ state: 'attached' });

Cypress's cy.get() retries for presence only — visibility is a separate assertion you chain on top:

// cy.get() retries until the element exists in the DOM (presence).
// Chaining .should('be.visible') adds the visibility check on top —
// without it, a hidden element still passes.
cy.get('#dynamic-loading-finish-1').should('be.visible');
cy.get('#dynamic-loading-finish-2').should('exist');

A two-question checklist

Next time a wait-related test is flaky, ask: is the element already in the DOM and just changing CSS visibility, or is it not in the DOM at all until the operation finishes? Then match your wait condition to that answer, not to whichever one your editor autocompleted first. When in doubt, inspect the DOM mid-operation in devtools — if the element is there with display: none, it's a visibility wait; if it's simply absent, it's a presence wait.

The Transporter Room challenge on this site is built to make the difference impossible to ignore: Example 1 hides an already-present element, Example 2 inserts one fresh, and a test that only handles one of them fails the other on purpose. It's also one of the pages covered by the `?delay=` speed knob in the Getting Started guide, so you can dial the wait down while iterating on the assertion.

Keep reading

Updated

Fun UI Playground — all challenges, bug hunts, getting started, glossary, about.