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