Skip to content

Commit

Permalink
Add a11y test helper for mocha unit tests
Browse files Browse the repository at this point in the history
fixes CNVS-24009

This adds a wrapper for the aXe testing library that we can use
in our mocha unit tests.

When adding a new component the a11y test will be automatically included
by the generator script.

The wrapper adds the capability to ignore specific rules (see MetricListItem.test.js
for an example)

Note that because the aXe test method is async the stack trace when there is a
failure isn't very helpful. You can see what test failed in the report though.
This may be something we can improve in the future.

Additional changes:
- Cleaned up dependencies and replaced lodash.uniqueid with the shortid module.
- Refactored some of the example code into a ComponentExample component
- updated Image, RangeInput and Link components based on the a11y test results
- added json-loader to load .babelrc file to render examples

Test plan:
- `npm start` and browser to http://localhost:8080; examples should render
- `npm test` all test cases should pass
- Modify a component so that it doesn't meet a11y standards (e.g. remove the
label element from RangeInput); Run `npm test`. The RangeInput a11y standards
test should fail.
- Modify the MetricListItem.test.js file and remove the ignores; The test should
fail when `npm test` is run.

Change-Id: I0743d3975f16fd86fde7a77754b9c7b303207201
Reviewed-on: https://gerrit.instructure.com/69959
Tested-by: Jenkins
Reviewed-by: Ryan Shaw <[email protected]>
QA-Review: Aaron Cannon <[email protected]>
Product-Review: Jennifer Stern <[email protected]>
  • Loading branch information
junyper committed Jan 14, 2016
1 parent 862eea0 commit db21dda
Show file tree
Hide file tree
Showing 31 changed files with 516 additions and 341 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
$sg-checkerboard-color: #EEE;
$sg-checkerboard-gradient: 45deg, $sg-checkerboard-color 25%, transparent 25%, transparent 75%, $sg-checkerboard-color 75%, $sg-checkerboard-color;
html {
height: 100%;
}
body {
margin: 0;
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
background-color: white;
background-image: linear-gradient($sg-checkerboard-gradient), linear-gradient($sg-checkerboard-gradient);
background-size: 16px 16px;
background-position: 0 0, 8px 8px;
height: 100%;

:global {
html {
height: 100%;
}
body {
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
margin: 0;
height: 100%;
background-color: white;
background-image: linear-gradient($sg-checkerboard-gradient), linear-gradient($sg-checkerboard-gradient);
background-size: 16px 16px;
background-position: 0 0, 8px 8px;
}
}

.root {
Expand All @@ -19,18 +22,25 @@ body {
overflow: hidden;
}

.error {
font-family: Menlo, Consolas, Monaco, 'Andale Mono', monospace;
color: white;
background: #c00;
padding: 1.5em;
.errorBg {
margin: 0;
line-height: 1.2;
font-size: 0.75em;
position: absolute;
top: 0; left: 0;
overflow: auto;
display: block;
width: 100vw; height: 100vh;
box-sizing: border-box;
z-index: -1;
}

.error,
.errorBg {
background: #c00;
}

.error {
color: white;
font-family: Menlo, Consolas, Monaco, 'Andale Mono', monospace;
line-height: 1.5;
font-size: 0.75em;
}
151 changes: 151 additions & 0 deletions docs/app/lib/components/ComponentExample/ComponentExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React, {Component, PropTypes} from 'react'
import ReactDOM from 'react-dom'
import { transform } from 'babel-standalone'
import WindowMessageListener from '../WindowMessageListener'
import debounce from 'lodash/function/debounce'
import defer from 'lodash/function/defer'
import window from 'global/window'

import styles from './ComponentExample.css'

import docs from '../../util/load-docs'

export default class ComponentExample extends Component {
static propTypes = {
code: PropTypes.string,
children: PropTypes.node
};

constructor () {
super()
this.state = {
error: null
}
}

componentDidMount () {
docs.globalize()

if (this.props.code) {
this.executeCode(this.props.code)
}
if (window.parent) {
WindowMessageListener.postMessage(window.parent, {
isMounted: true
})
this._handleResize = debounce(this.notifyParent, 200)
window.addEventListener('resize', this._handleResize)
}
}

componentWillUnmount () {
if (this._handleResize) {
window.removeEventListener('resize', this._handleResize)
}
}

handleMessage = (message) => {
if (message && typeof message.code === 'string') {
this.executeCode(message.code)
}
};

notifyParent = () => {
if (window.parent) {
const node = ReactDOM.findDOMNode(this)
window.setTimeout(function () {
WindowMessageListener.postMessage(window.parent, {
contentHeight: node.offsetHeight
})
}, 0)
}
};

compileCode (code) {
return transform(code, require('json!babel-config')).code
}

evalCode (code) {
/* eslint-disable no-eval */
return eval(code)
/* eslint-disable no-eval */
}

executeCode (code) {
const mountNode = this.refs.mount

ReactDOM.unmountComponentAtNode(mountNode)

this.setState({
error: null
})

if (!code) {
return
}

try {
const compiledCode = this.compileCode(code)
const component = this.evalCode(compiledCode)

ReactDOM.render(component, mountNode, () => {
defer(this.notifyParent)
})
} catch (err) {
this.handleError(err)
}
}

handleError (err) {
ReactDOM.unmountComponentAtNode(this.refs.mount)
this.setState({
error: err.toString()
})
defer(this.notifyParent)
}

renderError () {
const { error } = this.state
if (error) {
return (
<pre className={styles.error}>{error}</pre>
)
} else {
return null
}
}

renderErrorBg () {
const { error } = this.state
if (error) {
return (
<div className={styles.errorBg}></div>
)
} else {
return null
}
}

renderExample () {
const { error } = this.state
if (error) {
null
} else {
return (
<div ref="mount" className={styles.example}>
{this.props.children}
</div>
)
}
}

render () {
return (
<WindowMessageListener onReceiveMessage={this.handleMessage} className={styles.root}>
{ this.renderErrorBg() }
{ this.renderExample() }
{ this.renderError() }
</WindowMessageListener>
)
}
}
1 change: 1 addition & 0 deletions docs/app/lib/components/ComponentExample/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ComponentExample'
16 changes: 8 additions & 8 deletions docs/app/lib/components/ComponentPreview/ComponentPreview.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Based on https://github.com/joelburget/react-live-editor/blob/master/live-compile.jsx
import React, { Component, PropTypes } from 'react'
import MessageListener from '../MessageListener'
import _ from 'lodash'
import WindowMessageListener from '../WindowMessageListener'
import shortid from 'shortid'
import classnames from 'classnames'

import styles from './ComponentPreview.css'
Expand All @@ -18,7 +18,7 @@ export default class ComponentPreview extends Component {
frameIsLoaded: false,
isFullScreen: false
}
this.frameName = _.uniqueId('ComponentPreviewExample_')
this.frameName = 'ComponentPreviewExample_' + shortid.generate()
}

componentDidMount () {
Expand Down Expand Up @@ -50,7 +50,7 @@ export default class ComponentPreview extends Component {

renderPreview () {
if (this.state.frameIsLoaded) {
MessageListener.postMessage(this.refs.frame.contentWindow, {
WindowMessageListener.postMessage(this.refs.frame.contentWindow, {
code: this.props.code
})
} else {
Expand All @@ -65,7 +65,7 @@ export default class ComponentPreview extends Component {
}
// TODO: use a modal here
return (
<MessageListener
<WindowMessageListener
sourceName={this.frameName}
onReceiveMessage={this.handleMessage.bind(this)}
className={classnames(classes)}>
Expand All @@ -76,8 +76,8 @@ export default class ComponentPreview extends Component {
className={styles.frame}
name={this.frameName}
title={this.props.name + ' Example'}
src="example.html"></iframe>
</MessageListener>
src={'example.html'}></iframe>
</WindowMessageListener>
)
}
}
} // 'examples/components/' + this.props.name + '.html'
1 change: 0 additions & 1 deletion docs/app/lib/components/ComponentProps/ComponentProps.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
}

.table code {
font-family: Menlo, Consolas, Monaco, 'Andale Mono', monospace;
background: transparent;
}

Expand Down
1 change: 0 additions & 1 deletion docs/app/lib/components/MessageListener/index.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {PropTypes, Component} from 'react'
import window from 'global/window'

const origin = (function () {
const { location } = window
Expand All @@ -15,7 +16,7 @@ const origin = (function () {
}
})()

export default class MessageListener extends Component {
export default class WindowMessageListener extends Component {
static propTypes = {
sourceName: PropTypes.string,
onReceiveMessage: PropTypes.func,
Expand Down
1 change: 1 addition & 0 deletions docs/app/lib/components/WindowMessageListener/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './WindowMessageListener'
Loading

0 comments on commit db21dda

Please sign in to comment.