Deque

Automating peace of mind
with
Accessibility Testing &
Continuous Integration

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

Slides: http://bit.ly/a11y-and-ci

Who is Marcy Sutton?

axe-core
Accessibility Seattle Egghead.io Girl Develop It

https://a11ywins.tumblr.com

Nightmare Scenario

What if:

You just shipped a redesign making your website unusable to 15-20% of users ๐Ÿ˜ฎ

Web Accessibility

People with disabilities can perceive, understand, navigate, and interact with the Web, and that they can contribute to the Web.
https://www.w3.org/WAI/intro/accessibility.php

One bad deployment away

How not to merge

Software Testing
a winning strategy for web a11y

Kramer's coffee table book on coffee tables
It's meta!

Today

  • Software quality
  • Continuous integration
  • Automating accessibility
  • Using technology for good

Why geek out over this?

Dancing in an elevator

Validating Code & Covering Your Six


Functional Testing

  • Unit Testing
  • Integration Testing
  • System testing
  • Acceptance Testing

Non-Functional Testing

  • Performance Testing
  • Security Testing
  • Usability Testing
  • Compatibility Testing

Accessibility Testing

a.k.a. cool stuff ๐Ÿ†’๐Ÿ˜Ž

Automated

  • HTML/ARIA validation
  • Form labels
  • Color contrast
  • Accessible names
  • Focus management
  • Specifying a language

And more...

Manual

  • Focus order
  • Text alternative quality
  • Screen reader testing
  • Keyboard support
  • Contrast over images/gradients
  • Error identification

Source

How are people affected?

http://webaim.org/intro/#people

  • Visual
  • Hearing
  • Motor
  • Cognitive

Checking Your Work

https://www.w3.org/TR/WCAG21/

WCAG landing page

What can we automate?

Tests can catch roughly 30-50% of a11y issues, depending on the rule set.

Related: Auto-WCAG Community Group & Accessibility Conformance Testing (ACT) Task Force at W3C

~ But ~ Automated testing is no substitute for manual tests & real user feedback.

Accessibility Unit Tests

Good for:

  • Component-specific behavior
  • Interaction/focus APIs
  • Text alternatives
  • ARIA states

Unit Test Example

https://github.com/davidtheclark/react-aria-menubutton

  it('Manager#openMenu focusing in menu', function() {
    var manager = createManagerWithMockedElements();
    manager.openMenu({ focusMenu: true });
    expect(manager.isOpen).toBe(true);
    expect(manager.menu.setState).toHaveBeenCalledTimes(1);
    expect(manager.menu.setState.mock.calls[0]).toEqual([{ isOpen: true }]);
    expect(manager.button.setState).toHaveBeenCalledTimes(1);
    expect(manager.button.setState.mock.calls[0]).toEqual([{ menuOpen: true }]);

    return new Promise(function(resolve) {
      setTimeout(function() {
        expect(manager.focusItem).toHaveBeenCalledTimes(1);
        expect(manager.focusItem.mock.calls[0]).toEqual([0]);
        resolve();
      }, 0);
    });
  });
						

Integration/E2E Tests

Good for:

  • Real-world browser testing
  • Document/page-level rules
  • Widget interrop
  • Color contrast
  • Framework testing options

End-to-End test example

Angular Material 2 Dialog

import {browser, by, element, Key} from 'protractor';
import {expectToExist, expectFocusOn} from '../../util/asserts';
import {pressKeys, clickElementAtPoint} from '../../util/actions';
import {waitForElement} from '../../util/query';

describe('dialog', () => {
  beforeEach(() => browser.get('/dialog'));

  it('should open a dialog', () => {
    element(by.id('default')).click();
    expectToExist('md-dialog-container');
  });

  it('should close by clicking on the backdrop', () => {
    element(by.id('default')).click();

    waitForDialog().then(() => {
      clickOnBackrop();
      expectToExist('md-dialog-container', false);
    });
  });

  it('should close by pressing escape', () => {
    element(by.id('default')).click();

    waitForDialog().then(() => {
      pressKeys(Key.ESCAPE);
      expectToExist('md-dialog-container', false);
    });
  });

  it('should close by pressing escape when the first tabbable element has lost focus', () => {
    element(by.id('default')).click();

    waitForDialog().then(() => {
      clickElementAtPoint('md-dialog-container', { x: 0, y: 0 });
      pressKeys(Key.ESCAPE);
      expectToExist('md-dialog-container', false);
    });
  });

  it('should close by clicking on the "close" button', () => {
    element(by.id('default')).click();

    waitForDialog().then(() => {
      element(by.id('close')).click();
      expectToExist('md-dialog-container', false);
    });
  });

  it('should focus the first focusable element', () => {
    element(by.id('default')).click();

    waitForDialog().then(() => {
      expectFocusOn('md-dialog-container input');
    });
  });

  it('should restore focus to the element that opened the dialog', () => {
    let openButton = element(by.id('default'));

    openButton.click();

    waitForDialog().then(() => {
      clickOnBackrop();
      expectFocusOn(openButton);
    });
  });

  it('should prevent tabbing out of the dialog', () => {
    element(by.id('default')).click();

    waitForDialog().then(() => {
      let tab = Key.TAB;

      pressKeys(tab, tab, tab);
      expectFocusOn('#close');
    });
  });

  it('should be able to prevent closing by clicking on the backdrop', () => {
    element(by.id('disabled')).click();

    waitForDialog().then(() => {
      clickOnBackrop();
      expectToExist('md-dialog-container');
    });
  });

  it('should be able to prevent closing by pressing escape', () => {
    element(by.id('disabled')).click();

    waitForDialog().then(() => {
      pressKeys(Key.ESCAPE);
      expectToExist('md-dialog-container');
    });
  });

  function waitForDialog() {
    return waitForElement('md-dialog-container');
  }

  function clickOnBackrop() {
    clickElementAtPoint('.cdk-overlay-backdrop', { x: 0, y: 0 });
  }
});
						

Write a combination
of tests to catch a variety
of bugs ๐Ÿ’ป๐Ÿ”Ž๐Ÿ›

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

Test requirements for a11y

Test method Rendered Attached
Unit
Keyboard focus X X
Labeling
ARIA attributes ~ ~
End-to-end
Color contrast X X
Event handlers ~ ~
Accessibility Test APIs X X

Get help with an
Accessibility Testing API

Save yourself from:

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

aXe-core Accessibility Testing Engine

https://www.npmjs.com/package/axe-core

axe-core on npm

What is axe-core?

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

  • Open-source JS library
  • No network or API key required
  • Configurable rules & checks
  • No false positives
  • Based on standards

Unit test with axe-core

Expect no violations in the aXe results object

https://axe-core.org/docs/

  var axe = require('axe-core');

  describe('Custom component', function() {
    it('should have no a11y violations', function(done) {
      let options = {};

      axe.run('.some-element-selector', options, function (error, results) {
        expect(results.violations.length).toBe(0);
        done();
      });
    });
  });
						

Integration test with aXe-WebdriverJS

https://github.com/dequelabs/axe-webdriverjs
  var AxeBuilder = require('axe-webdriverjs'),
      WebDriver = require('selenium-webdriver');
 
  var driver = new WebDriver.Builder()
    .forBrowser('chrome').build();
 
  driver
    .get('https://localhost:4000')
    .then(function (done) {
      AxeBuilder(driver)
        .analyze(function (results) {
          expect(results.violations.length).toBe(0);
          done();
        });
    });

						

Closer to the metal with aXe-CLI

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

  $ npm install axe-cli -g

  $ axe www.deque.com

  $ axe www.deque.com --include #main --exclude #aside

  $ axe www.deque.com --browser chrome

  $ axe www.deque.com --rules color-contrast,html-has-lang

  $ axe www.deque.com --save deque-site.json
						

aXe-core JSON Output

https://github.com/dequelabs/axe-core/blob/develop/doc/API.md

  Accessibility Violations:  10
[ { description: 'Ensures buttons have discernible text',
    help: 'Buttons must have discernible text',
    helpUrl: 'https://dequeuniversity.com/rules/axe/2.2/button-name?application=webdriverjs',
    id: 'button-name',
    impact: 'critical',
    nodes:
     [ { all: [ [length]: 0 ],
         any:
          [ { data: null,
              id: 'non-empty-if-present',
              impact: 'critical',
              message: 'Element has a value attribute and the value attribute is empty',
              relatedNodes: [ [length]: 0 ] },
            { data: null,
              id: 'non-empty-value',
              impact: 'critical',
              message: 'Element has no value attribute or the value attribute is empty',
              relatedNodes: [ [length]: 0 ] },
            { data: '',
              id: 'button-has-visible-text',
              impact: 'critical',
              message: 'Element does not have inner text that is visible to screen readers',
              relatedNodes: [ [length]: 0 ] },
            { data: null,
              id: 'aria-label',
              impact: 'critical',
              message: 'aria-label attribute does not exist or is empty',
              relatedNodes: [ [length]: 0 ] },
            { data: null,
              id: 'aria-labelledby',
              impact: 'critical',
              message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible',
              relatedNodes: [ [length]: 0 ] },
            { data: null,
              id: 'role-presentation',
              impact: 'moderate',
              message: 'Element\'s default semantics were not overridden with role="presentation"',
              relatedNodes: [ [length]: 0 ] },
            { data: null,
              id: 'role-none',
              impact: 'moderate',
              message: 'Element\'s default semantics were not overridden with role="none"',
              relatedNodes: [ [length]: 0 ] },
            [length]: 7 ],
         html: '<button class="ui-datepicker-trigger" type="button">\n\t\t\t\t\t\t  \n\t\t\t\t\t\t</button>',
         impact: 'critical',
         none:
          [ { data: null,
              id: 'focusable-no-name',
              impact: 'serious',
              message: 'Element is in tab order and does not have accessible text',
              relatedNodes: [ [length]: 0 ] },
            [length]: 1 ],
         target:
          [ '#route0 > .wrapper.departure-date > button:nth-of-type(2)',
            [length]: 1 ] },
       [length]: 1 ],
    tags:
     [ 'cat.name-role-value',
       'wcag2a',
       'wcag412',
       'section508',
       'section508.22.a',
       [length]: 5 ] },
  { description: 'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
    help: 'Elements must have sufficient color contrast',
    helpUrl: 'https://dequeuniversity.com/rules/axe/2.2/color-contrast?application=webdriverjs',
    id: 'color-contrast',
    impact: 'critical',
    nodes:
     [ { all: [ [length]: 0 ],
         any:
          [ { data:
               { bgColor: '#344b6e',
                 contrastRatio: 4.49,
                 fgColor: '#acbad0',
                 fontSize: '12.0pt',
                 fontWeight: 'normal',
                 missingData: [ [length]: 0 ] },
              id: 'color-contrast',
              impact: 'critical',
              message: 'Element has insufficient color contrast of 4.49 (foreground color: #acbad0, background color: #344b6e, font size: 12.0pt, font weight: normal)',
              relatedNodes:
               [ { html: '<body class="public-en" data-twttr-rendered="true">',
                   target:
                    [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en',
                      [length]: 1 ] },
                 [length]: 1 ] },
            [length]: 1 ],
         html: '

A trip to Mars starts in your imagination. Are you bold enough, brave enough, crazy enough? We are. You belong on Mars with crazies like us. Most of us don\'t bite. Much.

', impact: 'critical', none: [ [length]: 0 ], target: [ '#left-column > div:nth-of-type(1) > h2', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: { bgColor: '#344b6e', contrastRatio: 4.31, fgColor: '#ff9999', fontSize: '13.5pt', fontWeight: 'normal', missingData: [ [length]: 0 ] }, id: 'color-contrast', impact: 'critical', message: 'Element has insufficient color contrast of 4.31 (foreground color: #ff9999, background color: #344b6e, font size: 13.5pt, font weight: normal)', relatedNodes: [ { html: '<body class="public-en" data-twttr-rendered="true">', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<h3>Be Bold...</h3>', impact: 'critical', none: [ [length]: 0 ], target: [ '#vap-plan > a > h3', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: { bgColor: '#344b6e', contrastRatio: 4.49, fgColor: '#acbad0', fontSize: '9.0pt', fontWeight: 'normal', missingData: [ [length]: 0 ] }, id: 'color-contrast', impact: 'critical', message: 'Element has insufficient color contrast of 4.49 (foreground color: #acbad0, background color: #344b6e, font size: 9.0pt, font weight: normal)', relatedNodes: [ { html: '<body class="public-en" data-twttr-rendered="true">', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<p>Step out of your comfort zone, and into a rocket with enough fuel to blast a Manhattan-sized crater if it explodes. But it won\'t. Probably.<br>\n  </p>', impact: 'critical', none: [ [length]: 0 ], target: [ '#vap-plan > p:nth-of-type(2)', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: { bgColor: '#344b6e', contrastRatio: 1.87, fgColor: '#067ab4', fontSize: '13.5pt', fontWeight: 'normal', missingData: [ [length]: 0 ] }, id: 'color-contrast', impact: 'critical', message: 'Element has insufficient color contrast of 1.87 (foreground color: #067ab4, background color: #344b6e, font size: 13.5pt, font weight: normal)', relatedNodes: [ { html: '<body class="public-en" data-twttr-rendered="true">', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<h3>Countdown...</h3>', impact: 'critical', none: [ [length]: 0 ], target: [ '#vap-book > a > h3', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: { bgColor: '#344b6e', contrastRatio: 4.49, fgColor: '#acbad0', fontSize: '9.0pt', fontWeight: 'normal', missingData: [ [length]: 0 ] }, id: 'color-contrast', impact: 'critical', message: 'Element has insufficient color contrast of 4.49 (foreground color: #acbad0, background color: #344b6e, font size: 9.0pt, font weight: normal)', relatedNodes: [ { html: '<body class="public-en" data-twttr-rendered="true">', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<p>If you\'re serious about traveling to Mars - really serious - then <a href="/demo/mars/mars2?a=last_will">prepare your last will and testament</a>, and book a trip! </p>', impact: 'critical', none: [ [length]: 0 ], target: [ '#vap-book > p:nth-of-type(2)', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: { bgColor: '#344b6e', contrastRatio: 2.83, fgColor: '#46a546', fontSize: '13.5pt', fontWeight: 'normal', missingData: [ [length]: 0 ] }, id: 'color-contrast', impact: 'critical', message: 'Element has insufficient color contrast of 2.83 (foreground color: #46a546, background color: #344b6e, font size: 13.5pt, font weight: normal)', relatedNodes: [ { html: '<body class="public-en" data-twttr-rendered="true">', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<h3>Blast Off!</h3>', impact: 'critical', none: [ [length]: 0 ], target: [ '#vap-travel > a > h3', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: { bgColor: '#344b6e', contrastRatio: 4.49, fgColor: '#acbad0', fontSize: '9.0pt', fontWeight: 'normal', missingData: [ [length]: 0 ] }, id: 'color-contrast', impact: 'critical', message: 'Element has insufficient color contrast of 4.49 (foreground color: #acbad0, background color: #344b6e, font size: 9.0pt, font weight: normal)', relatedNodes: [ { html: '<body class="public-en" data-twttr-rendered="true">', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<p>Expect violent turbulence, bone-crushing g-forces, muscle atrophy, and certain death (hey, everyone\'s death is certain at some point, right?).<br>\n  </p>', impact: 'critical', none: [ [length]: 0 ], target: [ '#vap-travel > p:nth-of-type(2)', [length]: 1 ] }, [length]: 7 ], tags: [ 'cat.color', 'wcag2aa', 'wcag143', [length]: 3 ] }, { description: 'Ensures every id attribute value is unique', help: 'id attribute value must be unique', helpUrl: 'https://dequeuniversity.com/rules/axe/2.2/duplicate-id?application=webdriverjs', id: 'duplicate-id', impact: 'critical', nodes: [ { all: [ [length]: 0 ], any: [ { data: 'control-panel', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: control-panel', relatedNodes: [ { html: '<div id="control-panel" class="container-fluid-full">', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loggedin > .container-fluid-full', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<div id="control-panel" class="container-fluid-full">', impact: 'critical', none: [ [length]: 0 ], target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loginnow > .container-fluid-full', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'left-control-nav', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: left-control-nav', relatedNodes: [ { html: '<nav id="left-control-nav" class="pull-left">', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loggedin > .container-fluid-full > .container > .span5.left-first.pull-left > nav', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<nav id="left-control-nav" class="pull-left">', impact: 'critical', none: [ [length]: 0 ], target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loginnow > .container-fluid-full > .container > .span5.left-first.pull-left > nav', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'search-bar', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: search-bar', relatedNodes: [ { html: '<div id="search-bar" class="pull-left">', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loggedin > .container-fluid-full > .container > .span5.left-first.pull-left > div', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<div id="search-bar" class="pull-left">', impact: 'critical', none: [ [length]: 0 ], target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loginnow > .container-fluid-full > .container > .span5.left-first.pull-left > div', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'search', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: search', relatedNodes: [ { html: '<form id="search" action="/demo/mars/mars2" method="get">\n\t\t\t\t\t\t <input type="hidden" name="fn" value="Search">\t\t\t\t\t\t \n\t\t\t\t\t\t\t<input type="text" class="search" name="query" placeholder="search">\n\t\t\t\t\t\t\t<input type="submit" class="control-search">\n\t\t\t\t\t\t</form>', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loggedin > .container-fluid-full > .container > .span5.left-first.pull-left > div > form', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<form id="search" action="/demo/mars/mars2" method="get">\n\t\t\t\t\t\t <input type="hidden" name="fn" value="Search">\t\t\t\t\t\t \n\t\t\t\t\t\t\t<input type="text" class="search" name="query" placeholder="search">\n\t\t\t\t\t\t\t<input type="submit" class="control-search">\n\t\t\t\t\t\t</form>', impact: 'critical', none: [ [length]: 0 ], target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loginnow > .container-fluid-full > .container > .span5.left-first.pull-left > div > form', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'right-control-nav', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: right-control-nav', relatedNodes: [ { html: '<nav id="right-control-nav" class="pull-right">', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loggedin > .container-fluid-full > .container > .span7.pull-right > .pull-right', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<nav id="right-control-nav" class="pull-right" style="display: inline;">', impact: 'critical', none: [ [length]: 0 ], target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loginnow > .container-fluid-full > .container > .span7.pull-right > .pull-right', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'vap-section', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: vap-section', relatedNodes: [ { html: '<div id="vap-section">', target: [ '#left-column > div:nth-of-type(2)', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<div id="vap-section">\n<h1 style="color:#eee;">Destination Mars </h1>\n\t<h2 style="color:#acbad0;">A trip to Mars starts in your imagination. Are you bold enough, brave enough, <strong>crazy enough?</strong> We are. You belong on Mars with crazies like us. Most of us don\'t bite. Much.</h2></div>', impact: 'critical', none: [ [length]: 0 ], target: [ '#left-column > div:nth-of-type(1)', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'nCountries', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: nCountries', relatedNodes: [ { html: '<input type="hidden" name="nCountries" value="1" id="nCountries">', target: [ '#passfinder > input:nth-of-type(4)', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<input type="hidden" id="nCountries" name="nCountries">', impact: 'critical', none: [ [length]: 0 ], target: [ '#select-country > input', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'passenger-select', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: passenger-select', relatedNodes: [ { html: '<div id="passenger-select" class="widget-container middle">', target: [ '#passfinder > div:nth-of-type(2)', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<div id="passenger-select" class="widget-container middle">', impact: 'critical', none: [ [length]: 0 ], target: [ '#form1 > div:nth-of-type(2)', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'passengers', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: passengers', relatedNodes: [ { html: '<div id="passengers">', target: [ '#passfinder > div:nth-of-type(2) > .interior-container > div', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<div id="passengers">', impact: 'critical', none: [ [length]: 0 ], target: [ '#form1 > div:nth-of-type(2) > .interior-container > div:nth-of-type(2)', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'traveler0', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: traveler0', relatedNodes: [ { html: '<select id="traveler0" class="traveler-type" name="paxAge0">', target: [ '#r-passenger0 > .wrapper.age-range > .traveler-type', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<select id="traveler0" class="traveler-type">\n\t\t\t\t\t\t\t\t\t\t<option value="0">Adult (26+)</option>\n\t\t\t\t\t\t\t\t\t\t<option value="1">Youth (12-25)</option>\n\t\t\t\t\t\t\t\t\t\t<option value="2">Child (4-11)</option>\n\t\t\t\t\t\t\t\t\t\t<option value="3">Senior (60+)</option>\n\t\t\t\t\t\t\t\t\t</select>', impact: 'critical', none: [ [length]: 0 ], target: [ '#passenger0 > .wrapper.age-range > .traveler-type', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'age0', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: age0', relatedNodes: [ { html: '<select id="age0" name="youthAge0" class="age">', target: [ '#r-passenger0 > .wrapper.youth-age > .age', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<select id="age0" class="age">', impact: 'critical', none: [ [length]: 0 ], target: [ '#passenger0 > .wrapper.youth-age > .age', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'default', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: default', relatedNodes: [ { html: '<a target="player" data-text="Why Mars died" class="fader first" href="http://www.youtube.com/embed/oC31pqk9sak?controls=1&showinfo=1&modestbranding=0&wmode=opaque&enablejsapi=1" id="default"></a>', target: [ '#video-box > .interior-container > .vid-nav > a:nth-of-type(2)', [length]: 1 ] }, { html: '<a target="player" data-text="The world that never was" class="fader first" href="http://www.youtube.com/embed/JgMXPXdqJn8?controls=1&showinfo=1&modestbranding=0&wmode=opaque&enablejsapi=1" id="default"></a>', target: [ '#video-box > .interior-container > .vid-nav > a:nth-of-type(3)', [length]: 1 ] }, [length]: 2 ] }, [length]: 1 ], html: '<a target="player" data-text="Life was possible on Mars" class="fader first active" href="http://www.youtube.com/embed/OagLGti_hTE?controls=1&showinfo=1&modestbranding=0&wmode=opaque&enablejsapi=1" id="default"></a>', impact: 'critical', none: [ [length]: 0 ], target: [ '#video-box > .interior-container > .vid-nav > .fader.first.active', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: 'ui-datepicker-div', id: 'duplicate-id', impact: 'critical', message: 'Document has multiple elements with the same id attribute: ui-datepicker-div', relatedNodes: [ { html: '<div id="ui-datepicker-div" class="ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>', target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > div:nth-of-type(10)', [length]: 1 ] }, [length]: 1 ] }, [length]: 1 ], html: '<div id="ui-datepicker-div" class="ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>', impact: 'critical', none: [ [length]: 0 ], target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > div:nth-of-type(9)', [length]: 1 ] }, [length]: 13 ], tags: [ 'cat.parsing', 'wcag2a', 'wcag411', [length]: 3 ] }, { description: 'Ensures <iframe> and <frame> elements contain a non-empty title attribute', help: 'Frames must have title attribute', helpUrl: 'https://dequeuniversity.com/rules/axe/2.2/frame-title?application=webdriverjs', id: 'frame-title', impact: 'critical', nodes: [ { all: [ [length]: 0 ], any: [ { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'non-empty-title', impact: 'critical', message: 'Element has no title attribute or the title attribute is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<iframe width="365" height="205" name="player" id="player" src="https://www.youtube.com/embed/OagLGti_hTE?controls=1&showinfo=1&modestbranding=0&wmode=opaque&enablejsapi=1" frameborder="0" allowfullscreen="" cd_frame_id_="cff1794f3ec5c01fd7a128a35718c031"></iframe>', impact: 'critical', none: [ [length]: 0 ], target: [ '#player', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'non-empty-title', impact: 'critical', message: 'Element has no title attribute or the title attribute is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<iframe id="fafbba78" name="f2bc5e72d" scrolling="no" style="border: none; overflow: hidden; height: 62px; width: 292px;" class="fb_ltr" src="/assets/demo-sites/mars/js/likebox.html" cd_frame_id_="555346de1cca73b48cf446ddfbe19cab"></iframe>', impact: 'critical', none: [ [length]: 0 ], target: [ '#fafbba78', [length]: 1 ] }, [length]: 2 ], tags: [ 'cat.text-alternatives', 'wcag2a', 'wcag241', 'section508', 'section508.22.i', [length]: 5 ] }, { description: 'Ensures every HTML document has a lang attribute', help: '<html> element must have a lang attribute', helpUrl: 'https://dequeuniversity.com/rules/axe/2.2/html-has-lang?application=webdriverjs', id: 'html-has-lang', impact: 'serious', nodes: [ { all: [ [length]: 0 ], any: [ { data: null, id: 'has-lang', impact: 'serious', message: 'The <html> element does not have a lang attribute', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], html: '<html class=" js no-flexbox flexbox-legacy canvas canvastext webgl no-touch geolocation postmessage websqldatabase indexeddb hashchange history draganddrop websockets rgba hsla multiplebgs backgroundsize borderimage borderradius boxshadow textshadow opacity cssanimations csscolumns cssgradients cssreflections csstransforms csstransforms3d csstransitions fontface generatedcontent video audio localstorage sessionstorage webworkers applicationcache svg inlinesvg smil svgclippaths js no-flexbox flexbox-legacy canvas canvastext webgl no-touch geolocation postmessage websqldatabase indexeddb hashchange history draganddrop websockets rgba hsla multiplebgs backgroundsize borderimage borderradius boxshadow textshadow opacity cssanimations csscolumns cssgradients cssreflections csstransforms csstransforms3d csstransitions fontface generatedcontent video audio localstorage sessionstorage webworkers applicationcache svg inlinesvg smil svgclippaths deque-axe-is-ready">', impact: 'serious', none: [ [length]: 0 ], target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready', [length]: 1 ] }, [length]: 1 ], tags: [ 'cat.language', 'wcag2a', 'wcag311', [length]: 3 ] }, { description: 'Ensures <img> elements have alternate text or a role of none or presentation', help: 'Images must have alternate text', helpUrl: 'https://dequeuniversity.com/rules/axe/2.2/image-alt?application=webdriverjs', id: 'image-alt', impact: 'critical', nodes: [ { all: [ [length]: 0 ], any: [ { data: null, id: 'has-alt', impact: 'critical', message: 'Element does not have an alt attribute', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'non-empty-title', impact: 'critical', message: 'Element has no title attribute or the title attribute is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 6 ], html: '<img src="/assets/demo-sites/mars/js/seg" width="1" height="1">', impact: 'critical', none: [ [length]: 0 ], target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > div:nth-of-type(1) > img:nth-of-type(3)', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'has-alt', impact: 'critical', message: 'Element does not have an alt attribute', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'non-empty-title', impact: 'critical', message: 'Element has no title attribute or the title attribute is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 6 ], html: '<img src="/assets/demo-sites/mars/images/mars-spaceman.jpg" class="" width="210" height="120">', impact: 'critical', none: [ [length]: 0 ], target: [ '#vertical-carousel > .carousel > ul > li:nth-of-type(1) > a > img', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'has-alt', impact: 'critical', message: 'Element does not have an alt attribute', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'non-empty-title', impact: 'critical', message: 'Element has no title attribute or the title attribute is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 6 ], html: '<img src="/assets/demo-sites/mars/images/mars-spaceman.jpg" class="" width="210" height="120">', impact: 'critical', none: [ [length]: 0 ], target: [ '#vertical-carousel > .carousel > ul > li:nth-of-type(8) > a > img', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'has-alt', impact: 'critical', message: 'Element does not have an alt attribute', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'non-empty-title', impact: 'critical', message: 'Element has no title attribute or the title attribute is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 6 ], html: '<img src="/assets/demo-sites/mars/images/mars-spaceman.jpg" class="" width="210" height="120">', impact: 'critical', none: [ [length]: 0 ], target: [ '#vertical-carousel > .carousel > ul > li:nth-of-type(15) > a > img', [length]: 1 ] }, [length]: 4 ], tags: [ 'cat.text-alternatives', 'wcag2a', 'wcag111', 'section508', 'section508.22.a', [length]: 5 ] }, { description: 'Ensures every form element has a label', help: 'Form elements must have labels', helpUrl: 'https://dequeuniversity.com/rules/axe/2.2/label?application=webdriverjs', id: 'label', impact: 'critical', nodes: [ { all: [ [length]: 0 ], any: [ { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'implicit-label', impact: 'critical', message: 'Form element does not have an implicit (wrapped) <label>', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'explicit-label', impact: 'critical', message: 'Form element does not have an explicit <label>', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'non-empty-title', impact: 'critical', message: 'Element has no title attribute or the title attribute is empty', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<input type="text" class="search" name="query" placeholder="search">', impact: 'critical', none: [ [length]: 0 ], target: [ '.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.js.no-flexbox.flexbox-legacy.canvas.canvastext.webgl.no-touch.geolocation.postmessage.websqldatabase.indexeddb.hashchange.history.draganddrop.websockets.rgba.hsla.multiplebgs.backgroundsize.borderimage.borderradius.boxshadow.textshadow.opacity.cssanimations.csscolumns.cssgradients.cssreflections.csstransforms.csstransforms3d.csstransitions.fontface.generatedcontent.video.audio.localstorage.sessionstorage.webworkers.applicationcache.svg.inlinesvg.smil.svgclippaths.deque-axe-is-ready > .public-en > .loginnow > .container-fluid-full > .container > .span5.left-first.pull-left > div > form > .search', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'implicit-label', impact: 'critical', message: 'Form element does not have an implicit (wrapped) <label>', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'explicit-label', impact: 'critical', message: 'Form element does not have an explicit <label>', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'non-empty-title', impact: 'critical', message: 'Element has no title attribute or the title attribute is empty', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<select name="time0" id="time0" class="select-time" tabindex="4">', impact: 'critical', none: [ [length]: 0 ], target: [ '#time0', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'implicit-label', impact: 'critical', message: 'Form element does not have an implicit (wrapped) <label>', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'explicit-label', impact: 'critical', message: 'Form element does not have an explicit <label>', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'non-empty-title', impact: 'critical', message: 'Element has no title attribute or the title attribute is empty', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<select id="traveler0" class="traveler-type">\n\t\t\t\t\t\t\t\t\t\t<option value="0">Adult (26+)</option>\n\t\t\t\t\t\t\t\t\t\t<option value="1">Youth (12-25)</option>\n\t\t\t\t\t\t\t\t\t\t<option value="2">Child (4-11)</option>\n\t\t\t\t\t\t\t\t\t\t<option value="3">Senior (60+)</option>\n\t\t\t\t\t\t\t\t\t</select>', impact: 'critical', none: [ [length]: 0 ], target: [ '#passenger0 > .wrapper.age-range > .traveler-type', [length]: 1 ] }, [length]: 3 ], tags: [ 'cat.forms', 'wcag2a', 'wcag332', 'wcag131', 'section508', 'section508.22.n', [length]: 6 ] }, { description: 'Ensures links have discernible text', help: 'Links must have discernible text', helpUrl: 'https://dequeuniversity.com/rules/axe/2.2/link-name?application=webdriverjs', id: 'link-name', impact: 'critical', nodes: [ { all: [ [length]: 0 ], any: [ { data: null, id: 'has-visible-text', impact: 'moderate', message: 'Element does not have text that is visible to screen readers', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<a class="link" href="demo/mars/#"><i class="icon-menu-home"></i> </a>', impact: 'critical', none: [ { data: null, id: 'focusable-no-name', impact: 'serious', message: 'Element is in tab order and does not have accessible text', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], target: [ '#main-nav > ul > .first > .link', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'has-visible-text', impact: 'moderate', message: 'Element does not have text that is visible to screen readers', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<a href="/demo/mars/mars2?a=crater_adventure">\n\t\t <img src="/assets/demo-sites/mars/images/mars-spaceman.jpg" class="" width="210" height="120"></a>', impact: 'critical', none: [ { data: null, id: 'focusable-no-name', impact: 'serious', message: 'Element is in tab order and does not have accessible text', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], target: [ '#vertical-carousel > .carousel > ul > li:nth-of-type(1) > a', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'has-visible-text', impact: 'moderate', message: 'Element does not have text that is visible to screen readers', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<a href="/demo/mars/mars2?a=crater_adventure">\n\t\t <img src="/assets/demo-sites/mars/images/mars-spaceman.jpg" class="" width="210" height="120"></a>', impact: 'critical', none: [ { data: null, id: 'focusable-no-name', impact: 'serious', message: 'Element is in tab order and does not have accessible text', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], target: [ '#vertical-carousel > .carousel > ul > li:nth-of-type(8) > a', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'has-visible-text', impact: 'moderate', message: 'Element does not have text that is visible to screen readers', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<a href="/demo/mars/mars2?a=crater_adventure">\n\t\t <img src="/assets/demo-sites/mars/images/mars-spaceman.jpg" class="" width="210" height="120"></a>', impact: 'critical', none: [ { data: null, id: 'focusable-no-name', impact: 'serious', message: 'Element is in tab order and does not have accessible text', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], target: [ '#vertical-carousel > .carousel > ul > li:nth-of-type(15) > a', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'has-visible-text', impact: 'moderate', message: 'Element does not have text that is visible to screen readers', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<a href="/demo/mars/mars2?a="></a>', impact: 'critical', none: [ { data: null, id: 'focusable-no-name', impact: 'serious', message: 'Element is in tab order and does not have accessible text', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], target: [ '#form1 > div:nth-of-type(2) > .interior-container > .re_ajax_p3 > a', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'has-visible-text', impact: 'moderate', message: 'Element does not have text that is visible to screen readers', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<a target="player" data-text="Life was possible on Mars" class="fader first active" href="http://www.youtube.com/embed/OagLGti_hTE?controls=1&showinfo=1&modestbranding=0&wmode=opaque&enablejsapi=1" id="default"></a>', impact: 'critical', none: [ { data: null, id: 'focusable-no-name', impact: 'serious', message: 'Element is in tab order and does not have accessible text', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], target: [ '#video-box > .interior-container > .vid-nav > .fader.first.active', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'has-visible-text', impact: 'moderate', message: 'Element does not have text that is visible to screen readers', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<a target="player" data-text="Why Mars died" class="fader first" href="http://www.youtube.com/embed/oC31pqk9sak?controls=1&showinfo=1&modestbranding=0&wmode=opaque&enablejsapi=1" id="default"></a>', impact: 'critical', none: [ { data: null, id: 'focusable-no-name', impact: 'serious', message: 'Element is in tab order and does not have accessible text', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], target: [ '#video-box > .interior-container > .vid-nav > a:nth-of-type(2)', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'has-visible-text', impact: 'moderate', message: 'Element does not have text that is visible to screen readers', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-label', impact: 'critical', message: 'aria-label attribute does not exist or is empty', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'aria-labelledby', impact: 'critical', message: 'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-presentation', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="presentation"', relatedNodes: [ [length]: 0 ] }, { data: null, id: 'role-none', impact: 'moderate', message: 'Element\'s default semantics were not overridden with role="none"', relatedNodes: [ [length]: 0 ] }, [length]: 5 ], html: '<a target="player" data-text="The world that never was" class="fader first" href="http://www.youtube.com/embed/JgMXPXdqJn8?controls=1&showinfo=1&modestbranding=0&wmode=opaque&enablejsapi=1" id="default"></a>', impact: 'critical', none: [ { data: null, id: 'focusable-no-name', impact: 'serious', message: 'Element is in tab order and does not have accessible text', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], target: [ '#video-box > .interior-container > .vid-nav > a:nth-of-type(3)', [length]: 1 ] }, [length]: 8 ], tags: [ 'cat.name-role-value', 'wcag2a', 'wcag111', 'wcag412', 'section508', 'section508.22.a', [length]: 6 ] }, { description: 'Ensures related <input type="radio"> elements have a group and that the group designation is consistent', help: 'Radio inputs with the same name attribute value must be part of a group', helpUrl: 'https://dequeuniversity.com/rules/axe/2.2/radiogroup?application=webdriverjs', id: 'radiogroup', impact: 'critical', nodes: [ { all: [ [length]: 0 ], any: [ { data: { name: 'widget-type', type: 'radio' }, id: 'group-labelledby', impact: 'critical', message: 'All elements with the name "widget-type" do not reference the same element with aria-labelledby', relatedNodes: [ [length]: 0 ] }, { data: { failureCode: 'no-legend', name: 'widget-type', type: 'radio' }, id: 'fieldset', impact: 'critical', message: 'Fieldset does not have a legend as its first child', relatedNodes: [ { html: '<fieldset>', target: [ '#widget-controls > .interior-container > form > fieldset', [length]: 1 ] }, [length]: 1 ] }, [length]: 2 ], html: '<input type="radio" name="widget-type" id="widget-controls-fares" value="0" checked="checked">', impact: 'critical', none: [ [length]: 0 ], target: [ '#widget-controls-fares', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: { name: 'route-type', type: 'radio' }, id: 'group-labelledby', impact: 'critical', message: 'All elements with the name "route-type" do not reference the same element with aria-labelledby', relatedNodes: [ [length]: 0 ] }, { data: { failureCode: 'no-group', name: 'route-type', type: 'radio' }, id: 'fieldset', impact: 'critical', message: 'Element does not have a containing fieldset or ARIA group', relatedNodes: [ { html: '<input type="radio" name="route-type" id="route-type-round-trip" value="1">', target: [ '#route-type-round-trip', [length]: 1 ] }, { html: '<input type="radio" name="route-type" id="route-type-multi-city" value="2">', target: [ '#route-type-multi-city', [length]: 1 ] }, [length]: 2 ] }, [length]: 2 ], html: '<input type="radio" name="route-type" id="route-type-one-way" value="0" checked="checked">', impact: 'critical', none: [ [length]: 0 ], target: [ '#route-type-one-way', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: { name: 'pass-question-radio', type: 'radio' }, id: 'group-labelledby', impact: 'critical', message: 'All elements with the name "pass-question-radio" do not reference the same element with aria-labelledby', relatedNodes: [ [length]: 0 ] }, { data: { failureCode: 'no-group', name: 'pass-question-radio', type: 'radio' }, id: 'fieldset', impact: 'critical', message: 'Element does not have a containing fieldset or ARIA group', relatedNodes: [ { html: '<input type="radio" name="pass-question-radio" id="pass-question-no" value="1" checked="checked">', target: [ '#pass-question-no', [length]: 1 ] }, [length]: 1 ] }, [length]: 2 ], html: '<input type="radio" name="pass-question-radio" id="pass-question-yes" value="0">', impact: 'critical', none: [ [length]: 0 ], target: [ '#pass-question-yes', [length]: 1 ] }, [length]: 3 ], tags: [ 'cat.forms', 'best-practice', [length]: 2 ] }, { description: 'Ensures tabindex attribute values are not greater than 0', help: 'Elements should not have tabindex greater than zero', helpUrl: 'https://dequeuniversity.com/rules/axe/2.2/tabindex?application=webdriverjs', id: 'tabindex', impact: 'serious', nodes: [ { all: [ [length]: 0 ], any: [ { data: null, id: 'tabindex', impact: 'serious', message: 'Element has a tabindex greater than 0', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], html: '<input type="text" value="" class="city-input ac_input ui-autocomplete-input" autocomplete="off" id="from0" name="from0" tabindex="1" role="textbox" aria-autocomplete="list" aria-haspopup="true">', impact: 'serious', none: [ [length]: 0 ], target: [ '#from0', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'tabindex', impact: 'serious', message: 'Element has a tabindex greater than 0', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], html: '<input type="text" value="" class="city-input ac_input ui-autocomplete-input" autocomplete="off" id="to0" name="to0" tabindex="1" role="textbox" aria-autocomplete="list" aria-haspopup="true">', impact: 'serious', none: [ [length]: 0 ], target: [ '#to0', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'tabindex', impact: 'serious', message: 'Element has a tabindex greater than 0', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], html: '<input size="10" id="deptDate0" name="deptDate0" placeholder="mm/dd/yyyy" value="" tabindex="3" class="hasDatepicker input-dept">', impact: 'serious', none: [ [length]: 0 ], target: [ '#deptDate0', [length]: 1 ] }, { all: [ [length]: 0 ], any: [ { data: null, id: 'tabindex', impact: 'serious', message: 'Element has a tabindex greater than 0', relatedNodes: [ [length]: 0 ] }, [length]: 1 ], html: '<select name="time0" id="time0" class="select-time" tabindex="4">', impact: 'serious', none: [ [length]: 0 ], target: [ '#time0', [length]: 1 ] }, [length]: 4 ], tags: [ 'cat.keyboard', 'best-practice', [length]: 2 ] }, [length]: 10 ]

Continuous Integration

Continuous integration diagram
Source: Carnegie Mellon

Common CI Solutions


  • Circle CI
  • Travis CI
  • Jenkins

Hosted CI in a Nutshell ๐ŸŒฐ

  • Write code & tests
  • Add integration to Git repo
  • Enable CI builds for the project
  • Push code

axe-core on Github: Circle CI checks along with license/CLA and security/snyk

Circle CI Configuration File

https://circleci.com/docs/1.0/configuration/

circle.yml (overrides defaults)

  machine:
    node:
      version: v6.1.0

  deployment:
    production:
      branch: production
      commands:
        - ./deploy_prod.sh
						
travis.yml (overrides defaults)

  language: node_js
    node_js:
      - 6.1.0
  deploy:
    provider: script
    script: scripts/deploy.sh
    on:
      branch: production
						

How does CI help for a11y?

  • Test more platforms
  • Prevent regressions
  • Encourage test coverage ๐Ÿ‘

CI Testing Example

axe-jasmine-unit repository on Github
axe-jasmine-unit package.json file in Sublime Text
axe-jasmine-unit Grunt file in Sublime Text
axe-jasmine-unit test in Sublime Text
Adding Circle from list of services to axe-jasmine-unit on Github
Adding Circle integration to axe-jasmine-unit on Github
Cirlce integration installed for axe-jasmine-unit on Github
Projects list on Circle including axe-jasmine-unit
axe-jasmine-unit successful build on Circle CI
axe-core builds in Circle CI

CI Gotchas

Leslie Knope getting got

If you see this:

Check the Node.js version


Running "parallel:browser-test" (parallel) task

    >> Loading "test-webdriver.js" tasks...ERROR
    >> >> SyntaxError: Unexpected token ...
    >> Warning: Task "test-webdriver:firefox" not found. Use --force to continue.
    >> 
    >> Aborted due to warnings.
    Warning: Task "parallel:browser-test" failed. Use --force to continue.

Aborted due to warnings.

grunt test returned exit code 3
					
Breaking on spread syntax

If you see this:

Try to reproduce locally, debug with logging


  color-contrast sticky header test violations

TypeError: undefined is not an object (evaluating 'datum.reason')

  0 passing (107ms)
  1 failing

  1) color-contrast sticky header test violations should find none:
     Uncaught 
...

>> 1/90 tests failed (3.39s)
Warning: Task "mocha:integration" failed. Use --force to continue.

Aborted due to warnings.
					

CI debugging tips

Enable logging
  mocha: {
    test: {
      options: {
        logErrors: true,
      }
    }
  }
 					
Wrap failing code in a try/catch
  try {
    // Your new failing code here
  }
  catch (e) {
    console.log(e)
  }

Tracking who broke the build

https://twitter.com/zqxwzq/status/868039653697482753
A TV monitor with broken build bandits wanted for bounty

Get tests passing

axe-core shadow dom tests passing on CircleCI

Yesss

Kip from Napoleon Dynamite saying YES

Recap

  • We can automate 30-50% of a11y issues.
  • Write a mix of unit & integration tests.
  • Expand platform coverage with CI.
  • Beware of gotchas with chasing down errors!
  • Donโ€™t forget usability and manual a11y testing.

Thanks!