import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Avatar } from './avatar'; import { AVATAR_ERROR_EVENT, AVATAR_LOAD_EVENT, AVATAR_STATUSCHANGE_EVENT, type AvatarStatusChangeEvent, } from './avatar.types'; function setup(markup: string): HTMLElement { document.body.innerHTML = markup; return document.body.firstElementChild as HTMLElement; } const FULL = `
Jane Doe
`; function image(root: HTMLElement): HTMLImageElement { return root.querySelector('[data-c42-avatar-image]')!; } function fallback(root: HTMLElement): HTMLElement { return root.querySelector('[data-c42-avatar-fallback]')!; } function statusEl(root: HTMLElement): HTMLElement { return root.querySelector('[data-c42-avatar-status]')!; } describe('Avatar', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('derives initials from the image alt when fallback is empty', () => { const root = setup(FULL); new Avatar(root); expect(fallback(root).textContent).toBe('JD'); }); it('derives initials from the name option over alt', () => { const root = setup(FULL); new Avatar(root, { name: 'Ada Lovelace King' }); expect(fallback(root).textContent).toBe('AL'); }); it('starts in the loading state and transitions to loaded on img load', () => { const root = setup(FULL); new Avatar(root); expect(root.dataset.state).toBe('loading'); image(root).dispatchEvent(new Event('load')); expect(root.dataset.state).toBe('loaded'); }); it('transitions to error on img error', () => { const root = setup(FULL); new Avatar(root); image(root).dispatchEvent(new Event('error')); expect(root.dataset.state).toBe('error'); }); it('emits avatar:load on load', () => { const root = setup(FULL); const avatar = new Avatar(root); const spy = vi.fn(); avatar.on(AVATAR_LOAD_EVENT, spy); image(root).dispatchEvent(new Event('load')); expect(spy).toHaveBeenCalledOnce(); }); it('emits avatar:error on error', () => { const root = setup(FULL); const avatar = new Avatar(root); const spy = vi.fn(); avatar.on(AVATAR_ERROR_EVENT, spy); image(root).dispatchEvent(new Event('error')); expect(spy).toHaveBeenCalledOnce(); }); it('goes to error immediately when the image has no src', () => { const root = setup(`
No Src
`); new Avatar(root); expect(root.dataset.state).toBe('error'); expect(fallback(root).textContent).toBe('NS'); }); it('goes to error with only a fallback element (no image)', () => { const root = setup(`
`); new Avatar(root, { name: 'Grace Hopper' }); expect(root.dataset.state).toBe('error'); expect(fallback(root).textContent).toBe('GH'); }); it('reflects an initial status on the status element', () => { const root = setup(FULL); new Avatar(root, { status: 'online' }); expect(statusEl(root).dataset.status).toBe('online'); expect(statusEl(root).getAttribute('aria-label')).toBe('online'); }); it('updates status and emits avatar:statuschange via setStatus', () => { const root = setup(FULL); const avatar = new Avatar(root); const spy = vi.fn(); avatar.on(AVATAR_STATUSCHANGE_EVENT, spy); avatar.setStatus('busy'); expect(statusEl(root).dataset.status).toBe('busy'); expect(spy).toHaveBeenCalledOnce(); expect(spy.mock.calls[0][0].detail.status).toBe('busy'); }); it('clears the status when set to null', () => { const root = setup(FULL); const avatar = new Avatar(root, { status: 'away' }); avatar.setStatus(null); expect(statusEl(root).dataset.status).toBeUndefined(); expect(statusEl(root).hasAttribute('aria-label')).toBe(false); }); it('resets to loading on setSrc', () => { const root = setup(FULL); const avatar = new Avatar(root); image(root).dispatchEvent(new Event('load')); expect(root.dataset.state).toBe('loaded'); avatar.setSrc('other.png'); expect(root.dataset.state).toBe('loading'); expect(image(root).getAttribute('src')).toBe('other.png'); }); it('sets role and aria-label from the name', () => { const root = setup(FULL); new Avatar(root, { name: 'Jane Doe' }); expect(root.getAttribute('role')).toBe('img'); expect(root.getAttribute('aria-label')).toBe('Jane Doe'); }); it('throws when neither image nor fallback is present', () => { const root = setup('
'); expect(() => new Avatar(root)).toThrow(/\[42\/avatar\]/); }); it('removes listeners on destroy', () => { const root = setup(FULL); const avatar = new Avatar(root); const spy = vi.fn(); avatar.on(AVATAR_LOAD_EVENT, spy); avatar.destroy(); image(root).dispatchEvent(new Event('load')); expect(spy).not.toHaveBeenCalled(); }); });