← Back to question bank
UI ComponentMidMedium#6008 · 35m

Practice: verify screen reader names

Turn "verify screen reader names" into a concrete interview exercise. Explain the risk, choose the smallest useful test boundary, and describe how the signal prevents regressions.

Answer Strategy

Verifying screen reader names is the question that exposes whether you understand the accessibility tree, not just visible text. The interview-grade rule: every interactive element must have a role and an accessible name; every dynamic content change must announce through a live region (role=status for polite, role=alert for assertive). When tests query by role with a name, they prove the accessible tree, not just the DOM.

Three queries cover most cases. getByRole('button', { name: 'Save' }) for interactive elements. getByRole('status') / getByRole('alert') for live region announcements. getByRole('article', { name: ... }) for sections labeled via aria-labelledby. Each fails fast when the accessible name is missing or wrong, while DOM/text queries silently pass on broken accessibility.

Volunteer the failures. A button labeled only via an icon-only span fails getByRole with name. A status region created on every render (instead of mounted once and updated) re-announces "Saving..." every keystroke and overwhelms users. An aria-labelledby pointing at an id that does not exist silently strips the accessible name. The reference suite shows the canonical assertions: silence when status is idle, polite for non-urgent updates, assertive for errors, and aria-labelledby resolution proven via name-scoped queries.

Reference Implementation: Status Banner And Card With Name-Scoped Tests

SaveStatusBanner uses role=status / role=alert for the right urgency. ProjectCard uses aria-labelledby so the heading IS the accessible name.

// Component under test: a status banner that announces success and error
// transitions. The accessible name and role are the contract; visual style
// is documentation that follows.
type SaveStatusBannerProps = {
  status: 'idle' | 'saving' | 'saved' | 'error';
  errorMessage?: string;
};

export function SaveStatusBanner({ status, errorMessage }: SaveStatusBannerProps) {
  if (status === 'idle') return null;
  if (status === 'error') {
    return (
      <div role="alert" aria-live="assertive">
        <strong>Save failed.</strong> {errorMessage}
      </div>
    );
  }
  return (
    <div role="status" aria-live="polite">
      {status === 'saving' ? 'Saving changes...' : 'All changes saved.'}
    </div>
  );
}

// A second component that demonstrates accessible naming via aria-labelledby
// instead of aria-label. Assistive tech reads the heading as the section's
// name, which is preferable when the heading is already on screen.
type ProjectCardProps = {
  projectId: string;
  name: string;
  description: string;
};

export function ProjectCard({ projectId, name, description }: ProjectCardProps) {
  const titleId = 'project-' + projectId + '-title';
  return (
    <article aria-labelledby={titleId}>
      <h3 id={titleId}>{name}</h3>
      <p>{description}</p>
    </article>
  );
}

Executable UI Sandbox

UI interview practice should behave like component documentation, not a static snippet. This uses the same isolation pattern as Storybook, Sandpack, CodeSandbox, and StackBlitz: editable source on one side, a sandboxed browser preview on the other. Edit the DOM code, run it, and verify focus, keyboard, pointer, and state behavior in the preview.

Browser sandbox
HTML, CSS, DOM events, focus, and keyboard behavior
Preview running...
Loading editor...

Testing Strategy

Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.

Roles + names
Every interactive element must be queryable via getByRole with name. Replace getByText queries with getByRole({ name }) to cover the accessible tree, not just visual text.
Live regions
Test that polite vs assertive matches the urgency. Confirm the live region exists at idle render time (or that it is mounted on first announcement) so screen readers can subscribe.
Real screen reader
Automated tests cover the tree shape; pair a manual VoiceOver / NVDA pass on the deployed page for tone and verbosity. jsdom does not announce.
import { describe, it, expect } from 'vitest';
import { render, screen, within } from '@testing-library/react';

describe('SaveStatusBanner', () => {
  it('idle renders nothing (no live region announcing silence)', () => {
    const { container } = render(<SaveStatusBanner status="idle" />);
    expect(container).toBeEmptyDOMElement();
  });

  it('saving uses role=status with aria-live=polite', () => {
    render(<SaveStatusBanner status="saving" />);
    const live = screen.getByRole('status');
    expect(live).toHaveAttribute('aria-live', 'polite');
    expect(live).toHaveTextContent(/saving/i);
  });

  it('error uses role=alert (assertive announcement)', () => {
    render(<SaveStatusBanner status="error" errorMessage="Network down" />);
    const alert = screen.getByRole('alert');
    expect(alert).toHaveAttribute('aria-live', 'assertive');
    expect(alert).toHaveTextContent(/save failed.*network down/i);
  });
});

describe('ProjectCard', () => {
  it('exposes the heading as the article accessible name', () => {
    render(<ProjectCard projectId="42" name="Falcon" description="Trading dashboard" />);
    // getByRole('article', { name: 'Falcon' }) succeeds only if aria-labelledby
    // resolves to the heading. This catches a typo in the id wiring.
    const article = screen.getByRole('article', { name: 'Falcon' });
    expect(within(article).getByText('Trading dashboard')).toBeInTheDocument();
  });

  it('two cards expose distinct accessible names', () => {
    render(
      <>
        <ProjectCard projectId="a" name="Alpha" description="" />
        <ProjectCard projectId="b" name="Bravo" description="" />
      </>
    );
    expect(screen.getByRole('article', { name: 'Alpha' })).toBeInTheDocument();
    expect(screen.getByRole('article', { name: 'Bravo' })).toBeInTheDocument();
  });
});

Interviewer Signal

Shows whether you can prove frontend behavior instead of relying on screenshots or manual confidence.

Constraints

  • Choose unit, integration, E2E, visual, or performance testing deliberately.
  • State the failure that the test catches.
  • Avoid brittle assertions that lock implementation details.

Model Answer Shape

  • Start with the user-impacting behavior.
  • Pick the smallest test that sees that behavior.
  • Add one higher-level test only when timing, browser behavior, or integration risk requires it.

Tradeoffs

  • Unit tests are fast and precise but cannot prove browser wiring.
  • E2E tests are realistic but should be reserved for workflows where integration risk matters.

Edge Cases

  • Out-of-order async results.
  • Environment-specific browser behavior.
  • False confidence from mocks that do not match production contracts.

Testing And Proof

  • Regression case for the named risk.
  • Negative path or error state.
  • Cleanup or retry behavior when relevant.

Follow-Ups

  • What would make this test flaky?
  • What would you monitor after shipping the fix?