- Created comprehensive API reference documentation covering authentication, request/response formats, error handling, and various API endpoints for pose estimation, system management, health checks, and WebSocket interactions. - Developed a detailed deployment guide outlining prerequisites, Docker and Kubernetes deployment steps, cloud deployment options for AWS, GCP, and Azure, and configuration for production environments.
476 lines
17 KiB
JavaScript
476 lines
17 KiB
JavaScript
// Test Runner for WiFi DensePose UI
|
|
|
|
import { API_CONFIG, buildApiUrl, buildWsUrl } from '../config/api.config.js';
|
|
import { apiService } from '../services/api.service.js';
|
|
import { wsService } from '../services/websocket.service.js';
|
|
import { poseService } from '../services/pose.service.js';
|
|
import { healthService } from '../services/health.service.js';
|
|
import { TabManager } from '../components/TabManager.js';
|
|
|
|
class TestRunner {
|
|
constructor() {
|
|
this.tests = [];
|
|
this.results = {
|
|
total: 0,
|
|
passed: 0,
|
|
failed: 0,
|
|
pending: 0
|
|
};
|
|
this.output = [];
|
|
}
|
|
|
|
// Add a test
|
|
test(name, category, testFn) {
|
|
this.tests.push({
|
|
name,
|
|
category,
|
|
fn: testFn,
|
|
status: 'pending'
|
|
});
|
|
}
|
|
|
|
// Run all tests
|
|
async runAllTests() {
|
|
this.clearResults();
|
|
this.log('Starting test suite...\n');
|
|
|
|
for (const test of this.tests) {
|
|
await this.runSingleTest(test);
|
|
}
|
|
|
|
this.updateSummary();
|
|
this.log(`\nTest suite completed. ${this.results.passed}/${this.results.total} tests passed.`);
|
|
}
|
|
|
|
// Run tests by category
|
|
async runTestsByCategory(category) {
|
|
this.clearResults();
|
|
const categoryTests = this.tests.filter(test => test.category === category);
|
|
|
|
this.log(`Starting ${category} tests...\n`);
|
|
|
|
for (const test of categoryTests) {
|
|
await this.runSingleTest(test);
|
|
}
|
|
|
|
this.updateSummary();
|
|
this.log(`\n${category} tests completed. ${this.results.passed}/${this.results.total} tests passed.`);
|
|
}
|
|
|
|
// Run a single test
|
|
async runSingleTest(test) {
|
|
this.log(`Running: ${test.name}...`);
|
|
|
|
try {
|
|
const startTime = Date.now();
|
|
await test.fn();
|
|
const duration = Date.now() - startTime;
|
|
|
|
test.status = 'pass';
|
|
this.results.passed++;
|
|
this.log(` ✓ PASS (${duration}ms)`);
|
|
|
|
} catch (error) {
|
|
test.status = 'fail';
|
|
test.error = error.message;
|
|
this.results.failed++;
|
|
this.log(` ✗ FAIL: ${error.message}`);
|
|
|
|
} finally {
|
|
this.results.total++;
|
|
this.updateTestDisplay(test);
|
|
}
|
|
}
|
|
|
|
// Assertion helpers
|
|
assert(condition, message) {
|
|
if (!condition) {
|
|
throw new Error(message || 'Assertion failed');
|
|
}
|
|
}
|
|
|
|
assertEqual(actual, expected, message) {
|
|
if (actual !== expected) {
|
|
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
|
}
|
|
}
|
|
|
|
assertNotEqual(actual, unexpected, message) {
|
|
if (actual === unexpected) {
|
|
throw new Error(message || `Expected not to equal ${unexpected}`);
|
|
}
|
|
}
|
|
|
|
assertThrows(fn, message) {
|
|
try {
|
|
fn();
|
|
throw new Error(message || 'Expected function to throw');
|
|
} catch (error) {
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
async assertRejects(promise, message) {
|
|
try {
|
|
await promise;
|
|
throw new Error(message || 'Expected promise to reject');
|
|
} catch (error) {
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
// Logging
|
|
log(message) {
|
|
this.output.push(message);
|
|
const outputElement = document.getElementById('testOutput');
|
|
if (outputElement) {
|
|
outputElement.style.display = 'block';
|
|
outputElement.textContent = this.output.join('\n');
|
|
outputElement.scrollTop = outputElement.scrollHeight;
|
|
}
|
|
}
|
|
|
|
// Clear results
|
|
clearResults() {
|
|
this.results = { total: 0, passed: 0, failed: 0, pending: 0 };
|
|
this.output = [];
|
|
|
|
// Reset test statuses
|
|
this.tests.forEach(test => {
|
|
test.status = 'pending';
|
|
delete test.error;
|
|
});
|
|
|
|
// Clear UI
|
|
this.updateSummary();
|
|
this.tests.forEach(test => this.updateTestDisplay(test));
|
|
|
|
const outputElement = document.getElementById('testOutput');
|
|
if (outputElement) {
|
|
outputElement.style.display = 'none';
|
|
outputElement.textContent = '';
|
|
}
|
|
}
|
|
|
|
// Update test display
|
|
updateTestDisplay(test) {
|
|
const container = document.getElementById(`${test.category}Tests`);
|
|
if (!container) return;
|
|
|
|
let testElement = container.querySelector(`[data-test="${test.name}"]`);
|
|
if (!testElement) {
|
|
testElement = document.createElement('div');
|
|
testElement.className = 'test-case';
|
|
testElement.setAttribute('data-test', test.name);
|
|
testElement.innerHTML = `
|
|
<div class="test-name">${test.name}</div>
|
|
<div class="test-status pending">pending</div>
|
|
`;
|
|
container.appendChild(testElement);
|
|
}
|
|
|
|
const statusElement = testElement.querySelector('.test-status');
|
|
statusElement.className = `test-status ${test.status}`;
|
|
statusElement.textContent = test.status;
|
|
|
|
if (test.error) {
|
|
statusElement.title = test.error;
|
|
}
|
|
}
|
|
|
|
// Update summary
|
|
updateSummary() {
|
|
document.getElementById('totalTests').textContent = this.results.total;
|
|
document.getElementById('passedTests').textContent = this.results.passed;
|
|
document.getElementById('failedTests').textContent = this.results.failed;
|
|
document.getElementById('pendingTests').textContent = this.tests.length - this.results.total;
|
|
}
|
|
}
|
|
|
|
// Create test runner instance
|
|
const testRunner = new TestRunner();
|
|
|
|
// Mock DOM elements for testing
|
|
function createMockContainer() {
|
|
const container = document.createElement('div');
|
|
container.innerHTML = `
|
|
<nav class="nav-tabs">
|
|
<button class="nav-tab active" data-tab="dashboard">Dashboard</button>
|
|
<button class="nav-tab" data-tab="hardware">Hardware</button>
|
|
</nav>
|
|
<div id="dashboard" class="tab-content active"></div>
|
|
<div id="hardware" class="tab-content"></div>
|
|
`;
|
|
return container;
|
|
}
|
|
|
|
// API Configuration Tests
|
|
testRunner.test('API_CONFIG contains required endpoints', 'apiConfig', () => {
|
|
testRunner.assert(API_CONFIG.ENDPOINTS, 'ENDPOINTS should exist');
|
|
testRunner.assert(API_CONFIG.ENDPOINTS.POSE, 'POSE endpoints should exist');
|
|
testRunner.assert(API_CONFIG.ENDPOINTS.HEALTH, 'HEALTH endpoints should exist');
|
|
testRunner.assert(API_CONFIG.ENDPOINTS.STREAM, 'STREAM endpoints should exist');
|
|
});
|
|
|
|
testRunner.test('buildApiUrl constructs correct URLs', 'apiConfig', () => {
|
|
const url = buildApiUrl('/api/v1/pose/current', { zone_id: 'zone1', limit: 10 });
|
|
testRunner.assert(url.includes('/api/v1/pose/current'), 'URL should contain endpoint');
|
|
testRunner.assert(url.includes('zone_id=zone1'), 'URL should contain zone_id parameter');
|
|
testRunner.assert(url.includes('limit=10'), 'URL should contain limit parameter');
|
|
});
|
|
|
|
testRunner.test('buildApiUrl handles path parameters', 'apiConfig', () => {
|
|
const url = buildApiUrl('/api/v1/pose/zones/{zone_id}/occupancy', { zone_id: 'zone1' });
|
|
testRunner.assert(url.includes('/api/v1/pose/zones/zone1/occupancy'), 'URL should replace path parameter');
|
|
testRunner.assert(!url.includes('{zone_id}'), 'URL should not contain placeholder');
|
|
});
|
|
|
|
testRunner.test('buildWsUrl constructs WebSocket URLs', 'apiConfig', () => {
|
|
const url = buildWsUrl('/api/v1/stream/pose', { token: 'test-token' });
|
|
testRunner.assert(url.startsWith('ws://') || url.startsWith('wss://'), 'URL should be WebSocket protocol');
|
|
testRunner.assert(url.includes('/api/v1/stream/pose'), 'URL should contain endpoint');
|
|
testRunner.assert(url.includes('token=test-token'), 'URL should contain token parameter');
|
|
});
|
|
|
|
// API Service Tests
|
|
testRunner.test('apiService has required methods', 'apiService', () => {
|
|
testRunner.assert(typeof apiService.get === 'function', 'get method should exist');
|
|
testRunner.assert(typeof apiService.post === 'function', 'post method should exist');
|
|
testRunner.assert(typeof apiService.put === 'function', 'put method should exist');
|
|
testRunner.assert(typeof apiService.delete === 'function', 'delete method should exist');
|
|
});
|
|
|
|
testRunner.test('apiService can set auth token', 'apiService', () => {
|
|
const token = 'test-token-123';
|
|
apiService.setAuthToken(token);
|
|
testRunner.assertEqual(apiService.authToken, token, 'Auth token should be set');
|
|
});
|
|
|
|
testRunner.test('apiService builds correct headers', 'apiService', () => {
|
|
apiService.setAuthToken('test-token');
|
|
const headers = apiService.getHeaders();
|
|
testRunner.assert(headers['Content-Type'], 'Content-Type header should exist');
|
|
testRunner.assert(headers['Authorization'], 'Authorization header should exist');
|
|
testRunner.assertEqual(headers['Authorization'], 'Bearer test-token', 'Authorization header should be correct');
|
|
});
|
|
|
|
testRunner.test('apiService handles interceptors', 'apiService', () => {
|
|
let requestIntercepted = false;
|
|
let responseIntercepted = false;
|
|
|
|
apiService.addRequestInterceptor(() => {
|
|
requestIntercepted = true;
|
|
return { url: 'test', options: {} };
|
|
});
|
|
|
|
apiService.addResponseInterceptor(() => {
|
|
responseIntercepted = true;
|
|
return new Response('{}');
|
|
});
|
|
|
|
testRunner.assert(apiService.requestInterceptors.length > 0, 'Request interceptor should be added');
|
|
testRunner.assert(apiService.responseInterceptors.length > 0, 'Response interceptor should be added');
|
|
});
|
|
|
|
// WebSocket Service Tests
|
|
testRunner.test('wsService has required methods', 'websocketService', () => {
|
|
testRunner.assert(typeof wsService.connect === 'function', 'connect method should exist');
|
|
testRunner.assert(typeof wsService.disconnect === 'function', 'disconnect method should exist');
|
|
testRunner.assert(typeof wsService.send === 'function', 'send method should exist');
|
|
testRunner.assert(typeof wsService.onMessage === 'function', 'onMessage method should exist');
|
|
});
|
|
|
|
testRunner.test('wsService generates unique connection IDs', 'websocketService', () => {
|
|
const id1 = wsService.generateId();
|
|
const id2 = wsService.generateId();
|
|
testRunner.assertNotEqual(id1, id2, 'Connection IDs should be unique');
|
|
testRunner.assert(id1.startsWith('ws_'), 'Connection ID should have correct prefix');
|
|
});
|
|
|
|
testRunner.test('wsService manages connection state', 'websocketService', () => {
|
|
const initialConnections = wsService.getActiveConnections();
|
|
testRunner.assert(Array.isArray(initialConnections), 'Active connections should be an array');
|
|
});
|
|
|
|
// Pose Service Tests
|
|
testRunner.test('poseService has required methods', 'poseService', () => {
|
|
testRunner.assert(typeof poseService.getCurrentPose === 'function', 'getCurrentPose method should exist');
|
|
testRunner.assert(typeof poseService.getZoneOccupancy === 'function', 'getZoneOccupancy method should exist');
|
|
testRunner.assert(typeof poseService.startPoseStream === 'function', 'startPoseStream method should exist');
|
|
testRunner.assert(typeof poseService.subscribeToPoseUpdates === 'function', 'subscribeToPoseUpdates method should exist');
|
|
});
|
|
|
|
testRunner.test('poseService subscription management', 'poseService', () => {
|
|
let callbackCalled = false;
|
|
const unsubscribe = poseService.subscribeToPoseUpdates(() => {
|
|
callbackCalled = true;
|
|
});
|
|
|
|
testRunner.assert(typeof unsubscribe === 'function', 'Subscribe should return unsubscribe function');
|
|
testRunner.assert(poseService.poseSubscribers.length > 0, 'Subscriber should be added');
|
|
|
|
unsubscribe();
|
|
testRunner.assertEqual(poseService.poseSubscribers.length, 0, 'Subscriber should be removed');
|
|
});
|
|
|
|
testRunner.test('poseService handles pose updates', 'poseService', () => {
|
|
let receivedUpdate = null;
|
|
|
|
poseService.subscribeToPoseUpdates(update => {
|
|
receivedUpdate = update;
|
|
});
|
|
|
|
const testUpdate = { type: 'pose_update', data: { persons: [] } };
|
|
poseService.notifyPoseSubscribers(testUpdate);
|
|
|
|
testRunner.assertEqual(receivedUpdate, testUpdate, 'Update should be received by subscriber');
|
|
});
|
|
|
|
// Health Service Tests
|
|
testRunner.test('healthService has required methods', 'healthService', () => {
|
|
testRunner.assert(typeof healthService.getSystemHealth === 'function', 'getSystemHealth method should exist');
|
|
testRunner.assert(typeof healthService.checkReadiness === 'function', 'checkReadiness method should exist');
|
|
testRunner.assert(typeof healthService.startHealthMonitoring === 'function', 'startHealthMonitoring method should exist');
|
|
testRunner.assert(typeof healthService.subscribeToHealth === 'function', 'subscribeToHealth method should exist');
|
|
});
|
|
|
|
testRunner.test('healthService subscription management', 'healthService', () => {
|
|
let callbackCalled = false;
|
|
const unsubscribe = healthService.subscribeToHealth(() => {
|
|
callbackCalled = true;
|
|
});
|
|
|
|
testRunner.assert(typeof unsubscribe === 'function', 'Subscribe should return unsubscribe function');
|
|
testRunner.assert(healthService.healthSubscribers.length > 0, 'Subscriber should be added');
|
|
|
|
unsubscribe();
|
|
testRunner.assertEqual(healthService.healthSubscribers.length, 0, 'Subscriber should be removed');
|
|
});
|
|
|
|
testRunner.test('healthService status checking', 'healthService', () => {
|
|
// Set mock health status
|
|
healthService.lastHealthStatus = { status: 'healthy' };
|
|
testRunner.assert(healthService.isSystemHealthy(), 'System should be healthy');
|
|
|
|
healthService.lastHealthStatus = { status: 'unhealthy' };
|
|
testRunner.assert(!healthService.isSystemHealthy(), 'System should not be healthy');
|
|
|
|
healthService.lastHealthStatus = null;
|
|
testRunner.assertEqual(healthService.isSystemHealthy(), null, 'System health should be null when no status');
|
|
});
|
|
|
|
// UI Component Tests
|
|
testRunner.test('TabManager can be instantiated', 'uiComponent', () => {
|
|
const container = createMockContainer();
|
|
const tabManager = new TabManager(container);
|
|
testRunner.assert(tabManager instanceof TabManager, 'TabManager should be instantiated');
|
|
});
|
|
|
|
testRunner.test('TabManager initializes tabs', 'uiComponent', () => {
|
|
const container = createMockContainer();
|
|
const tabManager = new TabManager(container);
|
|
tabManager.init();
|
|
|
|
testRunner.assert(tabManager.tabs.length > 0, 'Tabs should be found');
|
|
testRunner.assert(tabManager.tabContents.length > 0, 'Tab contents should be found');
|
|
});
|
|
|
|
testRunner.test('TabManager handles tab switching', 'uiComponent', () => {
|
|
const container = createMockContainer();
|
|
const tabManager = new TabManager(container);
|
|
tabManager.init();
|
|
|
|
let tabChangeEvent = null;
|
|
tabManager.onTabChange((newTab, oldTab) => {
|
|
tabChangeEvent = { newTab, oldTab };
|
|
});
|
|
|
|
// Switch to hardware tab
|
|
const hardwareTab = container.querySelector('[data-tab="hardware"]');
|
|
tabManager.switchTab(hardwareTab);
|
|
|
|
testRunner.assertEqual(tabManager.getActiveTab(), 'hardware', 'Active tab should be updated');
|
|
testRunner.assert(tabChangeEvent, 'Tab change event should be fired');
|
|
testRunner.assertEqual(tabChangeEvent.newTab, 'hardware', 'New tab should be correct');
|
|
});
|
|
|
|
testRunner.test('TabManager can enable/disable tabs', 'uiComponent', () => {
|
|
const container = createMockContainer();
|
|
const tabManager = new TabManager(container);
|
|
tabManager.init();
|
|
|
|
tabManager.setTabEnabled('hardware', false);
|
|
const hardwareTab = container.querySelector('[data-tab="hardware"]');
|
|
testRunner.assert(hardwareTab.disabled, 'Tab should be disabled');
|
|
testRunner.assert(hardwareTab.classList.contains('disabled'), 'Tab should have disabled class');
|
|
});
|
|
|
|
testRunner.test('TabManager can show/hide tabs', 'uiComponent', () => {
|
|
const container = createMockContainer();
|
|
const tabManager = new TabManager(container);
|
|
tabManager.init();
|
|
|
|
tabManager.setTabVisible('hardware', false);
|
|
const hardwareTab = container.querySelector('[data-tab="hardware"]');
|
|
testRunner.assertEqual(hardwareTab.style.display, 'none', 'Tab should be hidden');
|
|
});
|
|
|
|
// Integration Tests
|
|
testRunner.test('Services can be imported together', 'integration', () => {
|
|
testRunner.assert(apiService, 'API service should be available');
|
|
testRunner.assert(wsService, 'WebSocket service should be available');
|
|
testRunner.assert(poseService, 'Pose service should be available');
|
|
testRunner.assert(healthService, 'Health service should be available');
|
|
});
|
|
|
|
testRunner.test('Services maintain separate state', 'integration', () => {
|
|
// Set different states
|
|
apiService.setAuthToken('api-token');
|
|
poseService.subscribeToPoseUpdates(() => {});
|
|
healthService.subscribeToHealth(() => {});
|
|
|
|
// Verify independence
|
|
testRunner.assertEqual(apiService.authToken, 'api-token', 'API service should maintain its token');
|
|
testRunner.assert(poseService.poseSubscribers.length > 0, 'Pose service should have subscribers');
|
|
testRunner.assert(healthService.healthSubscribers.length > 0, 'Health service should have subscribers');
|
|
});
|
|
|
|
testRunner.test('Configuration is consistent across services', 'integration', () => {
|
|
// All services should use the same configuration
|
|
testRunner.assert(API_CONFIG.BASE_URL, 'Base URL should be configured');
|
|
testRunner.assert(API_CONFIG.ENDPOINTS, 'Endpoints should be configured');
|
|
testRunner.assert(API_CONFIG.WS_CONFIG, 'WebSocket config should be available');
|
|
});
|
|
|
|
// Event listeners for UI
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
document.getElementById('runAllTests').addEventListener('click', () => {
|
|
testRunner.runAllTests();
|
|
});
|
|
|
|
document.getElementById('runUnitTests').addEventListener('click', () => {
|
|
const unitCategories = ['apiConfig', 'apiService', 'websocketService', 'poseService', 'healthService', 'uiComponent'];
|
|
testRunner.clearResults();
|
|
|
|
(async () => {
|
|
for (const category of unitCategories) {
|
|
await testRunner.runTestsByCategory(category);
|
|
}
|
|
testRunner.updateSummary();
|
|
})();
|
|
});
|
|
|
|
document.getElementById('runIntegrationTests').addEventListener('click', () => {
|
|
testRunner.runTestsByCategory('integration');
|
|
});
|
|
|
|
document.getElementById('clearResults').addEventListener('click', () => {
|
|
testRunner.clearResults();
|
|
});
|
|
|
|
// Initialize test display
|
|
testRunner.tests.forEach(test => testRunner.updateTestDisplay(test));
|
|
testRunner.updateSummary();
|
|
});
|
|
|
|
export { testRunner }; |