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