Skip to main content

Scott Nath Frontend Architect

Scott Nath's picture
GitHubdev.toLinkedInMastodon
Article header image shows the Guidepup and Storybook logos between greater-than and less-than characters with text below them that reads "Simple Setup for Virtual Screen Reader + Storybook"

Simple setup: Virtual Screen Reader in Storybook

How to set up Storybook with Guidepup's Virtual Screen Reader and test what the screen reader speaks


Recent changes to the @guidepup Virtual Screen Reader library make it ridiculously easy to integrate into Storybook for use in your integration tests. This article shows how to get the basic setup going and how to write your first screen reader test in Storybook.

tl;dr

The Virtual Screen Reader is a great tool to mimic screen reader output and it’s works in Storybook without addons or configuration changes.

What is the Virtual Screen Reader?

The Virtual Screen Reader (docs, GitHub) is a library, available via NPM, which mimics the functionality of a screen reader. The VSR is from Guidepup (guidepup.dev), which has tons of screen reader tooling. The Virtual Screen Reader is the easiest @guidepup tool to set up and it can read your components like a screen reader would, giving you back the text for use in testing.

FYI: Guidepup has a suite of tools that mirror and/or directly-simulate screen reader experiences and capture the results. The tools are impressive and worth checking out if you want to automate accessibility testing into your workflow. For example, you can use directly use the system NVDA or Mac VoiceOver via Playwright. (check out Craig’s dev.to article on implementing Guidepup with Playwright and Mac VoiceOver for details)

How does the screen reader interpret a chunk of HTML?

Screen readers vary in interpretation, but this gives a rough understanding of how your HTML could get broken down by the VSR.

If the HTML a component puts out is this:

<header>
  <div class="storybook-header">
    <div>
      <svg>...</svg>
      <h1>Acme</h1>
    </div>
    <div>
      <span class="welcome">Welcome, <b>Jane Doe</b>!</span>
      <button type="button" class="...">Log out</button>
    </div>
  </div>
</header>

This is what the Virtual Screen Reader will translate it into:

<header> <!--BANNER-->
<h1>Acme</h1> <!--HEADING + text + HEADING-LEVEL-->
Welcome, <!--Text-->
Jane Doe <!--Text-->
! <!--Punctuation-->
<button>Log out</button> <!--BUTTON + text-->
</header> <!--END OF BANNER-->

How to set up your Storybook instance to use the Virtual Screen Reader

Follow these steps to test with VSR in your Storybook environment:

  1. Ensure your storybook environment is set up for Interaction tests (see Storybook setup docs)
  2. Install the VSR npm module npm i @guidepup/virtual-screen-reader -D
  3. PROFIT - No addon setup or other configuration needed!

How to write a Storybook integration test that uses the Virtual Screen Reader

This example is using the default Header component that installs with default Storybook. We’ll be adding Interaction tests which will use VSR. You can see the completed Header.stories.js on GitHub.

Import the library into your stories file

To integrate the VSR into your tests, import it in your test file. In this example, we’ll add the assertions into a play test, so we need expect from Storybook’s test library too.

import { virtual } from '@guidepup/virtual-screen-reader';
import { expect } from '@storybook/test';

Write the story and integration test

See comments in-the-code below for details

// Default LoggedIn story
export const LoggedIn = {
  args: {
    user: {
      name: 'Jane Doe',
    },
  },
  // add the play method - it's where you put your tests!
  play: async ({ args, canvasElement, step }) => {
    // Start up the Virtual Screen Reader, giving it the canvasElement
    await virtual.start({ container: canvasElement });

    // This `while` statement navigates through the component using the 
    //  Virtual Screen Reader to speak the text whereever the cursor is located.
    //  It will continue until it reaches the end of the <header> element, 
    //  which has a `banner` aria role, thus "end of banner"
    while ((await virtual.lastSpokenPhrase()) !== "end of banner") {
      // `.next()` moves the Virtual cursor to the next location.
      await virtual.next();
    }

    // What we expect the screen reader to say
    const expected = [
      'banner',
      // it is an `<h1>`, so `level 1`
      'heading, Acme, level 1',
      'Welcome,',
      // Using `args` here allows you to change args without breaking the test
      args.user.name,
      '!',
      'button, Log out',
      'end of banner'
    ]

    // Here's the test! It asserts the screen reader said what we expected.
    //  When we called `lastSpokenPhrase` every time the `while` looped, 
    //  the spoken text was added to the `PhraseLog`, in order
    expect(await virtual.spokenPhraseLog()).toEqual(expected);
    
    // Stop your Virtual Screen Reader instance
    await virtual.stop();
  }
};

Screenshot of test results in Storybook integration test using Virtual Screen Reader output

Conclusion

This example is simple on purpose so it can easily get the copypasta treatment. Once it works for you, dig into the Virtual Screen Reader docs and see how much more you can test for with this tool. Cheers!