ScreenshotNeo

BlogGuides

UI Testing Guide: How to Test Web Interfaces

Learn how to test web interfaces with component, API, end-to-end, and accessibility checks. Build a practical workflow around critical user journeys and meaningful UI states.

By the ScreenshotNeo team4 October 202610 min read

Test a web interface at several layers. Use component tests for isolated behavior, API tests for endpoint contracts, and end-to-end (E2E) tests for critical user journeys. Add accessibility checks to real interface states, then supplement automated checks with manual assessment. No one layer can establish that the entire interface works for every user.

This guide shows how to choose what to test, build a practical workflow with Playwright, and avoid common gaps. Playwright examples below use JavaScript and assume a web app with a sign-in form; replace the example URL, selectors, and expected outcomes with your application’s behavior.

1. Decide what needs testing

Start with user outcomes and failure risk. Write down what people need to accomplish, what could prevent completion, and the cost of that failure. Give the most valuable journeys a small number of durable E2E tests, then cover detailed interaction rules closer to the component.

Layer What it checks Good fit Limit
Component An individual UI component mounted in a browser Labels, validation, expanded states, button behavior Does not prove the complete application flow works
API HTTP endpoint behavior and request/response contracts Authentication, validation, error responses, data contracts Does not exercise the UI
End-to-end Application layers together through browser actions Sign-up, checkout, sign-in, or another high-value journey Broader coverage, but slower and more prone to flakiness than focused component tests
Accessibility Detectable rule violations and interaction behavior relevant to access Automated scans, semantic assertions, keyboard and focus checks, manual review Automated scans cover only detectable issues and cannot prove usability

These distinctions and tradeoffs align with Cypress’s own guidance on testing types; treat that as vendor guidance rather than an independent tool comparison. Accessibility checks are a layer that can be added to component and E2E tests, not a substitute for functional assertions.

Choose tests by risk

  • Cover each critical user outcome with at least one test that verifies the result a user sees.
  • Use component tests for detailed states and edge cases that would be cumbersome to reach through a long browser journey.
  • Test APIs separately when you need precise endpoint and contract feedback.
  • Keep E2E tests focused on important flows rather than repeating every component assertion through the full app.
  • Include error, loading, empty, and success states when they affect what a person can do.

2. Set up a browser test with Playwright

Playwright’s test runner can launch a browser, navigate to an app, interact with the page, and assert the result. Install it in the project using its documented setup command:

npm init playwright@latest

Choose JavaScript or TypeScript when prompted and select the browsers your project needs. The command creates a test directory and configuration. To run the generated and project tests, use:

npx playwright test
npx playwright show-report

A runnable sign-in journey

Save this as tests/sign-in.spec.js. The example expects a form with accessible labels and a dashboard heading after successful sign-in. Set BASE_URL to a running app that has a test account configured.

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

test('a user can sign in', async ({ page }) => {
  const baseUrl = process.env.BASE_URL ?? 'http://127.0.0.1:3000';
  const email = process.env.TEST_EMAIL;
  const password = process.env.TEST_PASSWORD;

  if (!email || !password) {
    throw new Error('Set TEST_EMAIL and TEST_PASSWORD for the sign-in test.');
  }

  await page.goto(`${baseUrl}/sign-in`);
  await page.getByLabel('Email').fill(email);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page).toHaveURL(/dashboard/);
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

test('invalid credentials show an error', async ({ page }) => {
  const baseUrl = process.env.BASE_URL ?? 'http://127.0.0.1:3000';

  await page.goto(`${baseUrl}/sign-in`);
  await page.getByLabel('Email').fill('not-a-real-user@example.test');
  await page.getByLabel('Password').fill('invalid-password');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByRole('alert')).toContainText('Invalid email or password');
});

Run the test with credentials supplied through environment variables. Keep secrets out of source control.

BASE_URL=http://127.0.0.1:3000 TEST_EMAIL=tester@example.test TEST_PASSWORD='replace-with-test-secret' npx playwright test tests/sign-in.spec.js

The labels, button name, URL, heading, and error text are application-specific assumptions. Prefer accessible roles and labels when they match the user-facing interface; if they do not, fix the interface where appropriate or choose a stable, intentional test selector.

Use a local server and configure projects

Cypress recommends a local development server for most integration tests and a smaller set of smoke tests against deployed production. That is a Cypress-specific documented workflow, but the general choice is useful across browser test setups: local runs give controlled data and fast feedback, while a few deployed smoke checks can catch deployment or environment problems.

A Playwright configuration can start the local server and define browser projects. Adapt the command and port to your app:

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

export default defineConfig({
  testDir: './tests',
  retries: process.env.CI ? 2 : 0,
  reporter: process.env.CI ? 'github' : 'list',
  use: {
    baseURL: 'http://127.0.0.1:3000',
    trace: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
  webServer: {
    command: 'npm run dev -- --host 127.0.0.1',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

Playwright supports browser projects and device presets through its configuration. Add browsers that correspond to your support needs; do not assume one browser run establishes compatibility everywhere. Retries can help collect evidence about intermittent failures, but they can also hide flaky tests if failures are not investigated.

3. Test components and APIs at their own layer

Component tests

Mount an individual component in a browser and assert behavior at its boundary. For a menu, useful checks include whether the trigger exposes its expanded state, whether activation opens the menu, and whether the menu items can be reached and activated. Component tests help isolate failures and cover combinations of UI state without replaying the entire application journey.

Choose a component-testing setup that fits your framework and existing build tools. Cypress documents component testing as focused and quicker relative to E2E, but the exact setup and runtime depend on the project. Keep component tests about behavior users can observe rather than implementation details that change during refactors.

API tests

Call endpoints directly to check status codes, response shapes, validation, authorization, and failure behavior. These tests can give precise feedback about the service contract, but they do not prove that the interface sends the right request or communicates the result correctly. Pair API coverage with UI tests where the integration matters.

4. Cover meaningful states and accessibility

An accessibility scan of only the first screen can miss a dialog, open menu, form error, or later step. Exercise the states people actually encounter and inspect them while they are visible. Add checks for accessible names, labels, keyboard movement, and focus behavior where relevant.

  1. Open the menu, dialog, disclosure, or other interactive state.
  2. Trigger invalid input and verify the error is visible and associated with the relevant field.
  3. Move through the interface with the keyboard and verify focus is visible and ordered sensibly.
  4. Check that controls expose meaningful accessible names and that images needing text alternatives have them.
  5. Run automated accessibility checks against each important state, then manually assess behavior the rules cannot determine.

W3C explains that WCAG success criteria are testable and that assessment involves both automated testing and human evaluation. It also recommends usability testing in addition to functional conformance evaluation, including people with disabilities in usability test groups when possible. Playwright and Cypress likewise caution that automated scans cannot establish that an interface is fully accessible or usable. Common detectable issues include poor contrast, missing labels for controls, and images without appropriate alternative text; automation does not judge every context or assistive technology experience.

For example, a menu test should open the menu before checking its contents, and a form test should submit invalid data before checking its error state. A scan limited to the initial or final page can miss those intermediate states.

5. Capture screenshots for visual review

Functional assertions answer whether an action produced the expected behavior. Screenshots can help review layout and visual states, but they need stable inputs: consistent test data, viewport, fonts, browser, and animation behavior. Review changes in context; a pixel difference may be a real regression or an expected content variation.

For local browser tests, capture a screenshot from Playwright after reaching the state you want to inspect:

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

test('capture the open navigation state', async ({ page }) => {
  await page.goto('http://127.0.0.1:3000');
  await page.getByRole('button', { name: 'Open menu' }).click();
  await expect(page.getByRole('navigation')).toBeVisible();
  await page.screenshot({ path: 'artifacts/open-navigation.png', fullPage: true });
});

Create the artifacts directory before running this test or change the destination to a directory your test runner creates. Use screenshots as review evidence alongside assertions, not as a replacement for checking semantics, interactions, or accessibility.

6. Or skip the browser setup

If you need a screenshot of a page without installing or maintaining browser capture code, ScreenshotNeo returns an image or PDF from one GET request. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Cookie banners are accepted and removed before capture, along with known consent platforms, newsletter popups, and chat widgets; each of those steps can be turned off. Bot checks, blank pages, and failed loads are never billed, and response headers report the page verdict and billing status. An MCP server gives AI agents tools to take screenshots, inspect page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
    f.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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

The Node.js example uses the provided request pattern and Bun’s file writer to save the returned image. With Node.js alone, write the response bytes using the built-in file system module:

import { writeFile } from 'node:fs/promises';

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

7. Keep tests reliable and affordable to maintain

  • Control data: use dedicated test accounts and predictable fixtures; avoid relying on state left by another test.
  • Wait for outcomes: assert that the expected UI state appears rather than adding arbitrary sleeps everywhere.
  • Keep journeys short: longer sequences have more opportunities for unrelated failures and take longer to diagnose.
  • Separate environments: local or test environments are easier to control; use production checks sparingly for smoke coverage.
  • Investigate retries: a retry that passes still indicates instability worth understanding.
  • Manage artifacts: retain traces, screenshots, and reports when they help diagnose failures, and set retention appropriate to CI storage constraints.
  • Choose coverage intentionally: prioritize supported browsers and devices based on users and risk; each additional configuration increases maintenance and execution work.

The research sources do not establish a neutral performance ranking between Cypress and Playwright, nor do they provide benchmark results. Choose based on language and framework fit, browser needs, debugging experience, CI workflow, accessibility integration, maintenance, and the cost of any hosted features. Cypress documents Cypress Accessibility as a paid premium solution in Cypress Cloud; verify current availability and pricing in its documentation before adopting it.

8. Troubleshooting common failures

Symptom Likely cause What to do
Element lookup fails The accessible name or label differs from the test, the page has not reached that state, or the selector is unstable Inspect the rendered page and use the actual accessible role/name; wait for the meaningful state rather than a guessed delay
Test passes locally but fails in CI Different environment variables, data, server readiness, browser, or timing Confirm the same test configuration and fixtures, ensure the server is ready, and inspect CI traces or screenshots
Intermittent timeout Uncontrolled network or app state, an overly long flow, or a real performance issue Identify the stalled step, make data deterministic, shorten the journey, and wait on a specific expected condition
Sign-in test fails unexpectedly Missing or expired test credentials, account state, or environment mismatch Supply a dedicated test account and verify it can sign in to the configured base URL
Accessibility scan reports nothing, but users still struggle The problem may require human judgment or assistive technology assessment Manually test keyboard and focus behavior, review content and interaction, and include users with disabilities in usability tests when possible
Visual screenshot changes every run Dynamic data, animation, fonts, viewport, or browser rendering varies Stabilize fixtures and rendering inputs, capture the same state, and review the difference rather than blindly accepting it
Production smoke test changes data The test uses a real account or mutating flow Use a safe test path and account, avoid destructive actions, and keep production checks limited to controlled smoke coverage

9. Frequently asked questions

Should every UI test be end-to-end?

No. Use E2E for important complete journeys and component tests for focused behavior. API tests add contract coverage without exercising the browser.

Can an automated accessibility scan certify my site?

No. Scans find some rule-detectable problems. Combine them with semantic assertions, manual assessment, and usability testing.

Should browser tests run against production?

Use a controlled local or test environment for most integration coverage. A small set of safe production smoke checks can catch deployment-specific failures; Cypress documents this as its recommended workflow.

Are screenshots enough to test a visual interface?

No. Screenshots help review appearance, while functional and accessibility assertions check behavior that pixels alone cannot explain.

Sources