aegisai / frontend / src / components / TestHarness.tsx
TestHarness.tsx
Raw
/**
 * Test Harness Component
 * Interactive testing interface for development
 * Add to App.tsx with: import { TestHarness } from './components/TestHarness';
 */

import React, { useState } from 'react';
import { testConnection } from '../services/geminiService';
import { Play, CheckCircle, XCircle, Loader } from 'lucide-react';

export const TestHarness: React.FC = () => {
  const [tests, setTests] = useState({
    geminiConnection: { status: 'pending', message: '' },
    cameraAccess: { status: 'pending', message: '' },
    frameCapture: { status: 'pending', message: '' },
    analysis: { status: 'pending', message: '' }
  });

  const [running, setRunning] = useState(false);

  const runTest = async (testName: string, testFn: () => Promise<any>) => {
    setTests(prev => ({
      ...prev,
      [testName]: { status: 'running', message: 'Testing...' }
    }));

    try {
      const result = await testFn();
      setTests(prev => ({
        ...prev,
        [testName]: {
          status: 'success',
          message: result || 'Test passed'
        }
      }));
    } catch (error) {
      setTests(prev => ({
        ...prev,
        [testName]: {
          status: 'failed',
          message: String(error)
        }
      }));
    }
  };

  const runAllTests = async () => {
    setRunning(true);

    // Test 1: Gemini Connection
    await runTest('geminiConnection', async () => {
      const success = await testConnection();
      if (!success) throw new Error('Connection failed');
      return 'Connected successfully';
    });

    await new Promise(resolve => setTimeout(resolve, 500));

    // Test 2: Camera Access
    await runTest('cameraAccess', async () => {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: true
      });
      stream.getTracks().forEach(track => track.stop());
      return 'Camera accessible';
    });

    await new Promise(resolve => setTimeout(resolve, 500));

    // Test 3: Frame Capture
    await runTest('frameCapture', async () => {
      const video = document.createElement('video');
      const canvas = document.createElement('canvas');
      
      const stream = await navigator.mediaDevices.getUserMedia({
        video: true
      });
      
      video.srcObject = stream;
      await video.play();
      
      canvas.width = 640;
      canvas.height = 480;
      const ctx = canvas.getContext('2d');
      ctx?.drawImage(video, 0, 0, 640, 480);
      
      const base64 = canvas.toDataURL('image/jpeg');
      
      stream.getTracks().forEach(track => track.stop());
      
      if (!base64.startsWith('data:image/jpeg')) {
        throw new Error('Invalid frame format');
      }
      
      return `Frame captured (${(base64.length / 1024).toFixed(1)}KB)`;
    });

    setRunning(false);
  };

  const getStatusIcon = (status: string) => {
    switch (status) {
      case 'success':
        return <CheckCircle className="text-green-500" size={20} />;
      case 'failed':
        return <XCircle className="text-red-500" size={20} />;
      case 'running':
        return <Loader className="text-blue-500 animate-spin" size={20} />;
      default:
        return <div className="w-5 h-5 border-2 border-gray-600 rounded-full" />;
    }
  };

  return (
    <div className="fixed bottom-4 right-4 bg-gray-900 border border-gray-700 rounded-lg p-4 shadow-2xl z-50 w-96">
      <div className="flex items-center justify-between mb-4">
        <h3 className="text-white font-bold">System Tests</h3>
        <button
          onClick={runAllTests}
          disabled={running}
          className="flex items-center gap-2 px-3 py-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white rounded text-sm"
        >
          <Play size={14} />
          {running ? 'Running...' : 'Run All'}
        </button>
      </div>

      <div className="space-y-3">
        {Object.entries(tests).map(([name, test]) => (
          <div
            key={name}
            className="flex items-start gap-3 p-2 bg-gray-800 rounded"
          >
            {getStatusIcon(test.status)}
            <div className="flex-1 min-w-0">
              <div className="text-white text-sm font-medium">
                {name.replace(/([A-Z])/g, ' $1').trim()}
              </div>
              <div className="text-gray-400 text-xs truncate">
                {test.message || 'Not run'}
              </div>
            </div>
          </div>
        ))}
      </div>

      <div className="mt-4 pt-3 border-t border-gray-700">
        <div className="text-xs text-gray-500">
          Press F12 to view detailed logs
        </div>
      </div>
    </div>
  );
};