Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
409 lines
12 KiB
JavaScript
409 lines
12 KiB
JavaScript
/**
|
|
* 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 `
|
|
<div class="host-card" data-host-id="${host.id}">
|
|
<div class="host-header">
|
|
<h3 class="host-name">${host.name}</h3>
|
|
<span class="badge ${getStatusBadgeClass(host.status)}">${getStatusText(host.status)}</span>
|
|
</div>
|
|
<div class="host-info">
|
|
<p class="host-ip">${host.ip || '-'}</p>
|
|
<p class="host-groups">${(host.groups || []).join(', ') || '-'}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Render a task row.
|
|
*/
|
|
function renderTaskRow(task) {
|
|
return `
|
|
<tr class="task-row" data-task-id="${task.id}">
|
|
<td class="task-playbook">${task.playbook}</td>
|
|
<td class="task-target">${task.target}</td>
|
|
<td class="task-status">
|
|
<span class="badge ${getStatusBadgeClass(task.status)}">${getStatusText(task.status)}</span>
|
|
</td>
|
|
<td class="task-duration">${task.duration ? formatDuration(task.duration) : '-'}</td>
|
|
<td class="task-date">${formatDate(task.created_at)}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// 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 = `<table><tbody>${renderTaskRow(task)}</tbody></table>`;
|
|
|
|
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 = `<table><tbody>${renderTaskRow(task)}</tbody></table>`;
|
|
|
|
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 = `<table><tbody>${renderTaskRow(task)}</tbody></table>`;
|
|
|
|
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 = `
|
|
<div class="loading-spinner active">
|
|
<span>Chargement...</span>
|
|
</div>
|
|
`;
|
|
|
|
const spinner = document.querySelector('.loading-spinner');
|
|
expect(spinner.classList.contains('active')).toBe(true);
|
|
});
|
|
|
|
it('shows empty state', () => {
|
|
document.body.innerHTML = `
|
|
<div class="empty-state">
|
|
<p>Aucun hôte configuré</p>
|
|
<button class="btn btn-primary">Ajouter un hôte</button>
|
|
</div>
|
|
`;
|
|
|
|
expect(document.querySelector('.empty-state')).toBeTruthy();
|
|
expect(document.querySelector('.empty-state p').textContent).toContain('Aucun');
|
|
});
|
|
|
|
it('shows error state', () => {
|
|
document.body.innerHTML = `
|
|
<div class="error-state">
|
|
<p class="error-message">Erreur de connexion au serveur</p>
|
|
<button class="btn btn-secondary">Réessayer</button>
|
|
</div>
|
|
`;
|
|
|
|
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);
|
|
});
|
|
});
|