import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AvatarGroup } from './avatar-group';
import { AVATARGROUP_CHANGE_EVENT, type AvatarGroupChangeEvent } from './avatar-group.types';
function setup(count: number, withOverflow = true): HTMLElement {
const avatars = Array.from(
{ length: count },
(_, i) => `
A${i}
`,
).join('');
const overflow = withOverflow ? '' : '';
document.body.innerHTML = `${avatars}${overflow}
`;
return document.body.firstElementChild as HTMLElement;
}
function overflowChip(root: HTMLElement): HTMLElement {
return root.querySelector('[data-c42-avatar-overflow]')!;
}
function visibleAvatars(root: HTMLElement): HTMLElement[] {
return Array.from(root.querySelectorAll('[data-c42-avatar]')).filter(
(el) => !el.hasAttribute('hidden'),
);
}
describe('AvatarGroup', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
it('throws when there are no avatar children', () => {
document.body.innerHTML = '';
const root = document.body.firstElementChild as HTMLElement;
expect(() => new AvatarGroup(root)).toThrow(/\[42\/avatar-group\]/);
});
it('sets role=group and aria-label from the label option', () => {
const root = setup(3);
new AvatarGroup(root, { label: 'Team members' });
expect(root.getAttribute('role')).toBe('group');
expect(root.getAttribute('aria-label')).toBe('Team members');
});
it('reflects stacked + spacing defaults on data-*', () => {
const root = setup(3);
new AvatarGroup(root);
expect(root.dataset.stacked).toBe('true');
expect(root.dataset.spacing).toBe('md');
});
it('honours stacked=false and a custom spacing', () => {
const root = setup(3);
new AvatarGroup(root, { stacked: false, spacing: 'lg' });
expect(root.dataset.stacked).toBe('false');
expect(root.dataset.spacing).toBe('lg');
});
it('shows all avatars and hides the chip when there is no limit', () => {
const root = setup(4);
new AvatarGroup(root);
expect(visibleAvatars(root)).toHaveLength(4);
expect(overflowChip(root).hasAttribute('hidden')).toBe(true);
expect(root.dataset.overflow).toBe('0');
});
it('caps visible avatars and fills the overflow chip with +N', () => {
const root = setup(5);
new AvatarGroup(root, { max: 3 });
expect(visibleAvatars(root)).toHaveLength(3);
expect(overflowChip(root).hasAttribute('hidden')).toBe(false);
expect(overflowChip(root).textContent).toBe('+2');
expect(overflowChip(root).getAttribute('aria-label')).toBe('2 more');
expect(root.dataset.total).toBe('5');
expect(root.dataset.visible).toBe('3');
expect(root.dataset.overflow).toBe('2');
});
it('does not show the chip when max >= total', () => {
const root = setup(3);
new AvatarGroup(root, { max: 5 });
expect(visibleAvatars(root)).toHaveLength(3);
expect(overflowChip(root).hasAttribute('hidden')).toBe(true);
});
it('recomputes overflow and emits change on setMax', () => {
const root = setup(5);
const group = new AvatarGroup(root);
const spy = vi.fn();
group.on(AVATARGROUP_CHANGE_EVENT, spy);
group.setMax(2);
expect(visibleAvatars(root)).toHaveLength(2);
expect(overflowChip(root).textContent).toBe('+3');
expect(spy).toHaveBeenCalledOnce();
expect(spy.mock.calls[0][0].detail).toEqual({ total: 5, visible: 2, overflow: 3 });
});
it('updates data-stacked via setStacked', () => {
const root = setup(3);
const group = new AvatarGroup(root, { stacked: true });
group.setStacked(false);
expect(root.dataset.stacked).toBe('false');
});
it('updates data-spacing via setSpacing', () => {
const root = setup(3);
const group = new AvatarGroup(root, { spacing: 'md' });
group.setSpacing('sm');
expect(root.dataset.spacing).toBe('sm');
});
it('refresh recomputes after avatars are added', () => {
const root = setup(2);
const group = new AvatarGroup(root, { max: 3 });
expect(overflowChip(root).hasAttribute('hidden')).toBe(true);
const extra = document.createElement('div');
extra.dataset.c42Avatar = '';
extra.innerHTML = 'NEW';
root.insertBefore(extra, overflowChip(root));
const more = document.createElement('div');
more.dataset.c42Avatar = '';
root.insertBefore(more, overflowChip(root));
group.refresh();
expect(group.total).toBe(4);
expect(visibleAvatars(root)).toHaveLength(3);
expect(overflowChip(root).textContent).toBe('+1');
});
it('works without an overflow chip element', () => {
const root = setup(4, false);
const group = new AvatarGroup(root, { max: 2 });
expect(visibleAvatars(root)).toHaveLength(2);
expect(root.dataset.overflow).toBe('2');
expect(() => group.setMax(1)).not.toThrow();
});
it('removes listeners on destroy', () => {
const root = setup(4);
const group = new AvatarGroup(root);
const spy = vi.fn();
group.on(AVATARGROUP_CHANGE_EVENT, spy);
group.destroy();
group.setMax(2);
expect(spy).not.toHaveBeenCalled();
});
});