/** * Tests unitaires pour js/countdown.js (classe CountdownTimer). * * Le script est chargé dans un contexte vm avec un DOM simulé : * la date courante est figée (FakeDate) et setInterval est factice, * ce qui rend les calculs de temps restant déterministes. */ 'use strict'; const { describe, test } = require('node:test'); const assert = require('node:assert'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const COUNTDOWN_PATH = path.join(__dirname, '..', '..', 'js', 'countdown.js'); const COUNTDOWN_SOURCE = fs.readFileSync(COUNTDOWN_PATH, 'utf8'); // Date courante figée pour tous les tests const FIXED_NOW = new Date('2030-01-01T00:00:00Z').getTime(); function makeElements() { return { 'countdown-days': { textContent: '' }, 'countdown-hours': { textContent: '' }, 'countdown-minutes': { textContent: '' }, 'countdown-seconds': { textContent: '' } }; } /** * Charge countdown.js dans un contexte vm isolé et retourne la classe. * * @param {object|null} options.elements Éléments DOM simulés (null = absents) * @param {number} options.now Timestamp courant simulé */ function loadCountdown({ elements = null, now = FIXED_NOW } = {}) { class FakeDate extends Date { constructor(...args) { if (args.length === 0) { super(now); } else { super(...args); } } static now() { return now; } } const windowMock = { addEventListener() {}, location: { href: '' } }; // Timers factices : le timer ne tourne jamais tout seul, // on enregistre simplement les appels pour vérification. const timers = { intervals: [], cleared: [] }; let nextIntervalId = 1; const sandbox = { document: { getElementById: (id) => (elements ? elements[id] || null : null), querySelectorAll: () => [], addEventListener() {} }, window: windowMock, Date: FakeDate, setInterval: (fn, delay) => { const id = nextIntervalId++; timers.intervals.push({ id, fn, delay }); return id; }, clearInterval: (id) => { timers.cleared.push(id); }, setTimeout: () => 0, console }; vm.createContext(sandbox); const CountdownTimer = vm.runInContext(COUNTDOWN_SOURCE + '\nCountdownTimer;', sandbox); return { CountdownTimer, window: windowMock, timers }; } describe('CountdownTimer', () => { test('formatNumber complète les nombres sur deux chiffres', () => { const { CountdownTimer } = loadCountdown(); assert.strictEqual(CountdownTimer.prototype.formatNumber(0), '00'); assert.strictEqual(CountdownTimer.prototype.formatNumber(5), '05'); assert.strictEqual(CountdownTimer.prototype.formatNumber(42), '42'); assert.strictEqual(CountdownTimer.prototype.formatNumber(123), '123'); }); test('calcule correctement le temps restant (jours/heures/minutes/secondes)', () => { const distance = 2 * 24 * 60 * 60 * 1000 + // 2 jours 3 * 60 * 60 * 1000 + // 3 heures 4 * 60 * 1000 + // 4 minutes 5 * 1000; // 5 secondes const elements = makeElements(); const { CountdownTimer } = loadCountdown({ elements }); const timer = new CountdownTimer(FIXED_NOW + distance); try { assert.strictEqual(elements['countdown-days'].textContent, '02'); assert.strictEqual(elements['countdown-hours'].textContent, '03'); assert.strictEqual(elements['countdown-minutes'].textContent, '04'); assert.strictEqual(elements['countdown-seconds'].textContent, '05'); } finally { timer.stop(); } }); test('reporte les heures au-delà de 24h dans les jours', () => { const distance = 36 * 60 * 60 * 1000; // 36 heures = 1 jour + 12 heures const elements = makeElements(); const { CountdownTimer } = loadCountdown({ elements }); const timer = new CountdownTimer(FIXED_NOW + distance); try { assert.strictEqual(elements['countdown-days'].textContent, '01'); assert.strictEqual(elements['countdown-hours'].textContent, '12'); assert.strictEqual(elements['countdown-minutes'].textContent, '00'); assert.strictEqual(elements['countdown-seconds'].textContent, '00'); } finally { timer.stop(); } }); test('une date passée arrête le compte à rebours et redirige vers /', () => { const elements = makeElements(); const { CountdownTimer, window } = loadCountdown({ elements }); const timer = new CountdownTimer(FIXED_NOW - 1000); try { // onComplete() redirige vers la page principale assert.strictEqual(window.location.href, '/'); // Les éléments ne sont pas mis à jour quand le compte est terminé assert.strictEqual(elements['countdown-days'].textContent, ''); assert.strictEqual(elements['countdown-hours'].textContent, ''); assert.strictEqual(elements['countdown-minutes'].textContent, ''); assert.strictEqual(elements['countdown-seconds'].textContent, ''); } finally { timer.stop(); } }); test('une distance nulle affiche zéro partout sans rediriger', () => { const elements = makeElements(); const { CountdownTimer, window } = loadCountdown({ elements }); const timer = new CountdownTimer(FIXED_NOW); try { assert.strictEqual(elements['countdown-days'].textContent, '00'); assert.strictEqual(elements['countdown-hours'].textContent, '00'); assert.strictEqual(elements['countdown-minutes'].textContent, '00'); assert.strictEqual(elements['countdown-seconds'].textContent, '00'); assert.strictEqual(window.location.href, ''); } finally { timer.stop(); } }); test('fonctionne sans éléments DOM présents', () => { const { CountdownTimer } = loadCountdown({ elements: null }); const timer = new CountdownTimer(FIXED_NOW + 60 * 1000); // Aucune erreur attendue malgré l'absence des éléments timer.stop(); }); test('démarre un intervalle d\'une seconde et stop() le nettoie', () => { const { CountdownTimer, timers } = loadCountdown(); const timer = new CountdownTimer(FIXED_NOW + 60 * 1000); assert.strictEqual(timers.intervals.length, 1); assert.strictEqual(timers.intervals[0].delay, 1000); assert.strictEqual(timers.cleared.length, 0); timer.stop(); assert.deepStrictEqual(timers.cleared, [timer.interval]); }); });