194 lines
7.1 KiB
JavaScript
194 lines
7.1 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
|
const { AxeBuilder } = require('@axe-core/playwright');
|
|
|
|
const PRODUCTION_URL = 'https://colinknapp.com';
|
|
const LOCAL_URL = 'http://localhost:8080';
|
|
|
|
async function getPageUrl(page) {
|
|
try {
|
|
// Try production first
|
|
await page.goto(PRODUCTION_URL, { timeout: 60000 });
|
|
return PRODUCTION_URL;
|
|
} catch (error) {
|
|
console.log('Production site not available, falling back to local');
|
|
await page.goto(LOCAL_URL, { timeout: 60000 });
|
|
return LOCAL_URL;
|
|
}
|
|
}
|
|
|
|
test.describe('Accessibility Tests', () => {
|
|
test('should pass WCAG 2.1 Level AAA standards', async ({ page }) => {
|
|
const url = await getPageUrl(page);
|
|
console.log(`Running accessibility tests against ${url}`);
|
|
|
|
// Run axe accessibility tests
|
|
const results = await new AxeBuilder({ page })
|
|
.withTags(['wcag2a', 'wcag2aa', 'wcag2aaa'])
|
|
.analyze();
|
|
|
|
// Check for any violations
|
|
expect(results.violations).toHaveLength(0);
|
|
});
|
|
|
|
test('should have proper ARIA attributes for theme toggle', async ({ page }) => {
|
|
const url = await getPageUrl(page);
|
|
console.log(`Running ARIA tests against ${url}`);
|
|
|
|
// Check theme toggle button
|
|
const themeToggle = await page.locator('#themeToggle');
|
|
expect(await themeToggle.getAttribute('aria-label')).toBe('Theme mode: Auto');
|
|
expect(await themeToggle.getAttribute('role')).toBe('switch');
|
|
expect(await themeToggle.getAttribute('aria-checked')).toBe('false');
|
|
expect(await themeToggle.getAttribute('title')).toBe('Toggle between light, dark, and auto theme modes');
|
|
expect(await themeToggle.getAttribute('tabindex')).toBe('0');
|
|
});
|
|
|
|
test('should have proper heading structure', async ({ page }) => {
|
|
const url = await getPageUrl(page);
|
|
console.log(`Running heading structure tests against ${url}`);
|
|
|
|
// Check main content area
|
|
const mainContent = await page.locator('.container-fluid');
|
|
expect(await mainContent.getAttribute('role')).toBe('main');
|
|
|
|
// Check heading hierarchy
|
|
const h1 = await page.locator('h1');
|
|
expect(await h1.count()).toBe(1);
|
|
|
|
const h2s = await page.locator('h2');
|
|
expect(await h2s.count()).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should have working external links', async ({ page, request }) => {
|
|
const url = await getPageUrl(page);
|
|
console.log(`Running link validation tests against ${url}`);
|
|
await page.goto(url, { timeout: 60000 });
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Get all external links
|
|
const externalLinks = await page.$$('a[href^="http"]:not([href^="http://localhost"])');
|
|
|
|
// Skip test if no external links found
|
|
if (externalLinks.length === 0) {
|
|
console.log('No external links found, skipping test');
|
|
return;
|
|
}
|
|
|
|
const brokenLinks = [];
|
|
for (const link of externalLinks) {
|
|
const href = await link.getAttribute('href');
|
|
if (!href) continue;
|
|
|
|
try {
|
|
const response = await request.head(href);
|
|
if (response.status() >= 400) {
|
|
brokenLinks.push({
|
|
href,
|
|
status: response.status()
|
|
});
|
|
}
|
|
} catch (error) {
|
|
brokenLinks.push({
|
|
href,
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
if (brokenLinks.length > 0) {
|
|
console.log('\nBroken or inaccessible links:');
|
|
brokenLinks.forEach(link => {
|
|
if (link.error) {
|
|
console.log(`- ${link.href}: ${link.error}`);
|
|
} else {
|
|
console.log(`- ${link.href}: HTTP ${link.status}`);
|
|
}
|
|
});
|
|
throw new Error('Some external links are broken or inaccessible');
|
|
}
|
|
});
|
|
|
|
test('should have proper color contrast', async ({ page }) => {
|
|
const url = await getPageUrl(page);
|
|
console.log(`Running color contrast tests against ${url}`);
|
|
await page.goto(url, { timeout: 60000 });
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Check text color contrast in both light and dark modes
|
|
const contrastInfo = await page.evaluate(() => {
|
|
const getContrastRatio = (color1, color2) => {
|
|
const getLuminance = (r, g, b) => {
|
|
const [rs, gs, bs] = [r, g, b].map(c => {
|
|
c = c / 255;
|
|
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
});
|
|
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
};
|
|
|
|
const parseColor = (color) => {
|
|
const rgb = color.match(/\d+/g).map(Number);
|
|
return rgb.length === 3 ? rgb : [0, 0, 0];
|
|
};
|
|
|
|
const l1 = getLuminance(...parseColor(color1));
|
|
const l2 = getLuminance(...parseColor(color2));
|
|
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
return ratio.toFixed(2);
|
|
};
|
|
|
|
const style = getComputedStyle(document.body);
|
|
const textColor = style.color;
|
|
const backgroundColor = style.backgroundColor;
|
|
const contrastRatio = getContrastRatio(textColor, backgroundColor);
|
|
|
|
return {
|
|
textColor,
|
|
backgroundColor,
|
|
contrastRatio: parseFloat(contrastRatio)
|
|
};
|
|
});
|
|
|
|
console.log('Color contrast information:', contrastInfo);
|
|
|
|
// WCAG 2.1 Level AAA requires a contrast ratio of at least 7:1 for normal text
|
|
expect(contrastInfo.contrastRatio).toBeGreaterThanOrEqual(7);
|
|
});
|
|
|
|
test('should have alt text for all images', async ({ page }) => {
|
|
const url = await getPageUrl(page);
|
|
console.log(`Running image alt text tests against ${url}`);
|
|
await page.goto(url, { timeout: 60000 });
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Get all images
|
|
const images = await page.$$('img');
|
|
|
|
// Skip test if no images found
|
|
if (images.length === 0) {
|
|
console.log('No images found, skipping test');
|
|
return;
|
|
}
|
|
|
|
const missingAlt = [];
|
|
for (const img of images) {
|
|
const alt = await img.getAttribute('alt');
|
|
const src = await img.getAttribute('src');
|
|
|
|
// Skip decorative images (empty alt is fine)
|
|
const role = await img.getAttribute('role');
|
|
if (role === 'presentation' || role === 'none') {
|
|
continue;
|
|
}
|
|
|
|
if (!alt) {
|
|
missingAlt.push(src);
|
|
}
|
|
}
|
|
|
|
if (missingAlt.length > 0) {
|
|
console.log('\nImages missing alt text:');
|
|
missingAlt.forEach(src => console.log(`- ${src}`));
|
|
throw new Error('Some images are missing alt text');
|
|
}
|
|
});
|
|
});
|