← back to Professional Directory
test-professional-directory.js
251 lines
#!/usr/bin/env node
/**
* Performance and accessibility test suite for Professional Directory.
* Tests: page load time, grid rendering, lazy loading, density slider,
* filter tabs, pagination, keyboard nav, screen reader compatibility,
* color contrast, and touch targets.
*/
const { chromium } = require('playwright');
const TEST_URL = 'http://localhost:9874';
const DESIGN_COUNT = 25;
const LARGE_GRID = 50;
async function runTests() {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
console.log('========================================');
console.log('Professional Directory Test Suite');
console.log('========================================\n');
const results = {
performance: {},
accessibility: {},
issues: []
};
try {
// ─────────────────────────────────────────────────────────────────
// PERFORMANCE TESTS
// ─────────────────────────────────────────────────────────────────
console.log('🔍 PERFORMANCE TESTS\n');
// 1. Page load time
console.log('1. Testing page load time (<2s target)...');
const startLoad = Date.now();
const response = await page.goto(TEST_URL, { waitUntil: 'networkidle' });
const loadTime = Date.now() - startLoad;
results.performance.pageLoadTime = loadTime;
const pageLoadPass = loadTime < 2000;
console.log(` Status: ${pageLoadPass ? '✅ PASS' : '❌ FAIL'} (${loadTime}ms, target <2000ms)`);
if (!pageLoadPass) results.issues.push(`Page load time ${loadTime}ms exceeds 2s target`);
// 2. Check for lazy loading attributes
console.log('\n2. Testing lazy loading on images...');
const lazyImages = await page.evaluate(() => {
const imgs = document.querySelectorAll('#results img');
return Array.from(imgs).map(img => ({
src: img.src,
hasLoading: img.hasAttribute('loading'),
loadingValue: img.getAttribute('loading'),
}));
});
const lazyLoadCount = lazyImages.filter(img => img.loadingValue === 'lazy').length;
const lazyLoadPass = lazyLoadCount > 0 || lazyImages.length === 0;
console.log(` Status: ${lazyLoadPass ? '✅ PASS' : '⚠️ WARN'} (${lazyLoadCount}/${lazyImages.length} images have loading="lazy")`);
if (lazyImages.length > 0 && lazyLoadCount === 0) {
results.issues.push('Images not using loading="lazy" attribute');
}
// 3. Grid render performance (measure FPS during density slider drag)
console.log('\n3. Testing density slider performance (target >30fps)...');
await page.waitForSelector('#grid-cols', { timeout: 5000 });
const sliderStart = Date.now();
// Animate slider from 5 to 50 columns
const sliderEl = await page.$('#grid-cols');
await page.evaluate(() => {
const slider = document.getElementById('grid-cols');
const originalValue = slider.value;
for (let i = 5; i <= 12; i++) {
slider.value = i;
slider.dispatchEvent(new Event('input', { bubbles: true }));
}
slider.value = originalValue;
slider.dispatchEvent(new Event('input', { bubbles: true }));
});
const sliderTime = Date.now() - sliderStart;
results.performance.sliderDragTime = sliderTime;
const sliderPass = sliderTime < 300; // Should be snappy
console.log(` Status: ${sliderPass ? '✅ PASS' : '⚠️ WARN'} (${sliderTime}ms for slider animation, target <300ms)`);
// 4. Filter tab click latency
console.log('\n4. Testing filter tab click latency (<500ms target)...');
const filterStart = Date.now();
await page.click('#category-select');
await page.selectOption('#category-select', 'medical');
// Wait for grid to update
await page.waitForTimeout(300);
const filterTime = Date.now() - filterStart;
results.performance.filterClickLatency = filterTime;
const filterPass = filterTime < 500;
console.log(` Status: ${filterPass ? '✅ PASS' : '⚠️ WARN'} (${filterTime}ms, target <500ms)`);
if (!filterPass) results.issues.push(`Filter latency ${filterTime}ms exceeds 500ms target`);
// 5. Pagination performance
console.log('\n5. Testing pagination rapid clicks (consistency check)...');
const pageState1 = await page.evaluate(() => document.getElementById('pageinfo').textContent);
await page.click('#next');
await page.waitForTimeout(100);
const pageState2 = await page.evaluate(() => document.getElementById('pageinfo').textContent);
await page.click('#next');
await page.waitForTimeout(100);
const pageState3 = await page.evaluate(() => document.getElementById('pageinfo').textContent);
const paginationPass = pageState1 !== pageState2 && pageState2 !== pageState3;
console.log(` Status: ${paginationPass ? '✅ PASS' : '❌ FAIL'} (pagination state changed correctly)`);
if (!paginationPass) results.issues.push('Pagination state did not change on rapid clicks');
// ─────────────────────────────────────────────────────────────────
// ACCESSIBILITY TESTS
// ─────────────────────────────────────────────────────────────────
console.log('\n\n🎯 ACCESSIBILITY TESTS\n');
// 1. Keyboard navigation
console.log('1. Testing keyboard navigation (Tab key focus)...');
await page.reload();
await page.waitForTimeout(500);
const focusElements = await page.evaluate(() => {
const tabbable = Array.from(document.querySelectorAll('button, input, select, a[href]'));
return tabbable.map(el => ({
tag: el.tagName,
type: el.type || el.tagName,
id: el.id,
hasVisibleFocus: window.getComputedStyle(el).outline !== 'none' ||
window.getComputedStyle(el).boxShadow !== 'none'
})).slice(0, 10);
});
console.log(` Found ${focusElements.length} focusable elements`);
console.log(` Status: ✅ PASS (keyboard navigation possible)`);
// 2. Screen reader compatibility (ARIA labels)
console.log('\n2. Testing screen reader compatibility (ARIA labels)...');
const ariaLabels = await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button, input, select'));
return buttons.map(el => ({
tag: el.tagName,
id: el.id,
ariaLabel: el.getAttribute('aria-label'),
title: el.getAttribute('title'),
label: el.textContent || el.value,
})).filter(el => el.title || el.ariaLabel || el.label).slice(0, 10);
});
console.log(` Found ${ariaLabels.length} labeled controls`);
const srPass = ariaLabels.length > 0;
console.log(` Status: ${srPass ? '✅ PASS' : '⚠️ WARN'} (controls have accessible labels)`);
// 3. Color contrast (status badges)
console.log('\n3. Testing color contrast on status badges...');
const badges = await page.evaluate(() => {
const els = Array.from(document.querySelectorAll('.badge, [style*="color"]'));
return els.slice(0, 5).map(el => {
const style = window.getComputedStyle(el);
return {
tag: el.tagName,
text: el.textContent.slice(0, 20),
bgColor: style.backgroundColor,
color: style.color,
};
});
});
console.log(` Sampled ${badges.length} badge colors`);
console.log(` Status: ⚠️ MANUAL REVIEW NEEDED (use WCAG contrast checker on badges)`);
results.issues.push('Color contrast on badges requires manual WCAG AA verification (Lc≥60)');
// 4. Touch target sizes (buttons, checkboxes)
console.log('\n4. Testing touch target sizes (≥48px target)...');
const touchTargets = await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button, input[type="checkbox"], input[type="range"]'));
return buttons.slice(0, 10).map(el => {
const rect = el.getBoundingClientRect();
return {
tag: el.tagName,
id: el.id,
width: Math.round(rect.width),
height: Math.round(rect.height),
minDimension: Math.min(rect.width, rect.height),
meets48px: Math.min(rect.width, rect.height) >= 48,
};
});
});
const touchPass = touchTargets.every(t => t.meets48px);
const failedTargets = touchTargets.filter(t => !t.meets48px).length;
console.log(` Tested ${touchTargets.length} controls`);
console.log(` Status: ${touchPass ? '✅ PASS' : '⚠️ WARN'} (${failedTargets} controls <48px)`);
if (!touchPass) {
results.issues.push(`${failedTargets} touch targets are <48px (accessibility concern)`);
}
// 5. Dark mode verification
console.log('\n5. Testing dark mode rendering...');
const bgColor = await page.evaluate(() => {
return window.getComputedStyle(document.body).backgroundColor;
});
// Dark bg should be something like rgb(12, 15, 26) or similar dark color
const isDarkMode = bgColor.includes('rgb') && bgColor !== 'rgba(0, 0, 0, 0)';
console.log(` Body background: ${bgColor}`);
console.log(` Status: ${isDarkMode ? '✅ PASS' : '⚠️ WARN'} (dark mode detected)`);
// ─────────────────────────────────────────────────────────────────
// SUMMARY
// ─────────────────────────────────────────────────────────────────
console.log('\n\n📊 TEST SUMMARY\n');
console.log('━━━ PERFORMANCE ━━━');
console.log(` • Page load time: ${results.performance.pageLoadTime}ms ${pageLoadPass ? '✅' : '❌'}`);
console.log(` • Grid render: ${sliderTime}ms ${sliderPass ? '✅' : '⚠️'}`);
console.log(` • Filter latency: ${filterTime}ms ${filterPass ? '✅' : '⚠️'}`);
console.log(` • Pagination state: ${paginationPass ? '✅' : '❌'}`);
console.log('\n━━━ ACCESSIBILITY ━━━');
console.log(` • Keyboard navigation: ✅ PASS`);
console.log(` • Screen reader labels: ${srPass ? '✅' : '⚠️'}`);
console.log(` • Color contrast: ⚠️ Manual review`);
console.log(` • Touch targets (≥48px): ${touchPass ? '✅' : '⚠️'} (${failedTargets} small)`);
console.log(` • Dark mode: ${isDarkMode ? '✅' : '⚠️'}`);
console.log('\n━━━ ISSUES FOUND ━━━');
if (results.issues.length === 0) {
console.log(' ✅ No critical issues found');
} else {
results.issues.forEach((issue, i) => {
console.log(` ${i + 1}. ${issue}`);
});
}
console.log('\n========================================\n');
} catch (error) {
console.error('Test error:', error);
results.issues.push(`Test execution error: ${error.message}`);
} finally {
await browser.close();
}
return results;
}
runTests().then(results => {
process.exit(results.issues.length > 2 ? 1 : 0);
});