365 lines
13 KiB
TypeScript
365 lines
13 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { jsonReportFilePath, reportFilePath } from '../../playwright.config';
|
|
|
|
interface PageSummary {
|
|
pageName: string;
|
|
critical: number;
|
|
serious: number;
|
|
moderate: number;
|
|
minor: number;
|
|
total: number;
|
|
}
|
|
|
|
export function updateAccessibilityReport(pageName: string, results: any) {
|
|
const reportDir = path.dirname(jsonReportFilePath);
|
|
if (!fs.existsSync(reportDir)) {
|
|
fs.mkdirSync(reportDir, { recursive: true });
|
|
}
|
|
|
|
let existingIssues: { [key: string]: any[] } = {};
|
|
if (fs.existsSync(jsonReportFilePath)) {
|
|
existingIssues = JSON.parse(fs.readFileSync(jsonReportFilePath, 'utf-8'));
|
|
}
|
|
|
|
if (!existingIssues[pageName]) {
|
|
existingIssues[pageName] = [];
|
|
}
|
|
|
|
results.violations.forEach(violation => {
|
|
const violationWithPage = { ...violation, pageName };
|
|
if (!isDuplicateIssue(existingIssues[pageName], violationWithPage)) {
|
|
existingIssues[pageName].push(violationWithPage);
|
|
}
|
|
});
|
|
|
|
fs.writeFileSync(jsonReportFilePath, JSON.stringify(existingIssues, null, 2));
|
|
}
|
|
|
|
export function consolidateJsonReport() {
|
|
if (!fs.existsSync(jsonReportFilePath)) {
|
|
console.error('JSON report file does not exist.');
|
|
return;
|
|
}
|
|
|
|
const existingIssues = JSON.parse(fs.readFileSync(jsonReportFilePath, 'utf-8'));
|
|
|
|
// Consolidate issues by page
|
|
const consolidatedIssues: { [key: string]: any[] } = {};
|
|
Object.keys(existingIssues).forEach(pageName => {
|
|
if (!consolidatedIssues[pageName]) {
|
|
consolidatedIssues[pageName] = [];
|
|
}
|
|
existingIssues[pageName].forEach(issue => {
|
|
if (!isDuplicateIssue(consolidatedIssues[pageName], issue)) {
|
|
consolidatedIssues[pageName].push(issue);
|
|
}
|
|
});
|
|
});
|
|
|
|
fs.writeFileSync(jsonReportFilePath, JSON.stringify(consolidatedIssues, null, 2));
|
|
}
|
|
|
|
function generateSummaryTable(existingIssues: { [key: string]: any[] }): string {
|
|
let totalCritical = 0;
|
|
let totalSerious = 0;
|
|
let totalModerate = 0;
|
|
let totalMinor = 0;
|
|
let totalTotal = 0;
|
|
|
|
Object.keys(existingIssues).forEach(pageName => {
|
|
const issues = existingIssues[pageName];
|
|
totalCritical += issues.filter(issue => issue.impact === 'critical').length;
|
|
totalSerious += issues.filter(issue => issue.impact === 'serious').length;
|
|
totalModerate += issues.filter(issue => issue.impact === 'moderate').length;
|
|
totalMinor += issues.filter(issue => issue.impact === 'minor').length;
|
|
totalTotal += issues.length;
|
|
});
|
|
|
|
return `
|
|
<table class="summary-table" role="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Page Name</th>
|
|
<th>Critical</th>
|
|
<th>Serious</th>
|
|
<th>Moderate</th>
|
|
<th>Minor</th>
|
|
<th>Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Object.keys(existingIssues).map(pageName => {
|
|
const issues = existingIssues[pageName];
|
|
const critical = issues.filter(issue => issue.impact === 'critical').length;
|
|
const serious = issues.filter(issue => issue.impact === 'serious').length;
|
|
const moderate = issues.filter(issue => issue.impact === 'moderate').length;
|
|
const minor = issues.filter(issue => issue.impact === 'minor').length;
|
|
const total = issues.length;
|
|
|
|
return `
|
|
<tr>
|
|
<td><a href="#${pageName.replace(/\s+/g, '-')}">${pageName}</a></td>
|
|
<td><a href="#${pageName.replace(/\s+/g, '-')}-critical">${critical}</a></td>
|
|
<td><a href="#${pageName.replace(/\s+/g, '-')}-serious">${serious}</a></td>
|
|
<td><a href="#${pageName.replace(/\s+/g, '-')}-moderate">${moderate}</a></td>
|
|
<td><a href="#${pageName.replace(/\s+/g, '-')}-minor">${minor}</a></td>
|
|
<td>${total}</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td><b>Total</b></td>
|
|
<td><b>${totalCritical}</b></td>
|
|
<td><b>${totalSerious}</b></td>
|
|
<td><b>${totalModerate}</b></td>
|
|
<td><b>${totalMinor}</b></td>
|
|
<td><b>${totalTotal}</b></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
`;
|
|
}
|
|
|
|
function generateImpactSections(issues: any[], pageName: string): string {
|
|
return ['critical', 'serious', 'moderate', 'minor'].map(impactType => {
|
|
const violations = issues.filter(issue => issue.impact === impactType);
|
|
if (violations.length === 0) return '';
|
|
|
|
return `
|
|
<div class="impact-section" id="${pageName.replace(/\s+/g, '-')}-${impactType}">
|
|
<h3>${impactType.charAt(0).toUpperCase() + impactType.slice(1)} Impact</h3>
|
|
<button type="button" class="collapsible">${impactType.charAt(0).toUpperCase() + impactType.slice(1)} Impact</button>
|
|
<div class="content">
|
|
<table role="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Impact</th>
|
|
<th>Description</th>
|
|
<th>Help URL</th>
|
|
<th>Tags</th>
|
|
<th>Nodes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${violations.map(violation => `
|
|
<tr>
|
|
<td>${violation.impact}</td>
|
|
<td>${violation.description}</td>
|
|
<td><a href="${violation.helpUrl}" target="_blank">${violation.helpUrl}</a></td>
|
|
<td>${violation.tags.join(', ')}</td>
|
|
<td>
|
|
${violation.nodes.map(node => `
|
|
<div class="node">
|
|
<div class="html">${node.html}</div>
|
|
<div class="target">Target: ${node.target.join(', ')}</div>
|
|
<div class="failureSummary">${node.failureSummary}</div>
|
|
<div class="any">
|
|
<strong>Any:</strong>
|
|
${node.any.map(check => `
|
|
<div class="check">
|
|
<div class="message">${check.message}</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<div class="all">
|
|
<strong>All:</strong>
|
|
${node.all.map(check => `
|
|
<div class="check">
|
|
<div class="message">${check.message}</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<div class="none">
|
|
<strong>None:</strong>
|
|
${node.none.map(check => `
|
|
<div class="check">
|
|
<div class="message">${check.message}</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function generateDetailedSections(existingIssues: { [key: string]: any[] }): string {
|
|
return Object.keys(existingIssues).map(pageName => {
|
|
const issues = existingIssues[pageName];
|
|
const impactSections = generateImpactSections(issues, pageName);
|
|
|
|
return `
|
|
<div class="page-section" id="${pageName.replace(/\s+/g, '-')}">
|
|
<h2>${pageName}</h2>
|
|
${impactSections}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
export function createAccessibilityHtmlReport() {
|
|
consolidateJsonReport();
|
|
|
|
if (!fs.existsSync(jsonReportFilePath)) {
|
|
console.error('JSON report file does not exist.');
|
|
return;
|
|
}
|
|
|
|
const existingIssues = JSON.parse(fs.readFileSync(jsonReportFilePath, 'utf-8'));
|
|
|
|
const inlineStyles = `
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background-color: #f4f4f4;
|
|
color: #333;
|
|
}
|
|
h1 {
|
|
text-align: center;
|
|
color: #4CAF50;
|
|
}
|
|
.summary-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 20px;
|
|
}
|
|
.summary-table th, .summary-table td {
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
text-align: left;
|
|
}
|
|
.summary-table th {
|
|
background-color: #007BFF;
|
|
color: white;
|
|
}
|
|
.summary-table td a {
|
|
color: #007BFF;
|
|
text-decoration: none;
|
|
}
|
|
.summary-table td a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.page-section {
|
|
margin-bottom: 40px;
|
|
padding: 20px;
|
|
background-color: #fff;
|
|
border-radius: 8px;
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
.impact-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
.violation {
|
|
margin-bottom: 20px;
|
|
}
|
|
.violation .impact {
|
|
font-weight: bold;
|
|
color: #d9534f;
|
|
}
|
|
.violation .description {
|
|
margin-top: 5px;
|
|
}
|
|
.violation .helpUrl {
|
|
margin-top: 5px;
|
|
}
|
|
.violation .tags {
|
|
margin-top: 5px;
|
|
}
|
|
.violation .nodes {
|
|
margin-top: 10px;
|
|
}
|
|
.violation .node {
|
|
margin-top: 5px;
|
|
}
|
|
.collapsible {
|
|
background-color: #f9f9f9;
|
|
color: #333;
|
|
cursor: pointer;
|
|
padding: 10px;
|
|
width: 100%;
|
|
border: none;
|
|
text-align: left;
|
|
outline: none;
|
|
font-size: 15px;
|
|
}
|
|
.active, .collapsible:hover {
|
|
background-color: #ccc;
|
|
}
|
|
.content {
|
|
padding: 0 18px;
|
|
display: none;
|
|
overflow: hidden;
|
|
background-color: #f1f1f1;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 20px;
|
|
}
|
|
th, td {
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
text-align: left;
|
|
}
|
|
tfoot {
|
|
font-weight: bold;
|
|
}
|
|
`;
|
|
|
|
let reportContent = `
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
<title>ISS Accessibility Report</title>
|
|
<style>${inlineStyles}</style>
|
|
</head>
|
|
<body>
|
|
<h1>Accessibility Report</h1>
|
|
`;
|
|
|
|
reportContent += generateSummaryTable(existingIssues);
|
|
reportContent += generateDetailedSections(existingIssues);
|
|
|
|
reportContent += `
|
|
<script>
|
|
var coll = document.getElementsByClassName("collapsible");
|
|
for (var i = 0; i < coll.length; i++) {
|
|
coll[i].addEventListener("click", function() {
|
|
this.classList.toggle("active");
|
|
var content = this.nextElementSibling;
|
|
if (content.style.display === "block") {
|
|
content.style.display = "none";
|
|
} else {
|
|
content.style.display = "block";
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
try {
|
|
fs.writeFileSync(reportFilePath, reportContent);
|
|
console.log(`Accessibility Report generated successfully at ${reportFilePath}`);
|
|
} catch (error) {
|
|
console.error('Error writing report file:', error);
|
|
}
|
|
}
|
|
|
|
function isDuplicateIssue(existingIssues: any[], newIssue: any): boolean {
|
|
return existingIssues.some(issue => {
|
|
return issue.id === newIssue.id && issue.pageName === newIssue.pageName;
|
|
});
|
|
}
|