import { describe, expect, it } from 'vitest'; import { INSTALL_PHASE_STARTING, INSTALL_STEP_LABEL, INSTALL_STEP_SEQUENCE, currentPhaseLabel, expectedStepCount, formatElapsed, progressPercent, type InstallStepView } from './cras-install-progress'; function step(name: string, ok = true): InstallStepView { return { step: name, at: '2026-07-29T20:00:00.000Z', ok }; } describe('currentPhaseLabel', () => { it('sin pasos anuncia la apertura de la instalación', () => { expect(currentPhaseLabel([])).toBe(INSTALL_PHASE_STARTING); }); it('traduce el último paso a la fase que sigue en curso, no a "terminado"', () => { // El caso que reportó el operador: `subir-artefacto` se persiste ANTES del fastPut de // ~270 MB, así que un ✓ en ese paso significa "empezó la subida". expect(currentPhaseLabel([step('preparar-artefacto'), step('subir-artefacto')])).toBe( 'Subiendo el artefacto por SFTP…' ); expect(currentPhaseLabel([step('preparar-artefacto')])).toBe( 'Descargando de Gitea y verificando sha256…' ); expect(currentPhaseLabel([step('ejecutar-instalador')])).toBe( 'Corriendo el instalador en el servidor…' ); }); it('un paso fallido es el desenlace, no una fase en curso', () => { expect(currentPhaseLabel([step('precondiciones'), step('error', false)])).toBe('Error'); expect(currentPhaseLabel([step('subir-artefacto', false)])).toBe('Subir artefacto por SFTP'); }); it('un paso desconocido no rompe la fila viva', () => { expect(currentPhaseLabel([step('paso-nuevo-del-instalador')])).toBe('Trabajando…'); }); }); describe('expectedStepCount', () => { it('cuenta verificar-servicio solo en los modos que dejan algo arrancado', () => { // `cras-install.ts` corta antes de ese paso cuando el modo no arranca nada. expect(expectedStepCount('service')).toBe(11); expect(expectedStepCount('desktop')).toBe(10); expect(expectedStepCount('none')).toBe(10); }); it('el servicio de usuario también verifica, aunque no use privilegios', () => { // Si esto se quedara comparando solo con 'service', la barra de una instalación sin // privilegios llegaría al 100 % con un paso todavía por delante. expect(expectedStepCount('user-service')).toBe(11); expect(progressPercent(10, 'user-service', 'running')).toBe(91); }); }); describe('progressPercent', () => { it('nunca llega a 100 mientras el run sigue corriendo', () => { expect(progressPercent(11, 'service', 'running')).toBe(95); expect(progressPercent(50, 'service', 'running')).toBe(95); }); it('100 solo cuando el backend cerró el run como completado', () => { expect(progressPercent(11, 'service', 'completed')).toBe(100); expect(progressPercent(0, 'service', 'completed')).toBe(100); }); it('escala con los pasos emitidos y el denominador del modo de arranque', () => { expect(progressPercent(0, 'service', 'running')).toBe(0); expect(progressPercent(5, 'none', 'running')).toBe(50); // Mismo conteo de pasos, denominador distinto: 6/11 vs 6/10. expect(progressPercent(6, 'service', 'running')).toBe(55); expect(progressPercent(6, 'none', 'running')).toBe(60); }); it('un run fallido conserva el avance parcial en lugar de saltar a 100', () => { expect(progressPercent(3, 'service', 'failed')).toBe(27); }); }); describe('formatElapsed', () => { it('formatea mm:ss y h:mm:ss', () => { expect(formatElapsed(0)).toBe('00:00'); expect(formatElapsed(65_000)).toBe('01:05'); expect(formatElapsed(599_000)).toBe('09:59'); expect(formatElapsed(3_725_000)).toBe('1:02:05'); }); it('un reloj desfasado no produce tiempos negativos', () => { expect(formatElapsed(-5_000)).toBe('00:00'); }); }); describe('cobertura de las etiquetas', () => { it('todo paso del camino feliz tiene etiqueta y fase siguiente', () => { for (const name of INSTALL_STEP_SEQUENCE) { expect(INSTALL_STEP_LABEL[name], `falta etiqueta de ${name}`).toBeTruthy(); expect(currentPhaseLabel([step(name)]), `falta fase de ${name}`).not.toBe( 'Trabajando…' ); } }); it('los pasos fuera del camino feliz también tienen etiqueta', () => { expect(INSTALL_STEP_LABEL['limpiar-staging']).toBeTruthy(); expect(INSTALL_STEP_LABEL.error).toBeTruthy(); }); });