DigitalConsumer.ISS/playwright-tests/impl/utils/ReportUtils.ts
2025-02-19 14:16:36 -05:00

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;
});
}