├── .gitignore ├── readme.md ├── package.json ├── index.js └── public ├── index.html └── js └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Website Accessibility Tester 2 | 3 | Web app to find website accessibility issues using [Pa11y](https://github.com/pa11y/pa11y) 4 | 5 | ## Usage 6 | 7 | Install dependencies 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | Run 14 | 15 | ```bash 16 | npm start 17 | ``` 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website-accessibility-tester", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.17.1", 14 | "pa11y": "^6.0.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const pa11y = require('pa11y') 3 | const PORT = process.env.PORT || 5000 4 | 5 | const app = express() 6 | 7 | app.use(express.static('public')) 8 | 9 | app.get('/api/test', async (req, res) => { 10 | if (!req.query.url) { 11 | res.status(400).json({ error: 'url is required' }) 12 | } else { 13 | const results = await pa11y(req.query.url) 14 | res.status(200).json(results) 15 | } 16 | }) 17 | 18 | app.listen(PORT, () => console.log(`Server started on port ${PORT}`)) 19 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | Website Accessibility Tester 14 | 19 | 20 | 21 |
22 |
23 |
24 |

Website Accessibility Tester

25 |
26 |
27 | 33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 |
42 |
43 | Loading... 44 |
45 |
46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | const issuesOutput = document.querySelector('#issues') 2 | const issuesCount = document.querySelector('#number') 3 | const alertMessage= '' 4 | const emptyUrl= '' 5 | const warningMessage= '' 6 | const CsvMessage= '' 7 | 8 | 9 | // Fetch accessibility issues 10 | const testAccessibility = async (e) => { 11 | e.preventDefault() 12 | const url = document.querySelector('#url').value 13 | if (url === '') { 14 | issuesOutput.innerHTML = emptyUrl 15 | } else { 16 | setLoading() 17 | 18 | const response = await fetch(`/api/test?url=${url}`) 19 | 20 | if (response.status !== 200) { 21 | setLoading(false) 22 | issuesOutput.innerHTML = alertMessage 23 | } else { 24 | const { issues } = await response.json() 25 | addIssuesToDOM(issues) 26 | setLoading(false) 27 | document.getElementById("clearResults").classList.remove("hideButton") 28 | document.getElementById("csvBtn").classList.remove("hideButton") 29 | } 30 | } 31 | } 32 | 33 | //Download CSV 34 | const csvIssues = async (e) => { 35 | e.preventDefault() 36 | const url = document.querySelector('#url').value 37 | if (url === '') { 38 | issuesOutput.innerHTML = emptyUrl 39 | } 40 | else { 41 | const response = await fetch(`/api/test?url=${url}`) 42 | 43 | if (response.status !== 200) { 44 | setLoading(false) 45 | alert(csvMessage) 46 | } 47 | else if(issues.length === 0){ 48 | alert(CsvMessage) 49 | } 50 | else { 51 | const { issues } = await response.json() 52 | const csv = issues.map(issue => { 53 | return `${issue.code},${issue.message},${issue.context}` 54 | }).join('\n') 55 | 56 | const csvBlob = new Blob([csv], { type: 'text/csv' }) 57 | const csvUrl = URL.createObjectURL(csvBlob) 58 | const link = document.createElement('a') 59 | link.href = csvUrl 60 | link.download = 'Accessibility_issues_list_'+url.substring(12)+'.csv' 61 | document.body.appendChild(link) 62 | link.click() 63 | document.body.removeChild(link) 64 | } 65 | } 66 | } 67 | 68 | // Add issues to DOM 69 | const addIssuesToDOM = (issues) => { 70 | 71 | issuesOutput.innerHTML = '' 72 | issuesCount.innerHTML = '' 73 | 74 | if (issues.length === 0) { 75 | issuesOutput.innerHTML = warningMessage 76 | } else { 77 | issuesCount.innerHTML = ` 78 |

${issues.length} issues found !

79 | ` 80 | issues.forEach((issue) => { 81 | const output = ` 82 |
83 |
84 |

${issue.message}

85 | 86 |

87 | ${escapeHTML(issue.context)} 88 |

89 | 90 |

91 | CODE: ${issue.code} 92 |

93 |
94 |
95 | ` 96 | 97 | issuesOutput.innerHTML += output 98 | }) 99 | } 100 | } 101 | 102 | // Set loading state 103 | const setLoading = (isLoading = true) => { 104 | const loader = document.querySelector('.loader') 105 | if (isLoading) { 106 | loader.style.display = 'block' 107 | issuesOutput.innerHTML = '' 108 | } else { 109 | loader.style.display = 'none' 110 | } 111 | } 112 | 113 | // Escape HTML 114 | function escapeHTML(html) { 115 | return html 116 | .replace(/&/g, '&') 117 | .replace(//g, '>') 119 | .replace(/"/g, '"') 120 | .replace(/'/g, ''') 121 | } 122 | 123 | //Clear results 124 | const clearResults = (e) => { 125 | e.preventDefault() 126 | issuesOutput.innerHTML = '' 127 | issuesCount.innerHTML = '' 128 | document.querySelector('#url').value = '' 129 | } 130 | 131 | document.querySelector('#form').addEventListener('submit', testAccessibility) 132 | document.querySelector('#clearResults').addEventListener('click', clearResults) 133 | document.querySelector('#csvBtn').addEventListener('click', csvIssues) --------------------------------------------------------------------------------