@casoon/auditmysite
Version:
A comprehensive command-line tool for automated accessibility, security, performance, and SEO testing using Playwright and pa11y, based on sitemap URLs
121 lines (102 loc) • 4.23 kB
text/typescript
import { BaseAccessibilityTest, TestContext, TestResult } from '../base-test';
export class SemanticHtmlTest extends BaseAccessibilityTest {
name = 'Semantic HTML';
description = 'Check for proper semantic HTML structure';
category = 'semantic';
priority = 'medium';
standards = ['WCAG 2.1 AA', 'WCAG 2.2 AA', 'Section 508'];
async run(context: TestContext): Promise<TestResult> {
const { page } = context;
try {
const result = await this.evaluateOnPage(page, () => {
const issues: string[] = [];
const warnings: string[] = [];
let totalIssues = 0;
// Check heading hierarchy
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
const headingLevels: number[] = [];
headings.forEach((heading: Element) => {
const level = parseInt(heading.tagName.charAt(1));
headingLevels.push(level);
});
// Check for proper heading hierarchy
for (let i = 1; i < headingLevels.length; i++) {
if (headingLevels[i] - headingLevels[i - 1] > 1) {
totalIssues++;
issues.push(`Heading hierarchy skipped: h${headingLevels[i - 1]} followed by h${headingLevels[i]}`);
}
}
// Check for multiple h1 elements
const h1Elements = document.querySelectorAll('h1');
if (h1Elements.length > 1) {
warnings.push(`Multiple h1 elements found (${h1Elements.length})`);
}
// Check for semantic elements
const semanticElements = document.querySelectorAll('main, nav, header, footer, section, article, aside');
const divElements = document.querySelectorAll('div');
const semanticRatio = semanticElements.length / (semanticElements.length + divElements.length);
if (semanticRatio < 0.1) {
warnings.push(`Low semantic HTML usage: ${(semanticRatio * 100).toFixed(1)}% semantic elements`);
}
// Check for lists
const lists = document.querySelectorAll('ul, ol');
const listItems = document.querySelectorAll('li');
const orphanedListItems = document.querySelectorAll('li:not(ul li):not(ol li)');
if (orphanedListItems.length > 0) {
totalIssues++;
issues.push(`${orphanedListItems.length} list items outside of list containers`);
}
// Check for tables
const tables = document.querySelectorAll('table');
tables.forEach((table: Element) => {
const tableElement = table as HTMLTableElement;
const hasCaption = tableElement.querySelector('caption');
const hasThead = tableElement.querySelector('thead');
const hasTbody = tableElement.querySelector('tbody');
if (!hasCaption) {
warnings.push(`Table missing caption: ${tableElement.outerHTML}`);
}
if (!hasThead && !hasTbody) {
warnings.push(`Table missing thead or tbody: ${tableElement.outerHTML}`);
}
});
// Check for proper list structure
const definitionLists = document.querySelectorAll('dl');
definitionLists.forEach((dl: Element) => {
const hasDt = dl.querySelector('dt');
const hasDd = dl.querySelector('dd');
if (!hasDt || !hasDd) {
totalIssues++;
issues.push(`Definition list missing dt or dd: ${dl.outerHTML}`);
}
});
return {
totalIssues,
issues,
warnings,
stats: {
headings: headings.length,
semanticElements: semanticElements.length,
divElements: divElements.length,
semanticRatio,
lists: lists.length,
listItems: listItems.length,
tables: tables.length
}
};
});
return this.createResult(
result.totalIssues === 0,
result.totalIssues,
result.issues,
result.warnings,
{
category: 'semantic-html',
stats: result.stats
}
);
} catch (error) {
return this.createErrorResult(`Semantic HTML test failed: ${error}`);
}
}
}