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();
});
});