diff --git a/frontend/src/lib/shared/primitives/Avatar.svelte b/frontend/src/lib/shared/primitives/Avatar.svelte new file mode 100644 index 00000000..a7f25870 --- /dev/null +++ b/frontend/src/lib/shared/primitives/Avatar.svelte @@ -0,0 +1,94 @@ + + +{#if overflow !== undefined} + + + +{overflow} + +{:else if placeholder} + + +{:else if decorative} + + +{:else} + + + {avatar.initials} + +{/if} diff --git a/frontend/src/lib/shared/primitives/Avatar.svelte.spec.ts b/frontend/src/lib/shared/primitives/Avatar.svelte.spec.ts new file mode 100644 index 00000000..243db6bc --- /dev/null +++ b/frontend/src/lib/shared/primitives/Avatar.svelte.spec.ts @@ -0,0 +1,105 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +import Avatar from './Avatar.svelte'; + +afterEach(() => cleanup()); + +function getAvatar(): HTMLElement | null { + return document.querySelector('[data-testid="avatar"]'); +} + +describe('Avatar', () => { + it('renders initials from name', async () => { + render(Avatar, { name: 'Anna Raddatz' }); + await expect.element(page.getByText('AR')).toBeInTheDocument(); + }); + + it('applies size-26 class for size=26', async () => { + render(Avatar, { name: 'Anna Raddatz', size: 26 }); + const el = getAvatar(); + expect(el).not.toBeNull(); + expect(el!.className).toMatch(/h-\[26px\]/); + expect(el!.className).toMatch(/w-\[26px\]/); + }); + + it('applies size-28 class for size=28', async () => { + render(Avatar, { name: 'Karl Müller', size: 28 }); + const el = getAvatar(); + expect(el).not.toBeNull(); + expect(el!.className).toMatch(/h-\[28px\]/); + expect(el!.className).toMatch(/w-\[28px\]/); + }); + + it('applies size-40 class for size=40', async () => { + render(Avatar, { name: 'Karl Müller', size: 40 }); + const el = getAvatar(); + expect(el).not.toBeNull(); + expect(el!.className).toMatch(/h-\[40px\]/); + expect(el!.className).toMatch(/w-\[40px\]/); + }); + + it('applies size-48 class for size=48', async () => { + render(Avatar, { name: 'Herbert Cram', size: 48 }); + const el = getAvatar(); + expect(el).not.toBeNull(); + expect(el!.className).toMatch(/h-\[48px\]/); + expect(el!.className).toMatch(/w-\[48px\]/); + }); + + it('stacked variant adds -ml-1.5 and ring-2 ring-surface', async () => { + render(Avatar, { name: 'Anna Raddatz', size: 26, stacked: true }); + const el = getAvatar(); + expect(el).not.toBeNull(); + expect(el!.className).toMatch(/-ml-1\.5/); + expect(el!.className).toMatch(/ring-2/); + expect(el!.className).toMatch(/ring-surface/); + }); + + it('non-stacked variant does not have -ml-1.5', async () => { + render(Avatar, { name: 'Anna Raddatz', size: 26, stacked: false }); + const el = getAvatar(); + expect(el).not.toBeNull(); + expect(el!.className).not.toMatch(/-ml-1\.5/); + }); + + it('decorative mode: aria-hidden=true when decorative prop is true', async () => { + render(Avatar, { name: 'Anna Raddatz', decorative: true }); + const el = getAvatar(); + expect(el).not.toBeNull(); + expect(el!.getAttribute('aria-hidden')).toBe('true'); + }); + + it('standalone mode: role=img and aria-label=name when decorative is false', async () => { + render(Avatar, { name: 'Herbert Cram', decorative: false }); + await expect.element(page.getByRole('img', { name: 'Herbert Cram' })).toBeInTheDocument(); + }); + + it('renders overflow +N chip when overflow prop is set', async () => { + render(Avatar, { overflow: 3 }); + await expect.element(page.getByText('+3')).toBeInTheDocument(); + }); + + it('renders placeholder (dashed border) when placeholder prop is true', async () => { + render(Avatar, { placeholder: true, size: 26 }); + const el = getAvatar(); + expect(el).not.toBeNull(); + expect(el!.className).toMatch(/border-dashed/); + }); + + it('has rounded-full class', async () => { + render(Avatar, { name: 'Karl Müller', size: 40 }); + const el = getAvatar(); + expect(el).not.toBeNull(); + expect(el!.className).toMatch(/rounded-full/); + }); + + it('stacked variant carries ring classes (dark mode edge on dark surface)', async () => { + render(Avatar, { name: 'Karl Müller', size: 26, stacked: true }); + const el = getAvatar(); + expect(el).not.toBeNull(); + // ring-2 ring-surface keeps avatar edges visible on dark surface (#011526) + expect(el!.className).toMatch(/ring/); + }); +});