@casoon/auditmysite
Version:
A comprehensive command-line tool for automated accessibility, security, performance, and SEO testing using Playwright and pa11y, based on sitemap URLs
260 lines • 11.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TouchTargetTest = void 0;
const base_test_1 = require("../base-test");
class TouchTargetTest extends base_test_1.BaseAccessibilityTest {
constructor(minSize, includeNonInteractive) {
super();
this.name = 'Touch Target Test';
this.description = 'Tests if interactive elements meet minimum touch target size requirements (44px)';
this.category = 'mobile';
this.priority = 'high';
this.standards = ['WCAG2AA', 'WCAG2AAA', 'Mobile Accessibility'];
this.minTouchTargetSize = 44; // Minimum size in pixels
this.includeNonInteractive = false; // Test non-interactive elements too
if (minSize)
this.minTouchTargetSize = minSize;
if (includeNonInteractive !== undefined)
this.includeNonInteractive = includeNonInteractive;
}
async run(context) {
const startTime = Date.now();
try {
// Navigate to page
await context.page.goto(context.url, { waitUntil: 'networkidle' });
// Wait for page to be fully loaded
await context.page.waitForTimeout(2000);
// Collect touch target information
const touchTargets = await this.collectTouchTargets(context.page);
// Analyze compliance
const violations = this.analyzeCompliance(touchTargets);
const score = this.calculateScore(touchTargets, violations);
const recommendations = this.generateRecommendations(violations);
const duration = Date.now() - startTime;
return {
passed: violations.length === 0,
count: touchTargets.length,
errors: violations.map(v => `${v.selector}: ${v.issue}`),
warnings: score < 90 ? ['Some touch targets are below optimal size'] : [],
details: {
touchTargets,
violations,
score,
recommendations,
duration,
url: context.url,
testName: this.name,
description: this.description
}
};
}
catch (error) {
return {
passed: false,
count: 0,
errors: [`Error occurred during touch target analysis: ${error}`],
warnings: [],
details: {
touchTargets: [],
violations: [],
score: 0,
duration: Date.now() - startTime,
url: context.url,
testName: this.name,
description: this.description
}
};
}
}
async collectTouchTargets(page) {
return await page.evaluate((minSize) => {
const touchTargets = [];
// Selectors for interactive elements
const selectors = [
'button',
'input[type="button"]',
'input[type="submit"]',
'input[type="reset"]',
'input[type="image"]',
'a[href]',
'select',
'textarea',
'input[type="text"]',
'input[type="email"]',
'input[type="password"]',
'input[type="search"]',
'input[type="tel"]',
'input[type="url"]',
'input[type="number"]',
'input[type="date"]',
'input[type="time"]',
'input[type="datetime-local"]',
'input[type="month"]',
'input[type="week"]',
'input[type="file"]',
'input[type="range"]',
'input[type="color"]',
'[role="button"]',
'[role="link"]',
'[role="menuitem"]',
'[role="tab"]',
'[role="option"]',
'[role="checkbox"]',
'[role="radio"]',
'[role="switch"]',
'[role="slider"]',
'[role="spinbutton"]',
'[tabindex]:not([tabindex="-1"])'
];
// Collect all interactive elements
const elements = document.querySelectorAll(selectors.join(','));
elements.forEach((element) => {
const rect = element.getBoundingClientRect();
const computedStyle = window.getComputedStyle(element);
// Get element dimensions
const width = rect.width;
const height = rect.height;
// Check if element is visible
if (width === 0 || height === 0 || computedStyle.display === 'none' || computedStyle.visibility === 'hidden') {
return;
}
// Determine element type
let type = 'other';
if (element.tagName === 'BUTTON' || element.getAttribute('role') === 'button') {
type = 'button';
}
else if (element.tagName === 'A') {
type = 'link';
}
else if (element.tagName === 'INPUT') {
type = 'input';
}
else if (element.tagName === 'SELECT') {
type = 'select';
}
else if (element.tagName === 'TEXTAREA') {
type = 'textarea';
}
// Get element text
let text = '';
if (element.tagName === 'INPUT' && element.type === 'submit') {
text = element.value || 'Submit';
}
else if (element.tagName === 'BUTTON') {
text = element.textContent?.trim() || '';
}
else if (element.tagName === 'A') {
text = element.textContent?.trim() || element.href;
}
else {
text = element.textContent?.trim() || '';
}
// Check compliance
const minSize = Math.min(width, height);
const isCompliant = minSize >= 44;
touchTargets.push({
selector: this.generateSelector(element),
tagName: element.tagName.toLowerCase(),
text: text.substring(0, 50), // Limit text length
width: Math.round(width),
height: Math.round(height),
minSize: Math.round(minSize),
isCompliant,
position: { x: Math.round(rect.left), y: Math.round(rect.top) },
type
});
});
return touchTargets;
}, this.minTouchTargetSize);
}
generateSelector(element) {
// Generate a unique selector for the element
if (element.id) {
return `#${element.id}`;
}
if (element.className) {
const classes = element.className.split(' ').filter(c => c.trim());
if (classes.length > 0) {
return `${element.tagName.toLowerCase()}.${classes.join('.')}`;
}
}
// Fallback to tag name with position
const siblings = Array.from(element.parentElement?.children || []);
const index = siblings.indexOf(element) + 1;
return `${element.tagName.toLowerCase()}:nth-child(${index})`;
}
analyzeCompliance(touchTargets) {
const violations = [];
touchTargets.forEach(target => {
if (!target.isCompliant) {
const issue = `Touch target size (${target.minSize}px) is below minimum requirement (${this.minTouchTargetSize}px)`;
const recommendation = this.getRecommendation(target);
violations.push({
selector: target.selector,
tagName: target.tagName,
text: target.text,
width: target.width,
height: target.height,
minSize: target.minSize,
issue,
recommendation
});
}
});
return violations;
}
calculateScore(touchTargets, violations) {
if (touchTargets.length === 0)
return 100;
const compliantCount = touchTargets.length - violations.length;
return Math.round((compliantCount / touchTargets.length) * 100);
}
generateRecommendations(violations) {
const recommendations = [];
if (violations.length === 0) {
recommendations.push('All touch targets meet minimum size requirements');
return recommendations;
}
// Group violations by type
const buttonViolations = violations.filter(v => v.tagName === 'button' || v.tagName === 'input');
const linkViolations = violations.filter(v => v.tagName === 'a');
const inputViolations = violations.filter(v => ['input', 'select', 'textarea'].includes(v.tagName));
if (buttonViolations.length > 0) {
recommendations.push(`Increase button sizes: ${buttonViolations.length} buttons are too small`);
}
if (linkViolations.length > 0) {
recommendations.push(`Increase link sizes: ${linkViolations.length} links are too small`);
}
if (inputViolations.length > 0) {
recommendations.push(`Increase input field sizes: ${inputViolations.length} input fields are too small`);
}
recommendations.push('Ensure all interactive elements have a minimum touch target size of 44px');
recommendations.push('Consider using padding to increase touch target size without changing visual appearance');
recommendations.push('Test on actual mobile devices to verify touch target usability');
return recommendations;
}
getRecommendation(target) {
switch (target.type) {
case 'button':
return `Increase button size to at least ${this.minTouchTargetSize}x${this.minTouchTargetSize}px or add padding`;
case 'link':
return `Increase link size to at least ${this.minTouchTargetSize}x${this.minTouchTargetSize}px or add padding`;
case 'input':
return `Increase input field height to at least ${this.minTouchTargetSize}px`;
case 'select':
return `Increase select dropdown size to at least ${this.minTouchTargetSize}x${this.minTouchTargetSize}px`;
case 'textarea':
return `Increase textarea size to at least ${this.minTouchTargetSize}px height`;
default:
return `Increase element size to at least ${this.minTouchTargetSize}x${this.minTouchTargetSize}px`;
}
}
setMinTouchTargetSize(size) {
this.minTouchTargetSize = size;
}
getMinTouchTargetSize() {
return this.minTouchTargetSize;
}
}
exports.TouchTargetTest = TouchTargetTest;
//# sourceMappingURL=touch-target-test.js.map