import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Progress } from './progress'; import { PROGRESS_CHANGE_EVENT, type ProgressChangeEvent } from './progress.types'; const MARKUP = `
`; function setup(markup = MARKUP): HTMLElement { document.body.innerHTML = markup; return document.body.firstElementChild as HTMLElement; } function label(root: HTMLElement): HTMLElement { return root.querySelector('[data-c42-progress-label]')!; } describe('Progress', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('sets the progressbar role and value attributes', () => { const root = setup(); new Progress(root, { value: 40 }); expect(root.getAttribute('role')).toBe('progressbar'); expect(root.getAttribute('aria-valuemin')).toBe('0'); expect(root.getAttribute('aria-valuemax')).toBe('100'); expect(root.getAttribute('aria-valuenow')).toBe('40'); }); it('exposes the percentage as a custom property and label', () => { const root = setup(); new Progress(root, { value: 25 }); expect(root.style.getPropertyValue('--c42-progress-percent')).toBe('25%'); expect(label(root).textContent).toBe('25%'); }); it('clamps the value to [min, max]', () => { const root = setup(); const p = new Progress(root, { value: 250 }); expect(p.getValue()).toBe(100); p.setValue(-10); expect(p.getValue()).toBe(0); }); it('supports custom min/max', () => { const root = setup(); const p = new Progress(root, { min: 0, max: 50, value: 25 }); expect(p.getValue()).toBe(25); expect(root.style.getPropertyValue('--c42-progress-percent')).toBe('50%'); }); it('marks complete when value reaches max', () => { const root = setup(); const p = new Progress(root, { value: 0 }); expect(root.dataset.state).toBe('loading'); p.setValue(100); expect(root.dataset.state).toBe('complete'); }); it('handles the indeterminate state', () => { const root = setup(); const p = new Progress(root, { indeterminate: true }); expect(root.dataset.state).toBe('indeterminate'); expect(root.hasAttribute('aria-valuenow')).toBe(false); p.setIndeterminate(false); expect(root.dataset.state).toBe('loading'); expect(root.hasAttribute('aria-valuenow')).toBe(true); }); it('setValue exits indeterminate mode', () => { const root = setup(); const p = new Progress(root, { indeterminate: true }); p.setValue(30); expect(root.dataset.state).toBe('loading'); expect(root.getAttribute('aria-valuenow')).toBe('30'); }); it('setMax re-clamps the value', () => { const root = setup(); const p = new Progress(root, { value: 80 }); p.setMax(50); expect(p.getValue()).toBe(50); }); it('guards against max <= min', () => { const root = setup(); const p = new Progress(root, { min: 10, max: 10, value: 10 }); expect(root.getAttribute('aria-valuemax')).toBe('11'); expect(p.getValue()).toBe(10); }); it('sets aria-label from the label option', () => { const root = setup(); new Progress(root, { label: 'Upload progress' }); expect(root.getAttribute('aria-label')).toBe('Upload progress'); }); it('emits progress:change on setValue', () => { const root = setup(); const p = new Progress(root, { value: 0 }); const spy = vi.fn(); p.on(PROGRESS_CHANGE_EVENT, spy); p.setValue(60); expect(spy).toHaveBeenCalledOnce(); expect(spy.mock.calls[0][0].detail).toMatchObject({ value: 60, percent: 60, max: 100 }); }); it('is a no-op when setting the same value', () => { const root = setup(); const p = new Progress(root, { value: 50 }); const spy = vi.fn(); p.on(PROGRESS_CHANGE_EVENT, spy); p.setValue(50); expect(spy).not.toHaveBeenCalled(); }); it('removes listeners on destroy', () => { const root = setup(); const p = new Progress(root, { value: 0 }); const spy = vi.fn(); p.on(PROGRESS_CHANGE_EVENT, spy); p.destroy(); p.setValue(99); expect(spy).not.toHaveBeenCalled(); }); });