/** * Tests for DOM rendering and user interactions. * * Covers: * - Status badge rendering * - Host list rendering * - Task list rendering * - Loading/empty/error states * - Navigation between pages */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { screen, fireEvent } from '@testing-library/dom'; import { setupMinimalDOM, waitForDOM } from './setup.js'; // ============================================================================ // UTILITY FUNCTIONS (extracted from main.js patterns) // ============================================================================ /** * Get status badge class based on status. */ function getStatusBadgeClass(status) { const statusMap = { 'online': 'badge-success', 'offline': 'badge-danger', 'unknown': 'badge-warning', 'running': 'badge-info', 'success': 'badge-success', 'failed': 'badge-danger', 'pending': 'badge-secondary', }; return statusMap[status] || 'badge-secondary'; } /** * Get status display text. */ function getStatusText(status) { const textMap = { 'online': 'En ligne', 'offline': 'Hors ligne', 'unknown': 'Inconnu', 'running': 'En cours', 'success': 'Réussi', 'failed': 'Échoué', 'pending': 'En attente', }; return textMap[status] || status; } /** * Format duration in seconds to human readable. */ function formatDuration(seconds) { if (seconds < 60) { return `${Math.round(seconds)}s`; } else if (seconds < 3600) { const mins = Math.floor(seconds / 60); const secs = Math.round(seconds % 60); return `${mins}m ${secs}s`; } else { const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); return `${hours}h ${mins}m`; } } /** * Format date to locale string. */ function formatDate(dateString) { if (!dateString) return '-'; const date = new Date(dateString); return date.toLocaleString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } /** * Render a host card. */ function renderHostCard(host) { return `

${host.name}

${getStatusText(host.status)}

${host.ip || '-'}

${(host.groups || []).join(', ') || '-'}

`; } /** * Render a task row. */ function renderTaskRow(task) { return ` ${task.playbook} ${task.target} ${getStatusText(task.status)} ${task.duration ? formatDuration(task.duration) : '-'} ${formatDate(task.created_at)} `; } // ============================================================================ // TESTS: Status Badge Functions // ============================================================================ describe('Status Badge Functions', () => { describe('getStatusBadgeClass', () => { it('returns success class for online status', () => { expect(getStatusBadgeClass('online')).toBe('badge-success'); }); it('returns danger class for offline status', () => { expect(getStatusBadgeClass('offline')).toBe('badge-danger'); }); it('returns warning class for unknown status', () => { expect(getStatusBadgeClass('unknown')).toBe('badge-warning'); }); it('returns info class for running status', () => { expect(getStatusBadgeClass('running')).toBe('badge-info'); }); it('returns secondary class for unknown status', () => { expect(getStatusBadgeClass('something-else')).toBe('badge-secondary'); }); }); describe('getStatusText', () => { it('returns French text for known statuses', () => { expect(getStatusText('online')).toBe('En ligne'); expect(getStatusText('offline')).toBe('Hors ligne'); expect(getStatusText('running')).toBe('En cours'); }); it('returns original status for unknown values', () => { expect(getStatusText('custom-status')).toBe('custom-status'); }); }); }); // ============================================================================ // TESTS: Formatting Functions // ============================================================================ describe('Formatting Functions', () => { describe('formatDuration', () => { it('formats seconds only', () => { expect(formatDuration(45)).toBe('45s'); }); it('formats minutes and seconds', () => { expect(formatDuration(125)).toBe('2m 5s'); }); it('formats hours and minutes', () => { expect(formatDuration(3725)).toBe('1h 2m'); }); it('rounds seconds', () => { expect(formatDuration(45.7)).toBe('46s'); }); }); describe('formatDate', () => { it('returns dash for null/undefined', () => { expect(formatDate(null)).toBe('-'); expect(formatDate(undefined)).toBe('-'); }); it('formats ISO date string', () => { const result = formatDate('2024-01-15T14:30:00Z'); // Should contain date parts (format varies by locale) expect(result).toMatch(/\d{2}/); }); }); }); // ============================================================================ // TESTS: Host Card Rendering // ============================================================================ describe('Host Card Rendering', () => { beforeEach(() => { setupMinimalDOM(); }); it('renders host card with all information', () => { const host = { id: 'host-1', name: 'server1.local', ip: '192.168.1.10', status: 'online', groups: ['env_prod', 'role_web'], }; document.body.innerHTML = renderHostCard(host); expect(document.querySelector('.host-name').textContent).toBe('server1.local'); expect(document.querySelector('.host-ip').textContent).toBe('192.168.1.10'); expect(document.querySelector('.badge').classList.contains('badge-success')).toBe(true); expect(document.querySelector('.host-groups').textContent).toBe('env_prod, role_web'); }); it('renders host card with unknown status', () => { const host = { id: 'host-2', name: 'server2.local', status: 'unknown', groups: [], }; document.body.innerHTML = renderHostCard(host); expect(document.querySelector('.badge').classList.contains('badge-warning')).toBe(true); expect(document.querySelector('.host-groups').textContent).toBe('-'); }); it('sets correct data attribute', () => { const host = { id: 'test-host-id', name: 'test', status: 'online' }; document.body.innerHTML = renderHostCard(host); expect(document.querySelector('.host-card').dataset.hostId).toBe('test-host-id'); }); }); // ============================================================================ // TESTS: Task Row Rendering // ============================================================================ describe('Task Row Rendering', () => { beforeEach(() => { setupMinimalDOM(); }); it('renders task row with all information', () => { const task = { id: 'task-1', playbook: 'backup.yml', target: 'all', status: 'success', duration: 125, created_at: '2024-01-15T10:30:00Z', }; document.body.innerHTML = `${renderTaskRow(task)}
`; expect(document.querySelector('.task-playbook').textContent).toBe('backup.yml'); expect(document.querySelector('.task-target').textContent).toBe('all'); expect(document.querySelector('.badge').classList.contains('badge-success')).toBe(true); expect(document.querySelector('.task-duration').textContent).toBe('2m 5s'); }); it('renders running task', () => { const task = { id: 'task-2', playbook: 'deploy.yml', target: 'webservers', status: 'running', created_at: '2024-01-15T10:30:00Z', }; document.body.innerHTML = `${renderTaskRow(task)}
`; expect(document.querySelector('.badge').classList.contains('badge-info')).toBe(true); expect(document.querySelector('.task-duration').textContent).toBe('-'); }); it('renders failed task', () => { const task = { id: 'task-3', playbook: 'update.yml', target: 'all', status: 'failed', duration: 5, }; document.body.innerHTML = `${renderTaskRow(task)}
`; expect(document.querySelector('.badge').classList.contains('badge-danger')).toBe(true); }); }); // ============================================================================ // TESTS: Loading/Empty/Error States // ============================================================================ describe('UI States', () => { beforeEach(() => { setupMinimalDOM(); }); it('shows loading state', () => { document.body.innerHTML = `
Chargement...
`; const spinner = document.querySelector('.loading-spinner'); expect(spinner.classList.contains('active')).toBe(true); }); it('shows empty state', () => { document.body.innerHTML = `

Aucun hôte configuré

`; expect(document.querySelector('.empty-state')).toBeTruthy(); expect(document.querySelector('.empty-state p').textContent).toContain('Aucun'); }); it('shows error state', () => { document.body.innerHTML = `

Erreur de connexion au serveur

`; expect(document.querySelector('.error-state')).toBeTruthy(); expect(document.querySelector('.error-message').textContent).toContain('Erreur'); }); }); // ============================================================================ // TESTS: Navigation // ============================================================================ describe('Page Navigation', () => { beforeEach(() => { setupMinimalDOM(); }); it('shows dashboard page by default', () => { const dashboard = document.getElementById('page-dashboard'); expect(dashboard.classList.contains('active')).toBe(true); }); it('hides other pages by default', () => { const hosts = document.getElementById('page-hosts'); const tasks = document.getElementById('page-tasks'); expect(hosts.classList.contains('active')).toBe(false); expect(tasks.classList.contains('active')).toBe(false); }); it('can switch pages', () => { const dashboard = document.getElementById('page-dashboard'); const hosts = document.getElementById('page-hosts'); // Simulate navigation dashboard.classList.remove('active'); hosts.classList.add('active'); expect(dashboard.classList.contains('active')).toBe(false); expect(hosts.classList.contains('active')).toBe(true); }); }); // ============================================================================ // TESTS: Login Screen // ============================================================================ describe('Login Screen', () => { beforeEach(() => { setupMinimalDOM(); }); it('login screen is hidden by default', () => { const loginScreen = document.getElementById('login-screen'); expect(loginScreen.classList.contains('hidden')).toBe(true); }); it('main content is visible by default', () => { const mainContent = document.getElementById('main-content'); expect(mainContent.classList.contains('hidden')).toBe(false); }); });