├── .gitignore ├── .npmignore ├── .babelrc ├── src ├── pending.js ├── index.js ├── cleanDom.js ├── setupSandbox.js ├── describeSome.js ├── keyboard.js ├── tooltip.js ├── dialog.js ├── simpleCheckbox.js ├── progressBar.js ├── slider.js ├── tabPanel.js ├── radioButton.js ├── tristateCheckbox.js └── accordion.js ├── resources ├── 2014-11-05-Test_DP_ARIA_grille_de_saisie.ods └── 2016-11-23-Test_DP_ARIA_grille_de_saisie-V2.1.ods ├── template ├── test │ └── index.js ├── karma.conf.js └── package.json ├── test ├── simpleCheckbox.js ├── tooltip.js ├── tristateCheckbox.js ├── radioButton.js ├── progressBar.js ├── dialog.js ├── accordion.js ├── tabPanel.js ├── slider.js └── lib │ ├── simpleCheckbox.js │ ├── tristateCheckbox.js │ └── radiobutton.js ├── karma.conf.js ├── LICENSE.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /template/node_modules 3 | /lib 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /karma.conf.js 2 | /resources 3 | /src 4 | /template 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/pending.js: -------------------------------------------------------------------------------- 1 | export default (test, reason) => { 2 | if (reason) { 3 | test._runnable.title += `\n\t ${reason}`; 4 | } 5 | return test.skip(); 6 | }; 7 | -------------------------------------------------------------------------------- /resources/2014-11-05-Test_DP_ARIA_grille_de_saisie.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DISIC/rgaa-test-suite/HEAD/resources/2014-11-05-Test_DP_ARIA_grille_de_saisie.ods -------------------------------------------------------------------------------- /resources/2016-11-23-Test_DP_ARIA_grille_de_saisie-V2.1.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DISIC/rgaa-test-suite/HEAD/resources/2016-11-23-Test_DP_ARIA_grille_de_saisie-V2.1.ods -------------------------------------------------------------------------------- /template/test/index.js: -------------------------------------------------------------------------------- 1 | var suite = require('rgaa-test-suite'); 2 | 3 | 4 | 5 | /** 6 | * 7 | */ 8 | describe('Mon composant', function() { 9 | // ... 10 | }); 11 | -------------------------------------------------------------------------------- /test/simpleCheckbox.js: -------------------------------------------------------------------------------- 1 | import SimpleCheckbox from './lib/simpleCheckbox'; 2 | import {simpleCheckbox} from '../src'; 3 | 4 | 5 | 6 | /** 7 | * 8 | */ 9 | describe( 10 | 'WAI-ARIA Simple CheckBox example', 11 | simpleCheckbox(({checked}) => { 12 | return SimpleCheckbox(checked); 13 | }) 14 | ); 15 | 16 | -------------------------------------------------------------------------------- /test/tooltip.js: -------------------------------------------------------------------------------- 1 | import {tooltip} from '../src'; 2 | 3 | 4 | 5 | /** 6 | * 7 | */ 8 | describe( 9 | 'React Bootstrap Tooltip', 10 | tooltip((options) => { 11 | $(document).tooltip(); 12 | 13 | const node = document.createElement('button'); 14 | node.title = options.text; 15 | 16 | return node; 17 | } 18 | )); 19 | -------------------------------------------------------------------------------- /test/tristateCheckbox.js: -------------------------------------------------------------------------------- 1 | import TristateCheckbox from './lib/tristateCheckbox'; 2 | import {tristateCheckbox} from '../src'; 3 | 4 | 5 | /** 6 | * 7 | */ 8 | describe( 9 | 'WAI-ARIA Tristate CheckBox example', 10 | tristateCheckbox(({title, state, items}) => { 11 | return TristateCheckbox(title, state, items); 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /test/radioButton.js: -------------------------------------------------------------------------------- 1 | import RadioButton from './lib/radiobutton'; 2 | import {radioButton, createWrapper} from '../src'; 3 | 4 | 5 | 6 | /** 7 | * 8 | */ 9 | describe( 10 | 'WAI-ARIA RadioButton example', 11 | radioButton(({id, label, items}) => { 12 | return RadioButton({ 13 | id, 14 | text: label 15 | }, items); 16 | }) 17 | ); 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | export {default as progressBar} from './progressBar'; 5 | export {default as tabPanel} from './tabPanel'; 6 | export {default as radioButton} from './radioButton'; 7 | export {default as slider} from './slider'; 8 | export {default as tooltip} from './tooltip'; 9 | export {default as dialog} from './dialog'; 10 | export {default as simpleCheckbox} from './simpleCheckbox'; 11 | export {default as tristateCheckbox} from './tristateCheckbox'; 12 | export {default as accordion} from './accordion'; 13 | -------------------------------------------------------------------------------- /template/karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | module.exports = function(config) { 5 | config.set({ 6 | files: [ 7 | 'test/*.js' 8 | ], 9 | frameworks: [ 10 | 'mocha' 11 | ], 12 | reporters: [ 13 | 'mocha' 14 | ], 15 | mochaReporter: { 16 | colors: { 17 | info: 'yellow' 18 | } 19 | }, 20 | browsers: [ 21 | 'Chrome', 22 | 'Firefox' 23 | ], 24 | preprocessors: { 25 | 'test/*.js': ['webpack'] 26 | }, 27 | webpack: { 28 | devtool: 'inline-source-map' 29 | } 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgaa-test-suite-template", 3 | "private": true, 4 | "scripts": { 5 | "start": "npm run test -- --watch", 6 | "test": "karma start" 7 | }, 8 | "devDependencies": { 9 | "rgaa-test-suite": "^0.1.0", 10 | "karma": "^1.3.0", 11 | "karma-chrome-launcher": "^2.0.0", 12 | "karma-firefox-launcher": "^1.0.0", 13 | "karma-mocha": "^1.3.0", 14 | "karma-mocha-reporter": "^2.2.1", 15 | "karma-webpack": "^1.7.0", 16 | "mocha": "^3.1.2", 17 | "webpack": "^1.13.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cleanDom.js: -------------------------------------------------------------------------------- 1 | import {toLower} from 'lodash'; 2 | 3 | 4 | 5 | /** 6 | * Removes every child of body that is not a . 7 | */ 8 | export default () => { 9 | if (!document.body.hasChildNodes()) { 10 | return; 11 | } 12 | 13 | const nodes = document.body.childNodes; 14 | const notScripts = []; 15 | 16 | for (let i = 0; i < nodes.length; i++) { 17 | if (toLower(nodes[i].tagName) !== 'script') { 18 | notScripts.push(nodes[i]); 19 | } 20 | } 21 | 22 | notScripts.forEach((node) => 23 | document.body.removeChild(node) 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /test/progressBar.js: -------------------------------------------------------------------------------- 1 | import {render, createElement} from 'react'; 2 | import RgaaReactBootstrap from 'rgaa_react-bootstrap'; 3 | import {progressBar, createWrapper} from '../src'; 4 | 5 | 6 | 7 | /** 8 | * 9 | */ 10 | describe( 11 | 'React Bootstrap ProgressBar', 12 | progressBar(({min, max, value}) => { 13 | const props = { 14 | min, 15 | max, 16 | now: value, 17 | label: '%(percent)s%' 18 | }; 19 | 20 | const node = document.createElement('div'); 21 | 22 | render( 23 | createElement(RgaaReactBootstrap.ProgressBar, props), 24 | node 25 | ); 26 | 27 | return node; 28 | } 29 | )); 30 | -------------------------------------------------------------------------------- /test/dialog.js: -------------------------------------------------------------------------------- 1 | import {render, createElement} from 'react'; 2 | import RgaaReactBootstrap from 'rgaa_react-bootstrap'; 3 | import {dialog as dialogTest, createWrapper} from '../src'; 4 | 5 | 6 | 7 | /** 8 | * 9 | */ 10 | describe( 11 | 'jQuery Dialog test', 12 | dialogTest(({title, content}) => { 13 | const $dialog = $(` 14 |
This the content of my dialog.
22 | 23 | 24 | ` 25 | }; 26 | 27 | const result = factory(props); 28 | 29 | this.open = result.open; 30 | this.close = result.close; 31 | 32 | this.beforeOpenFocusedElement = document.activeElement; 33 | this.open(); 34 | 35 | this.dialog = document.body.querySelector('[role="dialog"]'); 36 | this.alertDialog = document.body.querySelector('[role="alertdialog"]'); 37 | this.container = this.dialog || this.alertDialog; 38 | this.focusables = tabbable(this.container); 39 | }); 40 | 41 | afterEach(function() { 42 | cleanDom(); 43 | }); 44 | 45 | describe('Critère 1 : L\'implémentation ARIA est-elle conforme ?', function() { 46 | describeSome('Test 1.1: Le composant respecte-t-il une de ces conditions ?', function() { 47 | it('Le composant possède un role="dialog".', function() { 48 | expect(this.dialog).to.exist; 49 | }); 50 | 51 | it('Le composant possède un role="alertdialog".', function() { 52 | expect(this.alertDialog).to.exist; 53 | }); 54 | }); 55 | 56 | describeSome('Test 1.2 : Le composant respecte-t-il une de ces conditions ?', function() { 57 | it('Le composant possède une propriété aria-label="[Titre de la fenêtre]".', function() { 58 | expect(this.container.getAttribute('aria-label')).to.exist; 59 | }); 60 | 61 | it('Le composant possède une propriété aria-labelledby="[ID_titre]" référençant un passage de texte faisant office de titre.', function() { 62 | expect(this.container.getAttribute('aria-labelledby')).to.exist; 63 | }); 64 | }); 65 | 66 | describe('Test 1.3 : À l\'ouverture de la fenêtre, le focus est donné sur le premier élément focusable ?', function() { 67 | it('Cette condition est respectée.', function() { 68 | expect(document.activeElement) 69 | .to.equal(this.focusables[0]); 70 | }); 71 | }); 72 | 73 | describe('Test 1.4 : À la fermeture de la fenêtre, le focus est donné sur l\'élément ayant permis d\'ouvrir la fenêtre, cette règle est-elle respectée ?', function() { 74 | it('Cette condition est respectée.', function() { 75 | this.close(); 76 | expect(document.activeElement) 77 | .to.equal(this.beforeOpenFocusedElement); 78 | }); 79 | }); 80 | }); 81 | 82 | describe('Critère 2 : Les interactions au clavier sont-elles conformes ?', function() { 83 | describe('Test 2.1 : L\'utilisation de la touche [TAB] respecte-elle ces conditions ?', function() { 84 | it('La tabulation permet d\'atteindre l\'élément suivant et précédent du composant.', function() { 85 | effroi.keyboard.focus(this.focusables[0]); 86 | 87 | effroi.keyboard.tab(); 88 | expect(document.activeElement) 89 | .to.equal(this.focusables[1]); 90 | 91 | effroi.keyboard.shiftTab(); 92 | expect(document.activeElement) 93 | .to.equal(this.focusables[0]); 94 | }); 95 | 96 | it('La tabulation est restreinte aux éléments focusables du composant.', function() { 97 | effroi.keyboard.focus(this.focusables[0]); 98 | 99 | effroi.keyboard.shiftTab(); 100 | expect(document.activeElement) 101 | .to.equal(this.focusables[this.focusables.length - 1]); 102 | 103 | effroi.keyboard.tab(); 104 | expect(document.activeElement) 105 | .to.equal(this.focusables[0]); 106 | }); 107 | }); 108 | 109 | describe('Test 2.2 : L\'utilisation de la touche [ESC] permet-t-elle de fermer la fenêtre ?', function() { 110 | it('Cette condition est respectée.', function() { 111 | effroi.keyboard.hit('Esc'); 112 | 113 | expect(this.cleanContainer) 114 | .to.not.satisfy(isDialogOpened); 115 | }); 116 | }); 117 | }); 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /src/simpleCheckbox.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import effroi from 'effroi'; 3 | import pending from './pending'; 4 | import setupSandbox from './setupSandbox'; 5 | 6 | 7 | 8 | /** 9 | * 10 | */ 11 | const defaultMakeLabel = ({value}) => `${value}%`; 12 | 13 | 14 | 15 | /** 16 | * Returns a function that tests a simple checkbox component. 17 | * 18 | * @param function factory A factory function that takes 19 | * a map of options and returns a DOM node containing a 20 | * checkbox. 21 | */ 22 | export default function createSimpleCheckboxTest(factory, makeLabel = defaultMakeLabel) { 23 | return function testSimpleCheckbox() { 24 | describe('Critère 1 : L\'implémentation ARIA est-elle conforme ?', function() { 25 | describe('Test 1.1 : Le composant respecte-t-il ces conditions ?', function() { 26 | it('Le composant possède un role="checkbox".', function() { 27 | const props = { 28 | checked: true 29 | }; 30 | 31 | const sandbox = setupSandbox(factory(props)); 32 | const checkbox = sandbox.querySelector('[role="checkbox"]'); 33 | 34 | expect(checkbox).to.be.ok; 35 | }); 36 | 37 | it('Le composant possède la propriété aria-checked="true" lorsqu\'il est sélectionné.', function() { 38 | const props = { 39 | checked: true 40 | }; 41 | 42 | const sandbox = setupSandbox(factory(props)); 43 | const checkbox = sandbox.querySelector('[role="checkbox"]'); 44 | 45 | expect(checkbox.getAttribute('aria-checked')).to.equal('true'); 46 | }); 47 | 48 | it('Le composant possède la propriété aria-checked="false" lorsqu\'il n\'est pas sélectionné.', function() { 49 | const props = { 50 | checked: false 51 | }; 52 | 53 | const sandbox = setupSandbox(factory(props)); 54 | const checkbox = sandbox.querySelector('[role="checkbox"]'); 55 | 56 | expect(checkbox.getAttribute('aria-checked')).to.equal('false'); 57 | }); 58 | 59 | it('Le composant possède l\'attribut tabindex="0", si nécessaire.', function() { 60 | const props = { 61 | checked: true 62 | }; 63 | 64 | const sandbox = setupSandbox(factory(props)); 65 | const checkbox = sandbox.querySelector('[role="checkbox"]'); 66 | 67 | expect(checkbox.getAttribute('tabindex')).to.equal('0'); 68 | }); 69 | }) 70 | 71 | describe('Test 1.2 : Chaque état de composant symbolisé par une image respecte-t-il une de ces conditions ?', function() { 72 | before(function() { 73 | const props = { 74 | checked: true 75 | }; 76 | 77 | const sandbox = setupSandbox(factory(props)); 78 | this.images = sandbox.querySelectorAll('img'); 79 | this.presentationImages = sandbox.querySelectorAll('img[role="presentation"]'); 80 | }); 81 | 82 | it('L\'image possède le role="presentation".', function() { 83 | if (!this.images.length) { 84 | return pending(this, 'Aucune image trouvée.'); 85 | } 86 | expect(this.presentationImages.length).to.equal(1); 87 | }); 88 | 89 | it('L\'image est une image insérée via CSS.', function() { 90 | return pending(this, 'Non testable automatiquement.'); 91 | }); 92 | }) 93 | 94 | }) 95 | describe('Critère 2 : Les interactions au clavier sont-elles conformes ?', function() { 96 | describe('Test 2.1 : Pour chaque composant, l\'utilisation de la touche [Espace] respecte-t-elle ces conditions ?', function() { 97 | it('[Espace] permet de cocher le composant s\'il n\'est pas coché.', function() { 98 | const props = { 99 | checked: false 100 | }; 101 | 102 | const sandbox = setupSandbox(factory(props)); 103 | const checkbox = sandbox.querySelector('[role="checkbox"]'); 104 | 105 | // Simulates keyboard space event 106 | effroi.keyboard.focus(checkbox); 107 | effroi.keyboard.hit('Spacebar'); 108 | 109 | expect(checkbox.getAttribute('aria-checked')).to.equal('true'); 110 | }); 111 | 112 | it('[Espace] permet de décocher le composant s\'il est coché.', function() { 113 | const props = { 114 | checked: true 115 | }; 116 | 117 | const sandbox = setupSandbox(factory(props)); 118 | const checkbox = sandbox.querySelector('[role="checkbox"]'); 119 | 120 | // Simulates keyboard space event 121 | effroi.keyboard.focus(checkbox); 122 | effroi.keyboard.hit('Spacebar'); 123 | 124 | expect(checkbox.getAttribute('aria-checked')).to.equal('false'); 125 | }); 126 | }); 127 | }); 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /test/lib/tristateCheckbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * create a tristate checkbox 3 | * 4 | * code taken from https://www.w3.org/TR/wai-aria-practices-1.1/examples/checkbox/checkbox-1.html 5 | * 6 | * @param {string} state state of checkbox 7 | */ 8 | export default function tristateCheckbox(title, state, items) { 9 | const container = document.createElement('fieldset'); 10 | const legend = document.createElement('legend'); 11 | legend.innerHTML = title; 12 | 13 | const mainCheckbox = document.createElement('div'); 14 | mainCheckbox.setAttribute('role', 'checkbox'); 15 | mainCheckbox.setAttribute('tabindex', 0); 16 | mainCheckbox.setAttribute('aria-checked', state); 17 | mainCheckbox.setAttribute('class', 'group_checkbox'); 18 | const img = document.createElement('img'); 19 | img.setAttribute('src', ''); 20 | img.setAttribute('role', 'presentation'); 21 | const label = document.createElement('span'); 22 | label.innerHTML = 'Label'; 23 | mainCheckbox.appendChild(img); 24 | mainCheckbox.appendChild(label); 25 | mainCheckbox.addEventListener('keydown', toggleGroupCheckbox); 26 | mainCheckbox.addEventListener('click', toggleGroupCheckbox); 27 | mainCheckbox.addEventListener('focus', focusCheckbox); 28 | mainCheckbox.addEventListener('blur', blurCheckbox); 29 | 30 | container.appendChild(legend); 31 | container.appendChild(mainCheckbox); 32 | 33 | items.forEach((item) => { 34 | const checkboxLabel = document.createElement('label'); 35 | const checkbox = document.createElement('input'); 36 | checkbox.setAttribute('type', 'checkbox'); 37 | checkbox.checked = item.checked; 38 | checkbox.addEventListener('change', updateGroupCheckbox); 39 | checkbox.addEventListener('focus', focusStandardCheckbox); 40 | checkbox.addEventListener('blur', blurStandardCheckbox); 41 | const labelTitle = document.createElement('span'); 42 | labelTitle.innerHTML = item.label; 43 | 44 | checkboxLabel.appendChild(checkbox); 45 | checkboxLabel.appendChild(labelTitle); 46 | container.appendChild(checkboxLabel); 47 | }); 48 | 49 | return container; 50 | } 51 | 52 | 53 | 54 | 55 | /* 56 | * @function toggleGroupCheckBox 57 | * 58 | * @desc Toogles the state of a grouping checkbox 59 | * 60 | * @param {Object} event - Standard W3C event object 61 | * 62 | */ 63 | 64 | function toggleGroupCheckbox(event) { 65 | var node = event.currentTarget 66 | 67 | var image = node.getElementsByTagName('img')[0] 68 | 69 | var state = node.getAttribute('aria-checked').toLowerCase() 70 | 71 | if (event.type === 'click' || 72 | (event.type === 'keydown' && event.keyCode === 32)) { 73 | 74 | if (state === 'false' || state === 'mixed') { 75 | node.setAttribute('aria-checked', 'true') 76 | setCheckboxes(node, true) 77 | } 78 | else { 79 | node.setAttribute('aria-checked', 'false') 80 | setCheckboxes(node, false) 81 | } 82 | 83 | event.preventDefault() 84 | event.stopPropagation() 85 | 86 | } 87 | 88 | } 89 | 90 | /* 91 | * @function setCheckboxes 92 | * 93 | * @desc 94 | * 95 | * @param {Object} node - DOM node of updated checkbox 96 | * @param {Booleam} state - Set value of checkboxes 97 | * 98 | */ 99 | 100 | function setCheckboxes(node, state) { 101 | 102 | var checkboxes = node.parentNode.querySelectorAll('input[type=checkbox]') 103 | 104 | for (var i = 0; i < checkboxes.length; i++) { 105 | checkboxes[i].checked = state; 106 | } 107 | } 108 | 109 | /* 110 | * @function updateGroupCheckbox 111 | * 112 | * @desc 113 | * 114 | * @param {Object} node - DOM node of updated group checkbox 115 | */ 116 | function updateGroupCheckbox(event) { 117 | 118 | var node = event.currentTarget; 119 | 120 | var checkboxes = node.parentNode.parentNode.querySelectorAll('input[type=checkbox]'); 121 | 122 | var state = 'false'; 123 | var count = 0; 124 | 125 | for (var i = 0; i < checkboxes.length; i++) { 126 | if (checkboxes[i].checked) count += 1; 127 | } 128 | 129 | if (count > 0) state = 'mixed'; 130 | if (count === checkboxes.length) state = 'true'; 131 | 132 | var group_checkbox = node.parentNode.parentNode.getElementsByClassName('group_checkbox')[0]; 133 | 134 | group_checkbox.setAttribute('aria-checked', state); 135 | } 136 | /* 137 | * @function focusCheckBox 138 | * 139 | * @desc Adds focus to the class name of the checkbox 140 | * 141 | * @param {Object} event - Standard W3C event object 142 | */ 143 | 144 | function focusCheckbox(event) { 145 | event.currentTarget.className += ' focus'; 146 | } 147 | 148 | /* 149 | * @function blurCheckBox 150 | * 151 | * @desc Adds focus to the class name of the checkbox 152 | * 153 | * @param {Object} event - Standard W3C event object 154 | */ 155 | 156 | function blurCheckbox(event) { 157 | event.currentTarget.className = event.currentTarget.className.replace(' focus',''); 158 | } 159 | 160 | /* 161 | * @function focusStandardCheckBox 162 | * 163 | * @desc Adds focus styling to label element encapsulating standard checkbox 164 | * 165 | * @param {Object} event - Standard W3C event object 166 | */ 167 | 168 | function focusStandardCheckbox(event) { 169 | event.currentTarget.parentNode.className += ' focus'; 170 | } 171 | 172 | /* 173 | * @function blurStandardCheckBox 174 | * 175 | * @desc Adds focus styling to the label element encapsulating standard checkbox 176 | * 177 | * @param {Object} event - Standard W3C event object 178 | */ 179 | 180 | function blurStandardCheckbox(event) { 181 | event.currentTarget.parentNode.className = event.currentTarget.parentNode.className .replace(' focus',''); 182 | } 183 | -------------------------------------------------------------------------------- /src/progressBar.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {random} from 'lodash'; 3 | import pending from './pending'; 4 | import describeSome from './describeSome'; 5 | import setupSandbox from './setupSandbox'; 6 | 7 | 8 | 9 | /** 10 | * 11 | */ 12 | const defaultMakeLabel = ({value}) => `${value}%`; 13 | 14 | 15 | 16 | /** 17 | * Returns a function that tests a progress bar component. 18 | * 19 | * @param function factory A factory function that takes 20 | * a map of options and returns a DOM node containing a 21 | * progress bar. 22 | */ 23 | export default (factory, makeLabel = defaultMakeLabel) => () => 24 | describe('Motif de conception ARIA Progressbar', function() { 25 | describe('Critère 1 : L\'implémentation ARIA est-elle conforme ?', function() { 26 | describe('Test 1.1 : Le composant respecte-t-il ces conditions ?', function() { 27 | before(function() { 28 | this.props = { 29 | min: random(0, 30), 30 | max: random(70, 100), 31 | value: random(30, 70) 32 | }; 33 | 34 | this.sandbox = setupSandbox(factory(this.props)); 35 | this.progressBar = this.sandbox.querySelector('[role="progressbar"]'); 36 | }); 37 | 38 | it('Le composant possède un role="progressbar".', function() { 39 | expect(this.progressBar.getAttribute('role')) 40 | .to.be.ok; 41 | }); 42 | 43 | it('Le composant possède une propriété aria-valuemin="[valeur minimale]".', function() { 44 | expect(Number(this.progressBar.getAttribute('aria-valuemin'))) 45 | .to.equal(this.props.min); 46 | }); 47 | 48 | it('Le composant possède une propriété aria-valuemax="[valeur maximale]".', function() { 49 | expect(Number(this.progressBar.getAttribute('aria-valuemax'))) 50 | .to.equal(this.props.max); 51 | }); 52 | }); 53 | 54 | describeSome('Test 1.2 : Le composant respecte-t-il une de ces conditions ?', function() { 55 | before(function() { 56 | this.props = { 57 | min: random(0, 30), 58 | max: random(70, 100), 59 | value: random(30, 70) 60 | }; 61 | 62 | this.sandbox = setupSandbox(factory(this.props)); 63 | this.progressBar = this.sandbox.querySelector('[role="progressbar"]'); 64 | }); 65 | 66 | it('Le composant est constitué d\'une image qui possède un attribut alt pertinent.', function() { 67 | expect(this.progressBar.getAttribute('title')) 68 | .to.not.be.empty; 69 | }); 70 | 71 | it('Le composant possède une propriété aria-labelledby="[ID_texte]" référençant un passage de texte faisant office de nom.', function() { 72 | expect(this.progressBar.getAttribute('aria-labelledby')) 73 | .to.not.be.empty; 74 | }); 75 | 76 | it('Le composant possède un attribut title faisant office de nom.', function() { 77 | expect(this.progressBar.querySelector('img[alt]')) 78 | .to.not.be.empty; 79 | }); 80 | }); 81 | 82 | describe('Test 1.3 : Chaque progression, dont la valeur courante est connue respecte-t-elle ces conditions ?', function() { 83 | before(function() { 84 | this.props = { 85 | min: random(0, 30), 86 | max: random(70, 100), 87 | value: random(30, 70) 88 | }; 89 | 90 | this.sandbox = setupSandbox(factory(this.props)); 91 | this.progressBar = this.sandbox.querySelector('[role="progressbar"]'); 92 | }); 93 | 94 | it('Le composant possède une propriété aria-valuenow="[valeur courante]".', function() { 95 | expect(Number(this.progressBar.getAttribute('aria-valuenow'))) 96 | .to.equal(this.props.value); 97 | }); 98 | 99 | it('Le composant possède, si nécessaire, une propriété aria-valuetext="[valeur courante + texte]".', function() { 100 | expect(this.progressBar.getAttribute('aria-valuetext')) 101 | .to.equal(makeLabel(this.props)); 102 | }); 103 | 104 | it('La valeur de la propriété aria-valuenow est mise à jour selon la progression.', function() { 105 | return pending(this, ' Test à implémenter.'); 106 | }); 107 | 108 | it('La valeur de la propriété aria-valuetext est mise à jour selon la progression.', function() { 109 | return pending(this, ' Test à implémenter.'); 110 | }); 111 | }); 112 | 113 | describe('Test 1.4 : Chaque progression, dont la valeur courante est inconnue respecte-t-elle ces conditions ?', function() { 114 | before(function() { 115 | this.props = { 116 | min: random(0, 30), 117 | max: random(70, 100), 118 | value: undefined 119 | }; 120 | 121 | this.sandbox = setupSandbox(factory(this.props)); 122 | this.progressBar = this.sandbox.querySelector('[role="progressbar"]'); 123 | }); 124 | 125 | it('Le composant ne possède pas de propriété aria-valuenow.', function() { 126 | expect(this.progressBar.getAttribute('aria-valuenow')) 127 | .to.not.be.ok; 128 | }); 129 | 130 | it('Le composant ne possède pas de propriété aria-valuetext.', function() { 131 | expect(this.progressBar.getAttribute('aria-valuetext')) 132 | .to.not.be.ok; 133 | }); 134 | }); 135 | 136 | describe('Test 1.5 : Chaque progression qui concerne une région spécifique d\'un document respecte-t-elle ces conditions ?', function() { 137 | it('La région concernée possède une propriété aria-describedby="[ID_composant]".', function() { 138 | return pending(this, ' Non testable automatiquement.'); 139 | }); 140 | it('La région concernée possède une propriété aria-busy="true" durant toute la durée de la progression.', function() { 141 | return pending(this, ' Non testable automatiquement.'); 142 | }); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /src/slider.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import effroi from 'effroi'; 3 | import pending from './pending'; 4 | import describeSome from './describeSome'; 5 | import setupSandbox from './setupSandbox'; 6 | 7 | 8 | 9 | /** 10 | * 11 | */ 12 | export default function createSliderText(factory) { 13 | return function testSlider() { 14 | beforeEach(function() { 15 | this.props = { 16 | id: 'clean', 17 | min: -10, 18 | max: 10, 19 | current: 5, 20 | isVertical: false, 21 | withLabel: false 22 | }; 23 | 24 | this.sandbox = setupSandbox(factory(this.props)); 25 | this.slider = this.sandbox.querySelector('[role="slider"]'); 26 | }); 27 | 28 | describe('Critère 1 : L\'implémentation ARIA est-elle conforme ?', function() { 29 | describe('Test 1.1: Le composant respecte-t-il ces conditions ?', function() { 30 | it('Le composant possède un role="slider".', function() { 31 | expect(this.slider).to.exist; 32 | }); 33 | 34 | it('Le composant possède une propriété aria-valuenow="[valeur courante]".', function() { 35 | expect(this.slider.getAttribute('aria-valuenow')).to.equal(`${this.props.current}`); 36 | }); 37 | 38 | it('Le composant possède une propriété aria-valuemax="[valeur maximale]".', function() { 39 | expect(this.slider.getAttribute('aria-valuemax')).to.equal(`${this.props.max}`); 40 | }); 41 | 42 | it('Le composant possède une propriété aria-valuemin="[valeur minimum]".', function() { 43 | expect(this.slider.getAttribute('aria-valuemin')).to.equal(`${this.props.min}`); 44 | }); 45 | 46 | it('Le composant possède, si nécessaire, une propriété aria-valuetext="[valeur courante + text]".', function() { 47 | this.sandbox = setupSandbox( 48 | factory({ 49 | ...this.props, 50 | id: 'withLabel', 51 | withLabel: true 52 | }) 53 | ); 54 | 55 | const slider = this.sandbox.querySelector('[role="slider"]'); 56 | expect(slider.getAttribute('aria-valuetext')).to.be.ok; 57 | }); 58 | 59 | it('Lorsque le composant est déplacé, la propriété aria-valuenow est mise à jour.', function() { 60 | return pending(this, 'À tester manuellement avec la souris.'); 61 | }); 62 | 63 | it('Lorsque le composant est déplacé, la propriété aria-valuetext est mise à jour.', function() { 64 | return pending(this, 'À tester manuellement avec la souris.'); 65 | }); 66 | 67 | it('Lorsque le composant est présenté verticalement, il doit posséder une propriété' 68 | + ' aria-orientation="vertical", cette règle est-elle respectée ?', function() { 69 | this.sandbox = setupSandbox( 70 | factory({ 71 | ...this.props, 72 | id: 'vertical', 73 | isVertical: true 74 | }) 75 | ); 76 | 77 | const slider = this.sandbox.querySelector('[role="slider"]'); 78 | expect(slider.getAttribute('aria-orientation')).to.equal('vertical'); 79 | }); 80 | }); 81 | 82 | describeSome('Test 1.2: Le composant respecte-t-il une de ces conditions ?', function() { 83 | it('Le composant possède une propriété aria-labelledby="[ID_titre]"' 84 | + ' référençant un passage de texte faisant office de titre.', function() { 85 | expect(this.slider.getAttribute('aria-labelledby')).to.be.ok; 86 | }); 87 | 88 | it('Le composant possède un attribut title faisant office de titre.', function() { 89 | expect(this.slider.getAttribute('title')).to.be.ok; 90 | }); 91 | }); 92 | }); 93 | 94 | describe('Critère 2 : Les interactions au clavier sont-elles conformes ?', function() { 95 | describe('Test 2.1 : L\'utilisation de la touche [TAB] respecte-t-elle ces conditions ?', function() { 96 | beforeEach(function() { 97 | this.dummyInput = $('').appendTo('body').get(0); 98 | }); 99 | 100 | it('De l\'extérieur du composant, le focus est donné sur le composant.', function() { 101 | effroi.keyboard.tab(); 102 | expect(document.activeElement).to.equal(this.slider); 103 | }); 104 | 105 | it('Depuis le composant, le focus est donné sur l\'élément focusable suivant à l\'extérieur du composant.', function() { 106 | effroi.keyboard.focus(this.slider); 107 | effroi.keyboard.tab(); 108 | expect(document.activeElement).to.equal(this.dummyInput); 109 | }); 110 | 111 | afterEach(function() { 112 | $('.slider-dummy').remove(); 113 | }); 114 | }); 115 | 116 | describe('Test 2.2 : L\'utilisation des [TOUCHES DE DIRECTION] respecte-t-elle ces conditions ?', function() { 117 | beforeEach(function() { 118 | effroi.keyboard.focus(this.slider); 119 | this.initialValue = this.slider.getAttribute('aria-valuenow'); 120 | }); 121 | 122 | it('[Haut] permet d\'augmenter la valeur du slider.', function() { 123 | effroi.keyboard.hit('Up'); 124 | expect(this.slider.getAttribute('aria-valuenow')).to.not.be.equal(this.initialValue); 125 | }); 126 | 127 | it('[Droit] permet d\'augmenter la valeur du slider.', function() { 128 | effroi.keyboard.hit('Right'); 129 | expect(this.slider.getAttribute('aria-valuenow')).to.not.be.equal(this.initialValue); 130 | }); 131 | 132 | it('[Bas] permet de diminuer la valeur du slider.', function() { 133 | effroi.keyboard.hit('Down'); 134 | expect(this.slider.getAttribute('aria-valuenow')).to.not.be.equal(this.initialValue); 135 | }); 136 | 137 | it('[Gauche] permet de diminuer la valeur du slider.', function() { 138 | effroi.keyboard.hit('Left'); 139 | expect(this.slider.getAttribute('aria-valuenow')).to.not.be.equal(this.initialValue); 140 | }); 141 | }); 142 | }); 143 | }; 144 | } 145 | -------------------------------------------------------------------------------- /test/lib/radiobutton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * create a radiobutton 3 | * 4 | * code taken from https://www.w3.org/TR/wai-aria-practices-1.1/examples/radio/radio.html 5 | * 6 | * @param {Object} label object: {id, text} 7 | * @param {Array} items list of items transformed in radio buttons: [{text, checked}] 8 | */ 9 | export default function radioButton(label, items) { 10 | const container = document.createElement('div'); 11 | container.setAttribute('role', 'radiogroup'); 12 | container.setAttribute('aria-labelledby', label.id); 13 | const title = document.createElement('h1'); 14 | title.id = label.id; 15 | title.innerHTML = label.text; 16 | container.appendChild(title); 17 | const nothingIsChecked = !(items.some(item => item.checked)); 18 | items.forEach((item, index) => { 19 | const button = document.createElement('div'); 20 | button.setAttribute('role', 'radio'); 21 | button.setAttribute('aria-checked', item.checked ? 'true' : 'false'); 22 | button.tabIndex = -1; 23 | if (item.checked) { 24 | button.tabIndex = 0; 25 | } 26 | if (nothingIsChecked && index === 0) { 27 | button.tabIndex = 0; 28 | } 29 | button.innerHTML = item.text; 30 | button.addEventListener('click', clickRadioGroup); 31 | button.addEventListener('keydown', keyDownRadioGroup); 32 | button.addEventListener('focus', focusRadioButton); 33 | button.addEventListener('blur', blurRadioButton); 34 | container.appendChild(button); 35 | }); 36 | return container; 37 | } 38 | 39 | var KEYCODE = { 40 | DOWN: 40, 41 | LEFT: 37, 42 | RIGHT: 39, 43 | SPACE: 32, 44 | UP: 38 45 | } 46 | 47 | /* 48 | * @function firstRadioButton 49 | * 50 | * @desc Returns the first radio button 51 | * 52 | * @param {Object} event = Standard W3C event object 53 | */ 54 | 55 | function firstRadioButton(node) { 56 | 57 | var first = node.parentNode.firstChild; 58 | 59 | while(first) { 60 | if (first.nodeType === Node.ELEMENT_NODE) { 61 | if (first.getAttribute("role") === 'radio') return first; 62 | } 63 | first = first.nextSibling; 64 | } 65 | 66 | return null; 67 | } 68 | 69 | /* 70 | * @function lastRadioButton 71 | * 72 | * @desc Returns the last radio button 73 | * 74 | * @param {Object} event = Standard W3C event object 75 | */ 76 | 77 | function lastRadioButton(node) { 78 | 79 | var last = node.parentNode.lastChild; 80 | 81 | while(last) { 82 | if (last.nodeType === Node.ELEMENT_NODE) { 83 | if (last.getAttribute("role") === 'radio') return last; 84 | } 85 | last = last.previousSibling; 86 | } 87 | 88 | return last; 89 | } 90 | 91 | /* 92 | * @function nextRadioButton 93 | * 94 | * @desc Returns the next radio button 95 | * 96 | * @param {Object} event = Standard W3C event object 97 | */ 98 | 99 | function nextRadioButton(node) { 100 | 101 | var next = node.nextSibling; 102 | 103 | while(next) { 104 | if (next.nodeType === Node.ELEMENT_NODE) { 105 | if (next.getAttribute("role") === 'radio') return next; 106 | } 107 | next = next.nextSibling; 108 | } 109 | 110 | return null; 111 | } 112 | 113 | /* 114 | * @function previousRadioButton 115 | * 116 | * @desc Returns the previous radio button 117 | * 118 | * @param {Object} event = Standard W3C event object 119 | */ 120 | 121 | function previousRadioButton(node) { 122 | 123 | var prev = node.previousSibling; 124 | 125 | while(prev) { 126 | if (prev.nodeType === Node.ELEMENT_NODE) { 127 | if (prev.getAttribute("role") === 'radio') return prev; 128 | } 129 | prev = prev.previousSibling; 130 | } 131 | 132 | return null; 133 | } 134 | 135 | /* 136 | * @function getImage 137 | * 138 | * @desc Gets the image for radio box 139 | * 140 | * @param {Object} event = Standard W3C event object 141 | */ 142 | 143 | function getImage(node) { 144 | 145 | var child = node.firstChild; 146 | 147 | while(child) { 148 | if (child.nodeType === Node.ELEMENT_NODE) { 149 | if (child.tagName === 'IMG') return child; 150 | } 151 | child = child.nextSibling; 152 | } 153 | 154 | return null; 155 | } 156 | 157 | /* 158 | * @function setRadioButton 159 | * 160 | * @desc Toogles the state of a radio button 161 | * 162 | * @param {Object} event - Standard W3C event object 163 | * 164 | */ 165 | 166 | function setRadioButton(node, state) { 167 | var image = getImage(node); 168 | 169 | if (state == 'true') { 170 | node.setAttribute('aria-checked', 'true') 171 | node.tabIndex = 0; 172 | node.focus() 173 | } 174 | else { 175 | node.setAttribute('aria-checked', 'false') 176 | node.tabIndex = -1; 177 | } 178 | } 179 | 180 | /* 181 | * @function clickRadioGroup 182 | * 183 | * @desc 184 | * 185 | * @param {Object} node - DOM node of updated group radio buttons 186 | */ 187 | 188 | function clickRadioGroup(event) { 189 | var type = event.type; 190 | 191 | if (type === 'click') { 192 | // If either enter or space is pressed, execute the funtion 193 | 194 | var node = event.currentTarget; 195 | 196 | var radioButton = firstRadioButton(node); 197 | 198 | while (radioButton) { 199 | setRadioButton(radioButton, "false"); 200 | radioButton = nextRadioButton(radioButton); 201 | } 202 | 203 | setRadioButton(node, "true"); 204 | 205 | event.preventDefault(); 206 | event.stopPropagation(); 207 | } 208 | } 209 | 210 | /* 211 | * @function keyDownRadioGroup 212 | * 213 | * @desc 214 | * 215 | * @param {Object} node - DOM node of updated group radio buttons 216 | */ 217 | 218 | function keyDownRadioGroup(event) { 219 | var type = event.type; 220 | var next = false; 221 | 222 | if(type === "keydown"){ 223 | var node = event.currentTarget; 224 | 225 | switch (event.keyCode) { 226 | case KEYCODE.DOWN: 227 | case KEYCODE.RIGHT: 228 | var next = nextRadioButton(node); 229 | if (!next) next = firstRadioButton(node); //if node is the last node, node cycles to first. 230 | break; 231 | 232 | case KEYCODE.UP: 233 | case KEYCODE.LEFT: 234 | next = previousRadioButton(node); 235 | if (!next) next = lastRadioButton(node); //if node is the last node, node cycles to first. 236 | break; 237 | 238 | case KEYCODE.SPACE: 239 | next = node; 240 | break; 241 | } 242 | 243 | if (next) { 244 | var radioButton = firstRadioButton(node); 245 | 246 | while (radioButton) { 247 | setRadioButton(radioButton, "false"); 248 | radioButton = nextRadioButton(radioButton); 249 | } 250 | 251 | setRadioButton(next, "true"); 252 | 253 | event.preventDefault(); 254 | event.stopPropagation(); 255 | } 256 | } 257 | } 258 | 259 | /* 260 | * @function focusRadioButton 261 | * 262 | * @desc Adds focus styling to label element encapsulating standard radio button 263 | * 264 | * @param {Object} event - Standard W3C event object 265 | */ 266 | 267 | function focusRadioButton(event) { 268 | event.currentTarget.className += ' focus'; 269 | } 270 | 271 | /* 272 | * @function blurRadioButton 273 | * 274 | * @desc Adds focus styling to the label element encapsulating standard radio button 275 | * 276 | * @param {Object} event - Standard W3C event object 277 | */ 278 | 279 | function blurRadioButton(event) { 280 | event.currentTarget.className = event.currentTarget.className.replace(' focus',''); 281 | } 282 | -------------------------------------------------------------------------------- /src/tabPanel.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import effroi from 'effroi'; 3 | import describeSome from './describeSome'; 4 | import setupSandbox from './setupSandbox'; 5 | 6 | 7 | 8 | /** 9 | * 10 | */ 11 | const isPanelActive = (panel) => 12 | panel && $(panel).is(':visible'); 13 | 14 | /** 15 | * 16 | */ 17 | export default (factory) => () => 18 | describe('Motif de conception ARIA Tabpanel', function() { 19 | beforeEach(function() { 20 | this.props = { 21 | panels: [ 22 | { 23 | title: 'Header 1', 24 | content: 'Content 1', 25 | selected: false 26 | }, 27 | { 28 | title: 'Header 2', 29 | content: ` 30 | Content 2 31 | 32 | 33 | `, 34 | selected: true 35 | }, 36 | { 37 | title: 'Header 3', 38 | content: 'Content 3', 39 | selected: false 40 | } 41 | ] 42 | }; 43 | 44 | this.focusableBefore = document.createElement('button'); 45 | this.focusableAfter = document.createElement('button'); 46 | 47 | this.sandbox = setupSandbox( 48 | this.focusableBefore, 49 | factory(this.props), 50 | this.focusableAfter 51 | ); 52 | 53 | this.tabList = this.sandbox.querySelector('[role="tablist"]'); 54 | this.tabs = this.tabList.querySelectorAll('[role="tab"]'); 55 | this.panels = this.sandbox.querySelectorAll('[role="tabpanel"]'); 56 | this.selectedTab = this.tabs[1]; 57 | this.selectedPanel = this.panels[1]; 58 | this.button1 = document.getElementById('button1'); 59 | this.button2 = document.getElementById('button2'); 60 | 61 | effroi.keyboard.focus(this.focusableBefore); 62 | }); 63 | 64 | describe('Critère 1 : L\'implémentation ARIA est-elle conforme ?', function() { 65 | describe('Test 1.1: Le composant englobant les titres des onglets possède-t-il un role="tablist" ?', function() { 66 | it('Cette condition est respectée.', function() { 67 | expect(this.tabs.length) 68 | .to.equal(this.props.panels.length); 69 | }); 70 | }); 71 | 72 | describe('Test 1.2 : Chaque titre d\'onglet respecte-t-il ces conditions ?', function() { 73 | it('Le titre possède un role="tab".', function() { 74 | this.props.panels.forEach((panel, i) => { 75 | expect(this.tabs[i].getAttribute('role')) 76 | .to.equal('tab'); 77 | }); 78 | }); 79 | 80 | it('Le titre possède une propriété aria-selected="true" lorsque le panneau est affiché.', function() { 81 | this.props.panels.forEach((panel, i) => { 82 | if (panel.selected) { 83 | expect(this.tabs[i].getAttribute('aria-selected')) 84 | .to.equal('true'); 85 | } 86 | }); 87 | }); 88 | 89 | it('Le titre possède une propriété aria-selected="false" lorsque le panneau est masqué.', function() { 90 | this.props.panels.forEach((panel, i) => { 91 | if (!panel.selected) { 92 | expect(this.tabs[i].getAttribute('aria-selected')) 93 | .to.equal('false'); 94 | } 95 | }); 96 | }); 97 | }); 98 | 99 | describeSome('Test 1.3 : Chaque couple titre/panneau, respecte-t-il une de ces conditions ?', function() { 100 | it('Le titre possède une propriété aria-controls="[id]" référençant le panneau correspondant.', function() { 101 | this.props.panels.forEach((panel, i) => { 102 | expect(this.tabs[i].getAttribute('aria-controls')) 103 | .to.equal(this.panels[i].id); 104 | }); 105 | }); 106 | 107 | it('Le panneau possède une propriété aria-labelledby="[id]" référençant le titre correspondant.', function() { 108 | this.props.panels.forEach((panel, i) => { 109 | expect(this.panels[i].getAttribute('aria-labelledby')) 110 | .to.equal(this.tabs[i].id); 111 | }); 112 | }); 113 | }); 114 | 115 | describe('Test 1.4 : Chaque panneau possède-t-il un role="tabpanel" ?', function() { 116 | it('Cette condition est respectée.', function() { 117 | expect(this.panels.length) 118 | .to.equal(this.props.panels.length); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('Critère 2 : Les interactions au clavier sont-elles conformes ?', function() { 124 | describe('Test 2.1 : L\'utilisation de la touche [TAB] respecte-t-elle ces conditions ?', function() { 125 | it('De l\'extérieur du composant, le focus est donné sur le titre du panneau actif.', function() { 126 | effroi.keyboard.tab(); 127 | expect(document.activeElement) 128 | .to.equal(this.selectedTab); 129 | }); 130 | 131 | it('La tabulation permet d\'atteindre l\'élément suivant ou précédent du panneau actif.', function() { 132 | effroi.keyboard.focus(this.selectedTab); 133 | 134 | effroi.keyboard.tab(); 135 | expect(document.activeElement) 136 | .to.equal(this.button1); 137 | 138 | effroi.keyboard.tab(); 139 | expect(document.activeElement) 140 | .to.equal(this.button2); 141 | 142 | effroi.keyboard.shiftTab(); 143 | expect(document.activeElement) 144 | .to.equal(this.button1); 145 | }); 146 | 147 | it('Lorsque le dernier élément focusable du composant est atteint, le focus est donné sur l\'élément focusable suivant à l\'extérieur du composant.', function() { 148 | effroi.keyboard.focus(this.button2); 149 | 150 | effroi.keyboard.tab(); 151 | expect(document.activeElement) 152 | .to.equal(this.focusableAfter); 153 | }); 154 | 155 | it('Lorsque le premier élément focusable du composant est atteint, le focus est donné sur l\'élément focusable précédent à l\'extérieur du composant.', function() { 156 | effroi.keyboard.focus(this.selectedTab); 157 | 158 | effroi.keyboard.shiftTab(); 159 | expect(document.activeElement) 160 | .to.equal(this.focusableBefore); 161 | }); 162 | }); 163 | 164 | describe('Test 2.2 : L\'utilisation des [TOUCHES DE DIRECTION] respecte-t-elle ces conditions ?', function() { 165 | it('À partir du titre d\'un onglet, [Haut et Gauche] permet d\'atteindre le titre précédent.', function() { 166 | effroi.keyboard.focus(this.selectedTab); 167 | 168 | effroi.keyboard.hit('Up'); 169 | expect(document.activeElement) 170 | .to.equal(this.tabs[0]); 171 | 172 | effroi.keyboard.hit('Left'); 173 | expect(document.activeElement) 174 | .to.equal(this.tabs[2]); 175 | }); 176 | 177 | it('À partir du titre d\'un onglet, [Bas et Droite] permet d\'atteindre le titre suivant.', function() { 178 | effroi.keyboard.focus(this.selectedTab); 179 | 180 | effroi.keyboard.hit('Down'); 181 | expect(document.activeElement) 182 | .to.equal(this.tabs[2]); 183 | 184 | effroi.keyboard.hit('Right'); 185 | expect(document.activeElement) 186 | .to.equal(this.tabs[0]); 187 | }); 188 | }); 189 | 190 | describe('Test 2.3 : À partir du titre d’un onglet, si le panneau n’est pas activé par défaut, la touche [ESPACE] permet-elle d’activer le panneau ?', function() { 191 | it('Cette condition est respectée.', function() { 192 | effroi.keyboard.focus(this.selectedTab); 193 | effroi.keyboard.hit('Down'); 194 | effroi.keyboard.hit('Spacebar'); 195 | 196 | expect(this.tabs[2]) 197 | .to.satisfy(isPanelActive); 198 | }); 199 | }); 200 | 201 | describe('Test 2.4 : À partir du titre d’un onglet, si le panneau n’est pas activé par défaut, la touche [ENTER] permet-elle d’activer le panneau ?', function() { 202 | it('Cette condition est respectée.', function() { 203 | effroi.keyboard.focus(this.selectedTab); 204 | effroi.keyboard.hit('Down'); 205 | effroi.keyboard.hit('Enter'); 206 | 207 | expect(this.tabs[2]) 208 | .to.satisfy(isPanelActive); 209 | }); 210 | }); 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /src/radioButton.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import effroi from 'effroi'; 3 | import pending from './pending'; 4 | import setupSandbox from './setupSandbox'; 5 | 6 | 7 | 8 | /** 9 | * 10 | */ 11 | export default function createRadioButtonTest(factory) { 12 | return function testRadioButton() { 13 | beforeEach(function() { 14 | this.props = { 15 | id: 'un-groupe-avec-un-bouton-activé', 16 | label: 'Je suis le titre du groupe', 17 | items: [ 18 | {text: 'Je suis le 1er élément'}, 19 | {text: 'Je suis le 2ème élément et je suis coché par défaut', checked: true}, 20 | {text: 'Je suis le 3ème élément'}, 21 | {text: 'Je suis le 4ème élément'} 22 | ] 23 | }; 24 | 25 | // A dummy button that serves as the focus starting point. 26 | const focusableBefore = document.createElement('button'); 27 | 28 | this.sandbox = setupSandbox( 29 | focusableBefore, 30 | factory(this.props) 31 | ); 32 | 33 | this.container = this.sandbox.querySelector('[role="radiogroup"]'); 34 | this.buttons = this.sandbox.querySelectorAll('[role="radio"]'); 35 | this.images = this.sandbox.querySelectorAll('img'); 36 | this.presentationImages = this.sandbox.querySelectorAll('img[role="presentation"]'); 37 | this.checkedButtons = this.sandbox.querySelectorAll('[aria-checked="true"]'); 38 | 39 | effroi.keyboard.focus(focusableBefore); 40 | }); 41 | 42 | describe('Critère 1 : L\'implémentation ARIA est-elle conforme ?', function() { 43 | describe('Test 1.1: Chaque composant principal respecte-t-il ces conditions ?', function() { 44 | it('Le composant possède un role="radiogroup".', function() { 45 | expect(this.container).to.exist; 46 | }); 47 | 48 | it('Au maximum, un bouton radio du composant est sélectionné.', function() { 49 | expect(this.checkedButtons.length).to.be.at.most(1); 50 | }); 51 | }); 52 | 53 | describe('Test 1.2 : Chaque bouton radio respecte-t-il ces conditions ?', function() { 54 | it('L\'élément possède un role="radio".', function() { 55 | expect(this.buttons.length).to.equal(this.props.items.length); 56 | }); 57 | 58 | it('Lorsque l\'élément n\'est pas sélectionné, il possède une propriété aria-checked="false".' 59 | + '\n\t Lorsque l\'élément est sélectionné, il possède une propriété aria-checked="true".', function() { 60 | this.props.items.forEach((item, i) => { 61 | const button = this.buttons.item(i); 62 | 63 | expect(button.getAttribute('aria-checked')) 64 | .to.equal(item.checked ? 'true' : 'false'); 65 | }); 66 | }); 67 | }); 68 | 69 | describe('Test 1.3 : Chaque état d\'un bouton radio, symbolisé par une image, respecte-t-il une de ces conditions ?', function() { 70 | it('L\'image possède un role="presentation".', function() { 71 | if (!this.images.length) { 72 | return pending(this, 'Aucune image trouvée.'); 73 | } 74 | expect(this.presentationImages.length).to.equal(props.items.length); 75 | }); 76 | 77 | it('L\'image est une image insérée via CSS.', function() { 78 | return pending(this, 'Non testable automatiquement.'); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('Critère 2 : Les interactions au clavier sont-elles conformes ?', function() { 84 | beforeEach(function() { 85 | const cleanProps = { 86 | id: 'un-groupe-sans-rien-activé', 87 | label: 'Je suis le titre du groupe sans bouton activé', 88 | items: [ 89 | {text: '1er bouton'}, 90 | {text: '2ème bouton'}, 91 | {text: '3ème bouton'}, 92 | {text: '4ème bouton'} 93 | ] 94 | }; 95 | 96 | this.dummyInput = document.createElement('input'); 97 | this.cleanNode = factory(cleanProps); 98 | this.cleanButtons = this.cleanNode.querySelectorAll('[role="radio"]'); 99 | this.sandbox.appendChild(this.dummyInput); 100 | this.sandbox.appendChild(this.cleanNode); 101 | }); 102 | 103 | describe('Test 2.1 : L\'utilisation de la touche [TAB] respecte-t-elle ces conditions ?', function() { 104 | it('De l\'extérieur du composant, le focus est donné sur l\'élément actif.', function() { 105 | effroi.keyboard.tab(); 106 | expect(document.activeElement).to.equal(this.checkedButtons.item(0)); 107 | }); 108 | 109 | it('Depuis un élément, le focus est donné sur l\'élément focusable suivant à l\'extérieur du composant.', function() { 110 | effroi.keyboard.tab(); 111 | expect(document.activeElement).to.equal(this.checkedButtons.item(0)); 112 | effroi.keyboard.tab(); 113 | expect(document.activeElement).to.equal(this.dummyInput); 114 | }); 115 | 116 | it('De l\'extérieur du composant, si aucun élément n\'est sélectionné, le focus est donné sur le premier élément du composant.', function() { 117 | effroi.keyboard.tab(); 118 | expect(document.activeElement).to.equal(this.checkedButtons.item(0)); 119 | effroi.keyboard.tab(); 120 | expect(document.activeElement).to.equal(this.dummyInput); 121 | effroi.keyboard.tab(); 122 | expect(document.activeElement).to.equal(this.cleanButtons.item(0)); 123 | }); 124 | }); 125 | 126 | describe('Test 2.2 : L\'utilisation des [TOUCHES DE DIRECTION] respecte-t-elle ces conditions ?', function() { 127 | // because of the previous test, the current focus is the first button of the second radiogroup 128 | it('À partir d\'un élément, [Bas] déplace le focus sur l\'élément suivant.', function() { 129 | effroi.keyboard.tab(); 130 | expect(document.activeElement).to.equal(this.checkedButtons.item(0)); 131 | //this.checkedButtons.item(0) === this.buttons.item(1) 132 | effroi.keyboard.hit('Down'); 133 | expect(document.activeElement).to.equal(this.buttons.item(2)); 134 | }); 135 | 136 | it('À partir d\'un élément, [Droit] déplace le focus sur l\'élément suivant.', function() { 137 | effroi.keyboard.tab(); 138 | expect(document.activeElement).to.equal(this.checkedButtons.item(0)); 139 | //this.checkedButtons.item(0) === this.buttons.item(1) 140 | effroi.keyboard.hit('Right'); 141 | expect(document.activeElement).to.equal(this.buttons.item(2)); 142 | }); 143 | 144 | it('À partir d\'un élément, [Haut] déplace le focus sur l\'élément précédent.', function() { 145 | effroi.keyboard.tab(); 146 | expect(document.activeElement).to.equal(this.checkedButtons.item(0)); 147 | //this.checkedButtons.item(0) === this.buttons.item(1) 148 | effroi.keyboard.hit('Up'); 149 | expect(document.activeElement).to.equal(this.buttons.item(0)); 150 | }); 151 | 152 | it('À partir d\'un élément, [Gauche] déplace le focus sur l\'élément précédent.', function() { 153 | effroi.keyboard.tab(); 154 | expect(document.activeElement).to.equal(this.checkedButtons.item(0)); 155 | //this.checkedButtons.item(0) === this.buttons.item(1) 156 | effroi.keyboard.hit('Left'); 157 | expect(document.activeElement).to.equal(this.buttons.item(0)); 158 | }); 159 | 160 | it('À partir du premier élément, [Haut et Gauche] déplace le focus sur le dernier élément.', function() { 161 | effroi.keyboard.tab(); 162 | effroi.keyboard.tab(); 163 | effroi.keyboard.tab(); 164 | expect(document.activeElement).to.equal(this.cleanButtons.item(0)); 165 | 166 | const lastIndex = this.cleanButtons.length - 1; 167 | effroi.keyboard.hit('Up'); 168 | expect(document.activeElement).to.equal(this.cleanButtons.item(lastIndex)); 169 | 170 | effroi.keyboard.hit('Up'); 171 | effroi.keyboard.hit('Up'); 172 | effroi.keyboard.hit('Up'); 173 | 174 | effroi.keyboard.hit('Left'); 175 | expect(document.activeElement).to.equal(this.cleanButtons.item(lastIndex)); 176 | }); 177 | 178 | it('À partir du dernier élément, [Bas et Droit] déplace le focus sur le premier élément.', function() { 179 | effroi.keyboard.tab(); 180 | effroi.keyboard.tab(); 181 | effroi.keyboard.tab(); 182 | expect(document.activeElement).to.equal(this.cleanButtons.item(0)); 183 | 184 | effroi.keyboard.hit('Down'); 185 | effroi.keyboard.hit('Down'); 186 | effroi.keyboard.hit('Down'); 187 | 188 | effroi.keyboard.hit('Down'); 189 | expect(document.activeElement).to.equal(this.cleanButtons.item(0)); 190 | 191 | effroi.keyboard.hit('Down'); 192 | effroi.keyboard.hit('Down'); 193 | effroi.keyboard.hit('Down'); 194 | 195 | effroi.keyboard.hit('Right'); 196 | expect(document.activeElement).to.equal(this.cleanButtons.item(0)); 197 | }); 198 | }); 199 | }); 200 | }; 201 | } 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Suite de tests pour composants RGAA 3 2 | 3 | Une suite de tests [mocha](https://github.com/mochajs/mocha) pour tester la conformité de composants JavaScript avec les *design patterns* ARIA *Accordion*, *Dialog*, *ProgressBar*, *RadioButton*, *Checkbox*, *Slider*, *TabPanel* et *Tooltip*. 4 | 5 | Cet outil est particulièrement utile si vous voulez : 6 | - créer ou maintenir un composant JavaScript suivant un *design pattern*, en vous assurant tout au long du développement qu'il est accessible ; 7 | - utiliser une bibliothèque JavaScript existante et vérifier qu'elle est accessible ; 8 | - utiliser un seul outil pour tester des composants implémentés avec différents *frameworks*. 9 | 10 | Note : le référentiel se base sur [les *design patterns* ARIA](https://www.w3.org/TR/wai-aria-practices) et ajoute des règles sur certains composants. 11 | Vous trouverez la grille de tests ayant servi de référence pour l'implémentation de la librairie dans le dossier `./resources` : [2016-11-23-Test_DP_ARIA_grille_de_saisie-V2.1.ods](./resources/2016-11-23-Test_DP_ARIA_grille_de_saisie-V2.1.ods). 12 | 13 | ## Avertissement 14 | 15 | Cette suite de tests permet d'automatiser la plupart des tests nécessaires à s'assurer de l'accessibilité d'un composant. Cependant, certains tests ne peuvent pas être réalisés automatiquement, et sont donc à vérifier manuellement pour s'assurer d'une conformité totale. Ceci est indiqué dans les résultats de tests (plus de détails dans la section *Usage*). 16 | 17 | ## Installation 18 | 19 | Ces tests sont écrits en JavaScript avec la bibliothèque de test [mocha](https://github.com/mochajs/mocha). 20 | Installez la suite de tests en tant que dépendance de votre projet lors de vos développements via [npm](https://www.npmjs.com/get-npm) : 21 | 22 | ```sh 23 | npm install --save-dev rgaa-test-suite 24 | ``` 25 | 26 | Les tests nécessitent un DOM pour fonctionner. C'est pourquoi un lanceur de tests comme [karma](https://github.com/karma-runner/karma) ou un DOM virtuel comme [jsdom](https://github.com/tmpvar/jsdom) est requis. 27 | 28 | Pour démarrer, vous pouvez vous inspirer du dossier [./template](./template), qui contient une structure de projet pour lancer des tests avec mocha et karma. 29 | Copiez ce dossier où vous le souhaitez, puis lancez `npm install` à la racine. 30 | 31 | ## Usage 32 | 33 | Les composants devant être initialisés d'une certaine manière pour chaque test, un peu de code est nécessaire pour les mettre en place. 34 | 35 | Typiquement, chaque test nécessite d'écrire une *factory* : une petite fonction pour créer le composant concerné en fonction des paramètres requis par le test. C'est la *factory* qui fait le lien nécessaire entre la bibliothèque de tests et le composant. 36 | Ce fonctionnement permet à la suite de tests de rester générique, et de fonctionner avec des composants écrits en JavaScript pur, jQuery, Angular, ou React par exemple. 37 | 38 | [Comme dit plus haut](#avertissement), certains tests ne peuvent pas être éxécutés automatiquement. Quand vous lancez les tests, ils sont quand même affichés dans les résultats avec le statut *skipped* et un message d'avertissement. 39 | 40 | ### Exemple 41 | 42 | Voici un exemple d'implémentation du test d'une *ProgressBar* : 43 | 44 | ```js 45 | var testSuite = require('rgaa-test-suite'); 46 | 47 | /** 48 | * Une factory prenant en paramètre les options nécessaires 49 | * pour le test (ici min, max, et value). 50 | * Cette fonction doit retourner un élément de DOM contenant 51 | * le composant à tester, ici une ProgressBar. 52 | */ 53 | function progressBarFactory(options) { 54 | var node = document.createElement('div'); 55 | node.setAttribute('role', 'progressbar'); 56 | node.setAttribute('aria-valuemin', options.min); 57 | node.setAttribute('aria-valuemax', options.max); 58 | node.setAttribute('aria-value', options.value); 59 | 60 | return node; 61 | } 62 | 63 | /** 64 | * Mise en place du test mocha. 65 | */ 66 | describe('Ma super barre de progression', testSuite.progressBar(progressBarFactory)); 67 | ``` 68 | 69 | Note : si vous avez utilisé le dossier [./template](./template), vous pouvez tester cet exemple en créant un fichier `test/progressBar.js`, et en y copiant le code ci-dessus. 70 | 71 | Au lancement de mocha, `progressBar()` exécutera tous les tests du motif de conception : 72 | 73 | ```sh 74 | Ma super barre de progression 75 | Motif de conception ARIA Progressbar 76 | Critère 1 : L'implémentation ARIA est-elle conforme ? 77 | Test 1.1 : Le composant respecte-t-il ces conditions ? 78 | ✔ Le composant possède un role="progressbar" 79 | ✔ Le composant possède une propriété aria-valuemin="[valeur minimale]" 80 | ✔ Le composant possède une propriété aria-valuemax="[valeur maximale]" 81 | Test 1.2 : Le composant respecte-t-il une de ces conditions ? 82 | ... 83 | ``` 84 | 85 | Vous trouverez également des exemples d'implémentation des différents tests dans le dossier [./test](./test). 86 | 87 | ### Options des *factories* 88 | 89 | Chaque test requiert des options particulières pour initialiser les composants. 90 | Vous trouverez ci-dessous la liste de ces options : 91 | 92 | * [Accordion](#accordion) 93 | * [Simple Checkbox](#simple-checkbox) 94 | * [Tristate Checkbox](#tristate-checkbox) 95 | * [Dialog](#dialog) 96 | * [ProgressBar](#progressbar) 97 | * [RadioButton](#radiobutton) 98 | * [Slider](#slider) 99 | * [TabPanel](#tabpanel) 100 | * [Tooltip](#tooltip) 101 | 102 | #### Accordion 103 | 104 | Options : 105 | 106 | ```js 107 | { 108 | // liste des onglets 109 | panels: [ 110 | { 111 | title: 'Titre', // {string} titre de l'onglet 112 | content: 'Contenu', // {string} contenu de l'onglet (peut contenir du HTML) 113 | selected: true // {bool} si l'onglet est actif ou non 114 | } 115 | ] 116 | } 117 | ``` 118 | 119 | [Exemple](./test/accordion.js) 120 | 121 | 122 | #### Simple Checkbox 123 | Options : 124 | 125 | ```js 126 | { 127 | checked: true // {bool} si la checkbox est cochée 128 | } 129 | ``` 130 | 131 | [Exemple](./test/checkbox.js) 132 | 133 | #### Tristate Checkbox 134 | Options : 135 | 136 | ```js 137 | { 138 | state: 'mixed', // {string} si la checkbox est partiellement cochée 139 | items: [ 140 | { 141 | label: 'Lettuce', // {string} le label de la checkbox 142 | checked: true // {bool} si la checkbox est cochée 143 | }, 144 | { 145 | label: 'Tomato', 146 | checked: false 147 | }, 148 | { 149 | label: 'Mustard', 150 | checked: false 151 | }, 152 | { 153 | label: 'Sprouts', 154 | checked: false 155 | } 156 | ], 157 | title: 'Sandwich Condiments' // {string} Titre du groupement de checkbox 158 | } 159 | ``` 160 | 161 | [Exemple](./test/tristateCheckbox.js) 162 | 163 | #### Dialog 164 | 165 | Options : 166 | 167 | ```js 168 | { 169 | title: 'Titre', // {string} titre de la modale 170 | content: 'Contenu' // {string} contenu de la modale 171 | } 172 | ``` 173 | 174 | Pour que le test fonctionne, la factory devra renvoyer une fonction pour ouvrir la modale, et une pour la fermer. 175 | Ceci est dû à la manière dont sont couramment implémentées les modales, qui sont souvent ajoutées à la fin de `