Skip to content

[Bug]: Image diff mode switcher (Diff / Actual / Expected / Side by side / Slider) is mouse-only #42266

Description

Version

1.63.0-next (reproduced on main at d5a185a, also on 1.62.1)

Steps to reproduce

No external repo needed, any failing screenshot comparison shows it.

  1. Write a test that fails toMatchSnapshot / toHaveScreenshot on an image, run it with the html reporter.
  2. Open the report and expand the failing test. The image diff view shows a row of five mode switchers: Diff, Actual, Expected, Side by side, Slider.
  3. Try to reach any of them with Tab and activate it with Enter.

Same row appears in the trace viewer attachments tab and in the html reporter error view.

As a runnable check against the existing story, this fails on main:

const component = await mount<typeof Default>('shared/imageDiffView/Default');
const sxs = component.getByRole('tab', { name: 'Side by side' });
await sxs.focus();
await expect(sxs).toBeFocused();

Expected behavior

The switchers take focus and Enter activates them, the way the tab strips do since #41434 and the html-reporter chips do since #42149.

Actual behavior

Tab skips the whole row. There is no way to change which image is displayed without a mouse, and a screen reader is never told the controls exist at all, because they carry no role.

packages/web/src/shared/imageDiffView.tsx:102-106 renders them as bare divs:

{diff.diff && <div style={{ ...modeStyle, fontWeight: mode === 'diff' ? 600 : 'initial' }} onClick={() => setMode('diff')}>Diff</div>}
<div style={{ ...modeStyle, fontWeight: mode === 'actual' ? 600 : 'initial' }} onClick={() => setMode('actual')}>Actual</div>
<div style={{ ...modeStyle, fontWeight: mode === 'expected' ? 600 : 'initial' }} onClick={() => setMode('expected')}>{expectedImageTitle}</div>
<div style={{ ...modeStyle, fontWeight: mode === 'sxs' ? 600 : 'initial' }} onClick={() => setMode('sxs')}>Side by side</div>
<div style={{ ...modeStyle, fontWeight: mode === 'slider' ? 600 : 'initial' }} onClick={() => setMode('slider')}>Slider</div>

No role, no tabIndex, no onKeyDown. Which one is active is also communicated only through fontWeight, so it is invisible to assistive tech even if you could reach them.

Additional context

ImageDiffView is shared, so this covers three surfaces: html-reporter/src/testResultView.tsx:150, html-reporter/src/testErrorView.tsx:53 and trace-viewer/src/ui/attachmentsTab.tsx:130.

I have a fix ready that follows the same native-element approach as #41434, buttons in a tablist plus a focus ring from --vscode-focusBorder, and I checked the rendered geometry is unchanged. Glad to send it over if you want to assign this to me.

sorry if I have got any of this wrong, I dug into it myself and used claude code to sanity check the approach against how the repo already does keyboard support. freshman in college, just trying to be genuinely useful here :)

Environment

System:
  OS: macOS 26.5.2
  CPU: (10) arm64 Apple M4
Binaries:
  Node: 26.0.0
  npm: 11.12.1
npmPackages:
  playwright monorepo at main (1.63.0-next)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions