/**
* 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.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 = ``;
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 = ``;
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 = ``;
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);
});
});