Resource library

QA How-To

Playwright expect toBeVisible: Examples and Best Practices

Explore playwright expect toBeVisible examples for menus, dialogs, loading states, tabs, redirects, responsive layouts, iframes, and page objects reliably.

25 min read | 2,261 words

TL;DR

Reliable Playwright visibility examples trigger the UI change first, then use `await expect(locator).toBeVisible()` on a specific semantic outcome. Scope repeated elements, assert positive content after loading, and use separate matchers for enabled state, viewport position, or DOM removal.

Key Takeaways

  • Structure visibility tests as user action, semantic locator, visible outcome, and business proof.
  • Scope modal, card, row, navigation, and frame assertions to the intended container.
  • Pair spinner disappearance with a positive ready-state assertion.
  • Combine semantic state and visibility for tabs, accordions, menus, and dialogs.
  • Use project-specific responsive expectations instead of positional duplicate selectors.
  • Choose toBeHidden for hidden-or-removed behavior and toHaveCount(0) for required DOM removal.
  • Avoid separate visibility checks before actions unless visibility is itself a requirement.

Playwright expect toBeVisible examples should model real state transitions, not add decorative waits before every click. The best examples perform a user action, locate the specific outcome through accessible semantics, and await expect(locator).toBeVisible() with a timeout that matches the product behavior.

This cookbook covers common UI flows: menus, dialogs, loading results, toasts, tabs, accordions, redirects, repeated cards, responsive navigation, frames, and reusable page objects. Every pattern uses Playwright Test locator assertions, which re-resolve and retry instead of taking one fragile visibility snapshot.

TL;DR

UI flow Trigger Visibility target Follow-up proof
Menu Click named menu button Named navigation or menu Expected link is usable
Dialog Click action button Named dialog Dialog-specific control exists
Search Submit query Results heading or region Count or expected item
Toast Save data Role status or alert Correct message text
Tab Click named tab Matching tabpanel Tab selected state
Redirect Click link or submit Destination heading URL and page identity
Responsive UI Set project viewport Active named navigation Correct control is unique
Iframe Trigger embedded state Frame-scoped locator Frame content result

A visibility assertion is strongest when its locator names the business outcome. "Order confirmed" is more useful than ".container is visible."

1. Basic Playwright expect toBeVisible Examples With a Menu

This self-contained test demonstrates the complete arrange, act, assert pattern. The menu starts hidden, a user click reveals it, and the assertion verifies the named navigation.

import { test, expect } from '@playwright/test';

test('opens the account menu', async ({ page }) => {
  await page.setContent(`
    <button type="button" aria-expanded="false" aria-controls="account-menu">
      Account
    </button>
    <nav id="account-menu" aria-label="Account menu" hidden>
      <a href="/profile">Profile</a>
      <a href="/settings">Settings</a>
    </nav>
    <script>
      const trigger = document.querySelector('button');
      const menu = document.querySelector('nav');
      trigger.addEventListener('click', () => {
        menu.hidden = false;
        trigger.setAttribute('aria-expanded', 'true');
      });
    </script>
  `);

  const trigger = page.getByRole('button', { name: 'Account' });
  const menu = page.getByRole('navigation', { name: 'Account menu' });

  await expect(menu).toBeHidden();
  await trigger.click();

  await expect(trigger).toHaveAttribute('aria-expanded', 'true');
  await expect(menu).toBeVisible();
  await expect(menu.getByRole('link', { name: 'Settings' })).toBeVisible();
});

The example checks more than CSS visibility. The expanded attribute proves control state, the region assertion proves the menu appeared, and the scoped link assertion proves relevant content is exposed.

In an end-to-end test, continue by clicking the link and asserting the navigation outcome. Do not use a fixed delay between the trigger and toBeVisible(). The matcher already retries the condition.

For locator selection patterns, see the Playwright getByRole guide.

2. Dialog and Modal Examples

A modal example should locate the open dialog by accessible name, then scope all assertions and actions inside it. This avoids matching hidden dialog templates or same-named controls on the page underneath.

import { test, expect } from '@playwright/test';

test('opens and cancels the delete dialog', async ({ page }) => {
  await page.goto('/projects/PRJ-71');

  await page.getByRole('button', { name: 'Delete project' }).click();

  const dialog = page.getByRole('dialog', { name: 'Delete project?' });
  await expect(dialog).toBeVisible();

  await expect(
    dialog.getByText('This action cannot be undone.'),
  ).toBeVisible();

  await dialog.getByRole('button', { name: 'Cancel' }).click();
  await expect(dialog).toBeHidden();

  await expect(
    page.getByRole('heading', { name: 'Project PRJ-71' }),
  ).toBeVisible();
});

toBeHidden() accepts either a non-visible element or no matching element, which suits dialog libraries that unmount on close and those that keep hidden content mounted.

If the modal has an opening animation, do not assert internal controls through a global locator. Scoping inside the dialog tells Playwright which rendered instance matters. The later click performs its own actionability checks.

A destructive confirmation test should not proceed when the expected warning is missing. Keep that warning hard. Soft visibility checks are inappropriate when they would allow a delete action through an invalid confirmation state.

3. Loading Spinner to Results Pattern

Waiting only for a spinner to disappear can create a false sense of readiness. The spinner might vanish because a request failed. Assert the positive result state after the negative loading state.

import { test, expect } from '@playwright/test';

test('loads matching orders', async ({ page }) => {
  await page.goto('/orders');

  await page.getByLabel('Search orders').fill('ORD-105');
  await page.getByRole('button', { name: 'Search' }).click();

  const loading = page.getByRole('progressbar', { name: 'Loading orders' });
  await expect(loading).toBeVisible();
  await expect(loading).toBeHidden();

  const results = page.getByRole('region', { name: 'Order results' });
  await expect(results).toBeVisible();
  await expect(
    results.getByRole('link', { name: 'Order ORD-105' }),
  ).toBeVisible();
});

The initial spinner assertion belongs only when the requirement guarantees that users see it. Very fast responses can make a transient indicator unobservable, which creates a race in tests. If spinner appearance is not required, omit the first assertion and wait directly for the results.

A business-ready locator is better than a generic network-idle strategy. Modern applications often maintain background connections, analytics, or polling, while the relevant results are already usable.

The Playwright flaky test fixes guide explains how observable UI state reduces timing races.

4. Toast, Alert, and Status Message Examples

Transient messages require a stable semantic locator and an assertion immediately after the triggering action. Use role status for polite updates and role alert for urgent messages when the application markup provides them.

import { test, expect } from '@playwright/test';

test('shows save confirmation', async ({ page }) => {
  await page.goto('/profile');

  await page.getByLabel('Display name').fill('Ava Patel');
  await page.getByRole('button', { name: 'Save changes' }).click();

  const status = page.getByRole('status');
  await expect(status).toBeVisible();
  await expect(status).toHaveText('Profile saved');

  await expect(page.getByLabel('Display name')).toHaveValue('Ava Patel');
});

If the toast automatically disappears, avoid testing its disappearance unless that behavior is a requirement. Appearance plus correct content and persisted form state often provides better value.

A broad getByText('Saved') can match hidden accessibility text, old notifications, or multiple components. A role-based locator captures the intended notification channel. If several statuses exist, scope to the relevant form or use a specific accessible name.

For an error:

await page.getByRole('button', { name: 'Submit' }).click();

const alert = page.getByRole('alert');
await expect(alert).toBeVisible();
await expect(alert).toContainText('Email address is required');

Do not use waitForTimeout to catch a short-lived toast. If it disappears too quickly for a web-first assertion under normal conditions, the product may also be difficult for users to perceive.

5. Tabs and Accordions

Tabs and accordions combine visible content with semantic state. Assert both when the behavior contract includes both.

import { test, expect } from '@playwright/test';

test('opens the billing tab', async ({ page }) => {
  await page.goto('/settings');

  const billingTab = page.getByRole('tab', { name: 'Billing' });
  const billingPanel = page.getByRole('tabpanel', { name: 'Billing' });

  await billingTab.click();

  await expect(billingTab).toHaveAttribute('aria-selected', 'true');
  await expect(billingPanel).toBeVisible();
  await expect(
    billingPanel.getByRole('heading', { name: 'Billing settings' }),
  ).toBeVisible();
});

The panel assertion proves visible content, while the attribute asserts correct tab semantics. A product can accidentally show a panel without updating accessible selected state.

For an accordion:

const trigger = page.getByRole('button', { name: 'Shipping details' });
const details = page.getByRole('region', { name: 'Shipping details' });

await trigger.click();
await expect(trigger).toHaveAttribute('aria-expanded', 'true');
await expect(details).toBeVisible();

Avoid selectors based on .active or .show when roles and state attributes express the user contract. CSS classes are implementation details unless visual styling itself is under test.

6. Redirect and Destination Page Visibility

After a navigation action, combine URL and visible page identity. The URL proves routing, while the heading proves that the destination rendered successfully.

import { test, expect } from '@playwright/test';

test('opens order details from search results', async ({ page }) => {
  await page.goto('/orders?query=ORD-105');

  await page.getByRole('link', { name: 'Order ORD-105' }).click();

  await expect(page).toHaveURL(/\/orders\/ORD-105$/);
  await expect(
    page.getByRole('heading', { name: 'Order ORD-105' }),
  ).toBeVisible();
  await expect(
    page.getByRole('region', { name: 'Order summary' }),
  ).toBeVisible();
});

Do not rely on URL alone. Client-side routers can update the address before data loads, and error boundaries can render at the correct path. Conversely, checking only a common "Orders" heading may pass on a list page rather than the intended detail page.

Use the most specific stable identity available: order number, customer-approved title, or unique region name. If the app localizes headings, seed locale explicitly in the test project and assert the localized contract expected for that project.

7. Repeated Cards and Table Rows

Repeated content needs container scoping. Locate the card or row that represents the record, then assert a control inside it.

import { test, expect } from '@playwright/test';

test('shows actions for the selected product', async ({ page }) => {
  await page.goto('/catalog');

  const card = page.getByRole('article').filter({
    has: page.getByRole('heading', { name: 'Mechanical Keyboard' }),
  });

  await expect(card).toBeVisible();
  await expect(card.getByText('$129.00', { exact: true })).toBeVisible();
  await expect(
    card.getByRole('button', { name: 'Add to cart' }),
  ).toBeVisible();
});

For a table, identify a row with a unique cell:

const row = page.getByRole('row').filter({
  has: page.getByRole('cell', { name: 'ORD-105', exact: true }),
});

await expect(row).toBeVisible();
await expect(row.getByRole('cell', { name: 'Paid' })).toBeVisible();

Be aware that virtualized lists may mount only visible records. A locator for an unmounted row cannot become visible until the product scrolls or loads it. Use the application's paging, search, or scroll behavior, then assert the result.

If collection size matters, pair the visibility check with toHaveCount() or an array text assertion. Visibility of one item does not prove the complete list.

8. Responsive Navigation and Legitimate Alternatives

Desktop and mobile layouts can expose different controls. Prefer deterministic device projects, then assert the control expected for that project.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'desktop-chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'mobile-chromium',
      use: { ...devices['Pixel 7'] },
    },
  ],
});

A mobile test can be explicit:

test('opens mobile navigation', async ({ page }) => {
  await page.goto('/');

  await page.getByRole('button', { name: 'Open menu' }).click();

  const navigation = page.getByRole('navigation', {
    name: 'Mobile primary',
  });
  await expect(navigation).toBeVisible();
  await expect(
    navigation.getByRole('link', { name: 'Resources' }),
  ).toBeVisible();
});

When one test intentionally accepts one of two visible alternatives, locator composition can express it:

const signInEntry = page
  .getByRole('link', { name: 'Sign in' })
  .or(page.getByRole('button', { name: 'Sign in' }))
  .first();

await expect(signInEntry).toBeVisible();

Use first() because both alternatives might exist. If the product requires exactly one, assert cardinality or separate project expectations rather than silently accepting both.

9. Frame-Scoped Visibility Example

Content inside an iframe must be located through a frame locator. A page-level locator does not search inside the frame document.

import { test, expect } from '@playwright/test';

test('shows the hosted payment form', async ({ page }) => {
  await page.goto('/checkout');

  const paymentFrame = page.frameLocator(
    'iframe[title="Secure payment form"]',
  );

  const cardNumber = paymentFrame.getByLabel('Card number');
  const payButton = paymentFrame.getByRole('button', { name: 'Pay now' });

  await expect(cardNumber).toBeVisible();
  await expect(payButton).toBeVisible();
});

The iframe element itself and its inner content are different targets. If diagnosis suggests the frame never mounted, assert the page-level iframe locator first:

await expect(
  page.locator('iframe[title="Secure payment form"]'),
).toBeVisible();

Then check frame content. A third-party payment form may load slowly, so use a targeted timeout only if its service-level expectation genuinely differs from the rest of the application.

Do not bypass the frame with DOM evaluation. Frame locators preserve user-oriented interactions and normal assertion diagnostics.

10. Negative Visibility Examples

Choose between hidden and absent according to the product contract.

Requirement Assertion
Drawer may hide or unmount await expect(drawer).toBeHidden()
Deleted row must leave the DOM await expect(row).toHaveCount(0)
Element must remain mounted but hidden await expect(element).toBeHidden() plus toBeAttached() if needed
Element must leave viewport after scroll await expect(element).not.toBeInViewport()
Control remains visible but becomes disabled await expect(control).toBeDisabled()

Example for dismissing a banner:

const banner = page.getByRole('region', { name: 'Product announcement' });

await expect(banner).toBeVisible();
await banner.getByRole('button', { name: 'Dismiss' }).click();
await expect(banner).toBeHidden();

Example for a deleted row:

const row = page.getByRole('row').filter({
  hasText: 'obsolete-user@example.test',
});

await row.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Confirm deletion' }).click();

await expect(row).toHaveCount(0);

Do not use not.toBeVisible() to claim deletion if hidden markup would violate the requirement. The matcher must reflect whether absence, hidden state, or viewport movement is expected.

11. Page Object Visibility Contract

A page object can centralize stable locators and expose business-state assertions. Keep action and expectation names clear.

import { expect, type Locator, type Page } from '@playwright/test';

export class CartDrawer {
  readonly trigger: Locator;
  readonly drawer: Locator;
  readonly checkoutButton: Locator;

  constructor(page: Page) {
    this.trigger = page.getByRole('button', { name: 'Open cart' });
    this.drawer = page.getByRole('dialog', { name: 'Shopping cart' });
    this.checkoutButton = this.drawer.getByRole('button', {
      name: 'Checkout',
    });
  }

  async open(): Promise<void> {
    await this.trigger.click();
    await expect(this.drawer).toBeVisible();
  }

  async expectReady(): Promise<void> {
    await expect(this.drawer).toBeVisible();
    await expect(this.checkoutButton).toBeVisible();
    await expect(this.checkoutButton).toBeEnabled();
  }
}

open() validates the outcome promised by its name. expectReady() adds the stronger business conditions required before checkout.

Avoid an isOpen(): Promise<boolean> helper for required state. It encourages callers to write manual waits or branches. A retrying assertion helper produces better diagnostics. Boolean helpers are reasonable only for truly optional, settled variants.

For larger abstraction choices, read the Playwright framework best practices guide.

12. Playwright expect toBeVisible Examples and Best Practices Checklist

Use these rules across scenario types:

  1. Trigger the user-visible transition first.
  2. Locate the specific outcome by role, name, label, text, or stable test ID.
  3. Scope repeated content to a dialog, region, row, card, or frame.
  4. Await every visibility assertion.
  5. Pair disappearance with a positive ready-state check.
  6. Use URL plus page identity after navigation.
  7. Assert semantic state for tabs, accordions, and menus.
  8. Separate visibility from enabled, viewport, and click actionability requirements.
  9. Use project-level device settings for responsive expectations.
  10. Apply local timeouts only at legitimately slow boundaries.
  11. Avoid fixed sleeps, force, and DOM mutation.
  12. Preserve traces and screenshots for failure analysis.

When an example fails, inspect the triggering action before editing the assertion. If the action did not occur, no timeout can make the outcome valid. If the locator matches multiple or hidden copies, improve scope. If the correct element never appears, preserve the trace as evidence of a product or environment problem.

Build a visibility assertion matrix

For a complex feature, document each visible state before writing selectors. Record the trigger, the semantic target, whether the target may be removed, and the next meaningful proof. This tiny model prevents a helper from treating every condition as the same kind of visibility wait.

Feature state Required evidence Failure interpretation
Cart drawer opens Named dialog is visible Trigger, animation, or locator failed
Cart contents load Expected line item is visible Data or rendering failed
Checkout becomes available Button is visible and enabled Business rule or validation failed
Drawer closes Dialog is hidden Close behavior failed

The sequence also determines which assertion should carry a longer timeout. A local drawer transition should normally be fast. Remote checkout eligibility may have a different documented readiness window. Keep those budgets separate rather than raising every visibility assertion.

When several target states are valid, encode that variability at the test-data or project boundary. For example, an account with no saved address should see an address form, while an account with a saved address should see a summary. Seed the intended account state and assert one deterministic outcome. A test that accepts either screen without controlling setup can pass when the wrong experience appears.

Finally, assert the next business proof after visibility. A visible cart drawer is useful, but a visible expected product and enabled checkout button show that the component is ready for the user.

Interview Questions and Answers

Q: Show a good modal visibility pattern.

I click the named trigger, locate the dialog by role and accessible name, and await toBeVisible(). I scope all warning, button, and field locators inside that dialog, then use toBeHidden() after canceling or closing.

Q: Why assert positive content after a spinner disappears?

A spinner can disappear on success, error, or cancellation. A results heading, expected row, or status proves the business-ready state and avoids a false pass based only on absence.

Q: How do you assert a transient toast?

I locate the semantic status or alert immediately after the triggering action and await visibility and expected text. I do not add a fixed delay or require disappearance unless auto-dismiss behavior is part of the requirement.

Q: How do you test a tab activation?

I click the tab, assert aria-selected is true, and assert the matching tabpanel is visible. This covers both accessible state and rendered content.

Q: What is your strategy for repeated cards?

I locate the target card by a unique heading or other record identity, then locate and assert controls within it. This avoids matching the same button name in unrelated cards.

Q: How do you handle visibility inside an iframe?

I use page.frameLocator() for inner content and run normal locator assertions on that frame-scoped locator. If needed, I separately assert the iframe element is present and visible on the host page.

Q: When do you use toHaveCount(0) instead of toBeHidden()?

I use count zero when removal from the DOM is the requirement, such as a deleted table row. I use hidden when either CSS hiding or unmounting is acceptable.

Q: What makes a visibility assertion redundant?

A separate assertion before a click is often redundant because click waits for actionability. It becomes valuable when appearance is itself the expected outcome or when distinct visibility diagnosis is important.

Common Mistakes

  • Asserting a wrapper instead of the meaningful business result.
  • Waiting for spinner disappearance without confirming successful content.
  • Locating modal controls globally instead of inside the named dialog.
  • Using text that matches several old or hidden notifications.
  • Checking only URL after a client-side redirect.
  • Selecting a repeated card by DOM index.
  • Treating mobile and desktop duplicates as one ambiguous locator.
  • Searching page-level DOM for content inside an iframe.
  • Requiring element absence when hidden mounting is valid.
  • Adding visibility assertions before every click.
  • Using long timeouts to hide a missing trigger action.
  • Using screenshots alone when semantic state is the actual requirement.

Conclusion

The best Playwright expect toBeVisible examples connect a user action to one specific visible outcome. Menus expose named navigation, dialogs reveal scoped content, searches produce result regions, tabs update semantic state, and redirects render unique destination identity.

Copy the pattern, not just the matcher: act, locate semantically, await the visible result, then prove the business state that matters. That approach produces stable tests and failures that tell the team what users could not see.

Interview Questions and Answers

How would you verify a modal opens correctly?

I click the named trigger, assert the uniquely named dialog is visible, and scope its content assertions inside the dialog. I also verify the key warning or control, then assert the dialog is hidden after the close action.

What is the best wait after submitting a search?

I wait for the observable business result, such as the result region and expected record. Spinner disappearance can be an intermediate check, but it is not sufficient proof of success.

How would you test a toast that disappears automatically?

I assert role-based visibility and text immediately after the trigger. I test auto-dismiss only if it is a stated requirement, because appearance and persisted outcome usually provide more durable value.

How do you test accessible tab behavior?

I click the tab, assert `aria-selected` is true, and assert the corresponding tabpanel is visible. This validates both semantic state and rendered content.

How do you prevent duplicate locator issues in a product grid?

I identify the product card through a unique heading or stable record key, then locate the price and action within that card. I do not select by global button text or DOM position.

How do you assert third-party iframe content?

I use `frameLocator` to scope locators inside the iframe. I may separately assert the host iframe element when diagnosing whether mounting or inner loading failed.

How do you handle desktop and mobile alternatives?

I define deterministic Playwright projects and make each project's expected UI explicit. If a scenario legitimately accepts either of two locators, I compose them carefully and account for the possibility that both match.

What evidence do you inspect when toBeVisible times out?

I inspect the call log, trace, screenshot, locator count, triggering action, and current DOM state. I then check hidden duplicates, frame boundaries, accessible-name changes, network failure, and product state.

Frequently Asked Questions

What is a simple Playwright toBeVisible example?

Click the control that reveals content, create a semantic locator for that content, and write `await expect(locator).toBeVisible()`. Add a content or state assertion when visibility alone does not prove the business result.

How do I check that a Playwright modal is visible?

Locate it with `getByRole('dialog', { name: '...' })` and await `toBeVisible()`. Scope every modal-specific locator inside that dialog to avoid matching controls behind it.

How should I wait for a loading spinner?

Use `toBeHidden()` if spinner disappearance matters, then assert a positive result such as a heading, row, or ready status. Skip the initial visible assertion if the spinner is allowed to be too brief to observe.

How do I assert a toast message in Playwright?

Locate a role `status` or `alert` immediately after the trigger, then assert visibility and text. Avoid a fixed sleep because the matcher already retries.

How do I check visibility inside an iframe?

Create a frame locator with `page.frameLocator(selector)`, then locate the inner control and use the normal visibility assertion. A page locator does not search inside the frame document.

When should I use toHaveCount zero instead of toBeHidden?

Use `toHaveCount(0)` when the element must be removed from the DOM, such as a deleted record. Use `toBeHidden()` when either hidden or removed behavior satisfies the requirement.

How do I test responsive visible elements in Playwright?

Define deterministic desktop and mobile projects, then locate the active named navigation or control for each project. Avoid `nth()` selectors based only on duplicated DOM order.

Related Guides