Deque

Accessibility Testing
with Angular

By Marcy Sutton / @MarcySutton
Senior Front-End Engineer, Deque Systems

Who is Marcy Sutton?

axe-core
Egghead.io AngularJS Girl Develop It

Why Accessibility?

15% of the population has some kind of disability

Source: World Health Organization

Some people can’t:

  • Use a mouse
  • View a screen
  • See low contrast text
  • Hear dialogue or music
  • Understand complex language

Some people need:

  • Keyboard support
  • Screen reader support
  • High contrast text
  • Captions and transcripts
  • Plain language

Design and develop for ‘future you’

Microsoft Inclusive Design Toolkit

Permanent disability to injury or situational disability

Guess what: you can help!

…by making the Internet more accessible.

#A11y in Angular Apps

  • Write meaningful HTML
  • Enable the keyboard
  • Handle focus
  • Alert the user
  • Coverage with tests

Meaningful HTML


 <label for="roundTrip">
   <input type="radio" id="roundTrip">
   Round Trip
 </label>
							
HTML Radio Input

Also Meaningful HTML


 <button>Button</button>
							
HTML Button

Angular Material button in Chrome Accessibility Inspector

CSS changes button text to uppercase following the Material Design guidelines, which shows in the accessibility tree

Enable the keyboard

Handle focus

Alert the user

Intro to ARIA

https://www.w3.org/TR/wai-aria/

Role: what is it?
role="button"
State: what state is it in?
aria-checked="false"
Property: what's the nature of it?
aria-haspopup="true"
aria-label="Close modal"

Accessibility with ngAria: Angular 1 documentation

ngAria

aria-checked code example and link to documentation https://docs.angularjs.org/api/ngAria

While ngAria was a worthwhile effort,
it only helps certain use cases.

Build accessibility into your workflow
~ with ~
Automated Testing

~ But ~ It’s no substitute for real user feedback

Testing for accessibility in:

Angular 1*

Directive-Specific
Unit Tests

Good for:

  • Text alternatives / Labeling
  • Keyboard operability
  • ARIA attribute watching

Labeling unit test


  it('should apply aria-hidden="true" when parent has valid label', 
  inject(function() {

    var el = make('<md-radio-button aria-label="Squishy Avatar" '+
    				   'role="radio" tabindex="0"> '+
                '<div class="md-container"></div> '+
                  '<div class="md-label"> '+
                  '<md-icon md-svg-icon="android"></md-icon> '+
                '</div></md-radio-button>');

    expect(el.find('md-icon').attr('aria-hidden')).toEqual('true');
  }));
						
mdIcon Spec

Keyboard unit test


   it('closes on escape', inject(function($document, $mdConstant) {
      var menu = setup();
      openMenu(menu);
      expect(getOpenMenuContainer(menu).length).toBe(1);

      var openMenuEl = $document[0].querySelector('md-menu-content');

      pressKey(openMenuEl, $mdConstant.KEY_CODE.ESCAPE);
      waitForMenuClose();

      expect(getOpenMenuContainer(menu).length).toBe(0);
    }));
						
mdMenu Spec

Integration/
End-to-End Tests

Good for:

  • Color contrast
  • Widget keyboard interrop
  • Document-level rules

Get help with an API

Save yourself from:

  • Label/name computation
  • Incorrect ARIA usage
  • Color contrast
  • Data table markup
  • Viewport/zooming probz

A11y test APIs: You’ve got options

  • aXe
  • Chrome A11y Developer Tools
  • QUAIL
  • Tenon
  • WAVE

axe-core API

https://github.com/dequelabs/axe-core

  • Runs locally
  • Open source
  • Free
  • Good for integration tests
  • Also unit tests

There’s also a supported/enterprise option, Worldspace Attest

aXe Chrome extension ~ http://bit.ly/axe-chrome
Angular.js Docs in the aXe Chrome extension
Angular Material Demo App
Angular Material Muppets app

Demo App Template


  <md-toolbar layout="row">
	<h1 layout-align-gt-sm="center center" class="md-toolbar-tools">My Favorite Muppets</h1>
  </md-toolbar>
  <div layout="row" flex class="content-wrapper">
    <md-sidenav layout="column" class="md-sidenav-left md-whiteframe-z2" md-component-id="left">
        <md-list class="muppet-list">
          <md-item ng-repeat="it in muppets">
            <md-item-content>
              <md-button ng-click="selectMuppet(it)" ng-class="{'selected' : it === selected }">
                <img ng-src="{{it.iconurl}}" class="face" alt="">
                {{it.name}}
              </md-button>
            </md-item-content>
          </md-item>
        </md-list>
      </md-sidenav>
      <div layout="column" flex class="content-wrapper" id="primary-col">
          <md-content layout="column" flex class="md-padding">
            <h2>{{selected.name}}</h2>
            <p>{{selected.content}}</p>
            <div class="cell">
              <img ng-src="{{selected.imgurl}}" alt="{{selected.imgalt}}">
            </div>
          </md-content>
      </div>
  </div>
						
http://bit.ly/ngmaterial-muppets

End-to-End Test


  var AxeBuilder = require('axe-webdriverjs');

  describe('view1', function() {

    beforeEach(function() { browser.get('index.html'); });

    it('should change Muppets', function() {
      element.all(by.css('[ng-click="selectMuppet(it)"]')).first().
        sendKeys(protractor.Key.ENTER);

      expect(element.all(by.css('#primary-col h2')).first().getText()).
        toMatch('Animal');
    });

    it('should have no accessibility violations', function(done) {
      AxeBuilder(browser)
        .analyze(function(results) {
          if (results.violations.length > 0) {
            console.log(results.violations);
          }
          expect(results.violations.length).toBe(0);
          done();
      });
  });
						
https://github.com/dequelabs/axe-webdriverjs

Command-Line Demo

axe-angular1-demo

Testing for accessibility in:

Angular 2*

Angular 2: Big differences

  • Component-based
  • TypeScript & ES6
  • Property bindings

Medium article by Victor Savkin: Three Ways to Test Angular 2 Components

Test requirements for a11y

Test method Rendered Attached
Unit
Keyboard mechanics X X
Labeling
ARIA attributes ~ ~
End-to-end
Color contrast X X
Event handlers ~ ~
A11y API Audit X X

Angular Material 2 Preview

Material 2 checkbox operated with Voiceover
Material 2 checkbox using Voiceover

Angular 2 Unit Test 1/2

Applying a text alternative with `aria-labelledby`


 describe('mdCheckbox with provided aria-labelledby ', () => {
    let checkboxDebugElement: DebugElement;
    let checkboxNativeElement: HTMLElement;
    let inputElement: HTMLInputElement;

    it('should use the provided aria-labelledby', () => {
      fixture = TestBed.createComponent(CheckboxWithAriaLabelledby);
      
      checkboxDebugElement = fixture.debugElement.query(By.directive(MdCheckbox));
      checkboxNativeElement = checkboxDebugElement.nativeElement;

      inputElement = <HTMLInputElement>checkboxNativeElement.querySelector('input');

      fixture.detectChanges();
      expect(inputElement.getAttribute('aria-labelledby')).toBe('some-id');
  });

  /** Simple test component with aria-labelledby set. */
  @Component({
    template: ``
  })
  class CheckboxWithAriaLabelledby {}
					
Material 2 Checkbox Spec

Angular 2 Unit Test 2/2

ARIA attribute watching

	
    it('toggles "aria-checked" on the host element', function() {
      builder.createAsync(CheckboxController).then(function(fixture) {
        let el = fixture.debugElement.query(By.css('.md-checkbox'));

        expect(el.nativeElement.getAttribute('aria-checked')).toEqual('false');

        controller = fixture.componentInstance;
        changePromise = waitForChange();
        controller.isChecked = true;

        fixture.detectChanges();

        expect(el.nativeElement.getAttribute('aria-checked')).toEqual('true');
      });
    });
							
Older Material 2 Checkbox Spec

Angular 2 Keyboard Unit Tests

Gif with Supergirl saying I'm Bleeding?

Angular 2 issue on Github: Creating fake event objects for testing components

Angular 2 Keyboard Test


    it('should respond to keyboard events', () => {
      let fixture = TestBed.createComponent(Board);
      fixture.detectChanges();

      var event1 = new KeyboardEvent('keydown', { key: '1' });

      var event3 = new KeyboardEvent('keydown', { key: '3' });

      window.dispatchEvent(event1);
      window.dispatchEvent(event3);
      fixture.detectChanges();

      let board = fixture.nativeElement;
      let box = board.querySelectorAll('.ttt-box')[2];

      expect(box.textContent).toContain('x');
    });
						
Tic-tac-toe Game Spec by Julie Ralph

End-to-End Testing in Angular 2

~ with ~

Protractor

End-to-End Keyboard Test


  /*----- menu.e2e.ts -------*/
  import { MenuPage } from './menu-page';

  describe('menu', () => {
    let page: MenuPage;

    beforeEach(function() {
      page = new MenuPage();
    });

    describe('keyboard events', () => {
      beforeEach(() => {
        // click start button to avoid tabbing past navigation
        page.start().click();
        page.pressKey(protractor.Key.TAB);
      });

      it('should auto-focus the first item when opened with keyboard', () => {
        page.pressKey(protractor.Key.ENTER);
        page.expectFocusOn(page.items(0));
     });

      it('should focus subsequent items when down arrow is pressed', () => {
        page.pressKey(protractor.Key.ENTER);
        page.pressKey(protractor.Key.DOWN);
        page.expectFocusOn(page.items(1));
      });
    });
  });

  /*----- menu-page.ts -------*/
  pressKey(key: any): void {
    browser.actions().sendKeys(key).perform();
  }

  expectFocusOn(el: ElementFinder): void {
    expect(browser.driver.switchTo().activeElement().getInnerHtml())
        .toBe(el.getInnerHtml());
  }

	 				
Material 2 Menu

Mocking Keyboard Events

Bug in ChromeDriver

DOM helper getKeyEvent does not use keyCode
https://github.com/angular/angular/issues/9419

End-to-end Testing with aXe-core


  import * as axe from 'axe-core';
  
  ...

  it('should have no accessibility violations', function(done) {
	browser.executeScript(axe.source);

	// Run A11Y tests in the browsers event loop.
	browser.executeAsyncScript((resolve: any) => {
	  return new Promise<axe.AxeResults>(res => {
	  	axe.a11yCheck(document, {}, resolve)
	  });
	}).then((results: any) => {
	  if (results.violations.length > 0) {
	    console.log(results.violations);
	  }
	  expect(results.violations.length).toBe(0);
	  done();
	});
  });
							
Protractor test

Recap

  • Accessibility is similar with all web frameworks.
  • Unit tests provide a component accessibility contract.
  • Use integration tests to redirect human resources.
  • Use an API for extra a11y muscle.
  • Prevent broken experiences from going out the door!

Thanks!

twitter.com/marcysutton

github.com/marcysutton