ScreenshotNeo

BlogHow-to

How to Test a Web App’s Dropdown Menus with Visual Regression Screenshots

Test dropdown behavior and appearance together with Playwright screenshots. Capture the right states, reduce flaky diffs, and review visual changes with confidence.

By the ScreenshotNeo team4 October 20269 min read

A reliable dropdown visual regression test opens the control through a real browser interaction, asserts that the expected menu content appears, and captures that state with a screenshot assertion. With Playwright Test, use await expect(page).toHaveScreenshot() for the page or await expect(locator).toHaveScreenshot() for a focused element. Keep behavior assertions alongside the screenshot: a matching image alone does not prove the menu opened or works.

Use a consistent browser and host environment for baseline creation and comparison, control pointer position and volatile content, and review every new or changed baseline before accepting it. The example below uses a button that opens an account menu; adjust its roles and accessible names to match your app.

1. Set up a Playwright visual test

Install Playwright Test and its browser if your project does not already use it:

npm init playwright@latest
npx playwright install

Create a test such as tests/account-menu.spec.ts:

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

test('account dropdown opens and matches its baseline', async ({ page }) => {
  await page.setViewportSize({ width: 1280, height: 800 });
  await page.goto('/settings');

  const trigger = page.getByRole('button', { name: 'Account options' });
  await expect(trigger).toBeVisible();
  await trigger.click();

  const menu = page.getByRole('menu');
  await expect(menu).toBeVisible();
  await expect(menu.getByRole('menuitem', { name: 'Profile' })).toBeVisible();

  await expect(menu).toHaveScreenshot('account-menu-open.png');
});

This is an illustrative pattern, not a tested example. The menu role is appropriate only if your component follows that accessibility pattern. A native <select> has different semantics and interaction; assert the relevant control and option state instead of assuming it exposes menu and menuitem.

Run the test with:

npx playwright test tests/account-menu.spec.ts

When a screenshot reference does not exist, Playwright creates one. Review the generated image and commit the approved reference with the test suite. Later runs compare against that baseline. Playwright retries screenshot capture until two consecutive screenshots match, which helps with transient rendering changes but does not replace stable test setup.

2. Choose which dropdown states to cover

Cover states that matter for your component and users. A compact test suite might include the closed state and the open state; add other cases when they represent meaningful behavior or layout:

  • Closed: the trigger is visible and the menu is absent or hidden.
  • Open: clicking the trigger reveals the menu and its expected items.
  • Selected or expanded: a chosen value, nested menu, or expanded section is visibly represented.
  • Keyboard and focus: keyboard input opens or navigates the menu as intended, and focus is visible on the relevant item.
  • Disabled or error: capture these only if the component has a meaningful disabled or error presentation.
  • Responsive: test a narrow viewport when the dropdown changes layout or behavior at mobile sizes.

For hover-open menus, use hover() on the trigger, then assert visibility and capture. For keyboard operation, exercise the keys your component supports and assert the focus or selected item before taking the screenshot. If one test passes through several states, capture each intermediate state at the point it occurs; a final screenshot cannot show states that have already been dismissed.

3. Capture the page or just the menu

A page screenshot checks the dropdown in its surrounding layout, which can catch clipping, stacking, and alignment changes. A locator screenshot focuses on the menu and usually reduces unrelated visual noise:

await expect(page).toHaveScreenshot('account-menu-page-open.png');
await expect(menu).toHaveScreenshot('account-menu-only.png');

Use the page capture when the relationship between the trigger, menu, and nearby content matters. Use the locator capture when you want to review the menu’s contents and styling independently. Avoid capturing a tiny region that excludes the positioning or overlap behavior you need to test.

4. Make screenshot comparisons repeatable

  1. Keep the rendering environment stable. Use the same browser project, browser version, operating system or CI image, viewport, device scale, color scheme, and headless settings when recording and comparing references. Browser rendering can vary with host OS, version, settings, hardware, power state, headless mode, and other factors, as the Playwright visual comparisons documentation explains.
  2. Wait for a condition, not an arbitrary delay. Assert that the menu and its expected items are visible. If the component loads data, wait for the expected content. Ensure fonts and relevant icons have loaded before capture when they affect appearance.
  3. Control the pointer deliberately. Pointer position can activate hover styles that change the image. Move it away if the resting appearance is what you are testing, or deliberately hover the trigger if that is the state under test.
  4. Stabilize real sources of volatility. Timestamps, randomized values, rotating content, and animations can make references noisy. Playwright screenshot assertions support a screenshot stylesheet through stylePath; use it to suppress or normalize genuinely volatile areas, not the dropdown or nearby layout whose appearance you need to verify.
  5. Inspect diffs before changing thresholds. Review the image difference and identify its cause before adjusting maxDiffPixels or related screenshot comparison settings. A permissive threshold can hide a real layout or styling regression.
  6. Update baselines intentionally. After a visual change you intend to keep, run npx playwright test --update-snapshots, inspect the new references, and commit only the reviewed changes.

See the official Playwright screenshot comparison guide for baseline behavior and volatile-element styling, and the Page API reference for screenshot options.

5. Add focused checks for common interaction patterns

Hover-open menu

test('hover menu is visible', async ({ page }) => {
  await page.goto('/navigation');
  const trigger = page.getByRole('button', { name: 'Products' });
  await trigger.hover();

  const menu = page.getByRole('menu');
  await expect(menu).toBeVisible();
  await expect(menu).toHaveScreenshot('products-menu-hover.png');
});

Keyboard-open menu

Use the keys and expected focus behavior defined by your component. This example assumes Enter opens the menu and focus moves to its first item:

test('menu opens from the keyboard', async ({ page }) => {
  await page.goto('/settings');
  const trigger = page.getByRole('button', { name: 'Account options' });
  await trigger.focus();
  await page.keyboard.press('Enter');

  const menu = page.getByRole('menu');
  await expect(menu).toBeVisible();
  await expect(menu.getByRole('menuitem').first()).toBeFocused();
  await expect(menu).toHaveScreenshot('account-menu-keyboard.png');
});

If your design keeps focus on the trigger, uses arrow keys to open, or follows native select behavior, change the interaction and assertions to match that design. Do not force menu roles onto a control with different semantics.

Responsive menu

test('mobile account menu is visible', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto('/settings');
  await page.getByRole('button', { name: 'Account options' }).click();

  const menu = page.getByRole('menu');
  await expect(menu).toBeVisible();
  await expect(page).toHaveScreenshot('account-menu-mobile.png');
});

6. Local Playwright versus hosted visual review

Playwright’s built-in assertions keep reference images with the project and work well when your team is comfortable reviewing diffs in its existing code workflow. Hosted services can add cloud snapshot rendering, shared visual review, or targeted capture of intermediate states. Chromatic documents a Playwright setup and targeted snapshots; Percy provides a Playwright client. Compare the workflows based on browser and platform coverage, baseline ownership, intermediate-state capture, CI integration, review needs, and current vendor terms. The research for this guide did not establish current prices or program terms.

ScreenshotNeo is a website screenshot API and MCP server; it is useful when you need a screenshot from a reachable URL or want an AI agent to request one. A screenshot API call by itself does not run your Playwright interaction sequence or create a visual regression baseline. For an interactive dropdown test, use browser automation to reach the open state; an API can capture a suitable URL when your app exposes that state reliably.

Or skip the browser setup

For a screenshot of a reachable page, ScreenshotNeo returns an image or PDF with one request. See the ScreenshotNeo API documentation for options. This call captures the target URL; it does not replace the interaction and baseline assertions in the Playwright test above.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and responses identify the page verdict and billing status in headers. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for 1,000 free screenshots a month, with no card required.

Troubleshooting visual dropdown tests

Symptom Likely cause Fix
The menu is missing from the screenshot The test captured before opening it, used the wrong trigger interaction, or proceeded past the open state. Perform the click, hover, or keyboard action first; assert menu visibility; capture immediately at that state.
The menu assertion times out The locator does not match the app’s accessibility roles or names, the menu did not open, or the page has not reached its ready state. Inspect the actual semantics and accessible name. Assert the trigger is visible, use the component’s real interaction, and wait for expected content.
Snapshots differ only on CI Browser, OS image, device scale, headless settings, fonts, or other rendering conditions differ from baseline generation. Generate and compare references in a consistent CI image and browser configuration. Avoid generating baselines on a different platform from the one used for comparison.
Diffs flicker between runs Pointer hover, animation, delayed fonts, network data, timestamps, or random content varies. Set a deliberate pointer location, wait on visible conditions and loaded assets, and stabilize only the volatile content that is outside the test’s purpose.
A broad threshold makes tests pass, but defects slip through The comparison allows too many pixels to change. Review the raw diff, fix the source of nondeterminism, and lower the threshold to a level that still detects meaningful menu changes.
A new baseline contains an unexpected change The update command accepted an unintended difference or the UI changed in an unreviewed way. Inspect the reference and diff, verify whether the change was intended, and restore or correct the baseline before committing.

Performance, reliability, and maintenance

Focused locator screenshots are often easier to review and avoid comparing unrelated page regions, while full-page screenshots provide context for positioning and overlap. Keep the number of snapshots tied to meaningful states; every additional viewport or interaction state adds baseline files and review work. Stable assertions and a fixed rendering environment reduce reruns caused by noise. Hosted visual review may help teams share diffs and capture intermediate states, but it adds a service workflow whose current cost and terms should be checked directly with the vendor.

Visual checks complement functional tests. Keep explicit assertions for visibility, expected text, selected state, keyboard behavior, and the result of choosing an item. A screenshot can reveal a visual regression, but it cannot establish all interaction outcomes.

FAQ

Should I screenshot the trigger, the menu, or the whole page?

Capture the menu alone for focused styling checks; capture the page when placement, clipping, or overlap with surrounding content matters. It can be useful to keep both checks if they catch different defects.

Do I need a hosted visual testing service?

No. Playwright Test can create and compare local screenshot references. A hosted workflow is optional when shared review, cloud rendering, or targeted capture features fit your team.

Can an API screenshot prove the dropdown works?

No. A screenshot records a rendered image. Use browser interaction and behavior assertions to prove the dropdown opens and behaves correctly; use a screenshot API for URL-based captures when that fits the page setup.

When should I update a reference image?

After confirming the visual change is intended and reviewing the new image and diff. Updating references without review can make an unintended regression the new expected appearance.