import { describe, it, expect, vi, afterEach } from 'vitest'; import '@testing-library/jest-dom/vitest'; import { render, cleanup, fireEvent } from '@solidjs/testing-library'; import { MessageActionBar, MessageAvatar } from './message'; import { actionIcon, BUILTIN_ACTION_LABEL } from '../ui/action-icons'; import type { ChatMessageAction, CustomAction } from '../elements/chat-types'; afterEach(cleanup); describe('action-icons registry', () => { it('resolves every built-in action name to a component', () => { (['copy', 'like', 'dislike', 'regenerate', 'edit'] as ChatMessageAction[]).forEach((n) => { expect(actionIcon(n)).toBeTypeOf('function'); }); }); it('resolves common custom icon names', () => { ['share', 'bookmark', 'download', 'link', 'trash', 'check', 'x', 'star', 'flag', 'reply', 'more'].forEach((n) => { expect(actionIcon(n)).toBeTypeOf('function'); }); }); it('returns undefined for unknown or absent names', () => { expect(actionIcon('not-a-real-icon')).toBeUndefined(); expect(actionIcon(undefined)).toBeUndefined(); expect(actionIcon('')).toBeUndefined(); }); it('exposes labels for all built-ins', () => { expect(BUILTIN_ACTION_LABEL).toMatchObject({ copy: 'Copy', like: 'Like', dislike: 'Dislike', regenerate: 'Regenerate', edit: 'Edit', }); }); }); describe('MessageActionBar', () => { it('renders built-in actions and emits their name on click', () => { const onAction = vi.fn(); const { getByLabelText } = render(() => ( )); const copy = getByLabelText('Copy'); expect(copy).toBeInTheDocument(); expect(copy).toHaveAttribute('data-action', 'copy'); // built-in renders an icon (svg), not the label text expect(copy.querySelector('svg')).toBeTruthy(); fireEvent.click(copy); expect(onAction).toHaveBeenCalledWith('copy'); fireEvent.click(getByLabelText('Regenerate')); expect(onAction).toHaveBeenCalledWith('regenerate'); }); it('renders a custom action with a known icon and emits its id', () => { const onAction = vi.fn(); const share: CustomAction = { id: 'share', label: 'Share', icon: 'share' }; const { getByLabelText } = render(() => ( )); const btn = getByLabelText('Share'); expect(btn).toHaveAttribute('data-action', 'share'); expect(btn.querySelector('svg')).toBeTruthy(); fireEvent.click(btn); expect(onAction).toHaveBeenCalledWith('share'); }); it('renders a label-only button for an unknown custom icon (no crash)', () => { const onAction = vi.fn(); const custom: CustomAction = { id: 'archive', label: 'Archive', icon: 'definitely-missing' }; const { getByLabelText } = render(() => ( )); const btn = getByLabelText('Archive'); expect(btn).toBeInTheDocument(); expect(btn.querySelector('svg')).toBeFalsy(); expect(btn).toHaveTextContent('Archive'); fireEvent.click(btn); expect(onAction).toHaveBeenCalledWith('archive'); }); it('renders label-only when a custom action has no icon', () => { const onAction = vi.fn(); const { getByLabelText } = render(() => ( )); const btn = getByLabelText('Mute'); expect(btn.querySelector('svg')).toBeFalsy(); expect(btn).toHaveTextContent('Mute'); }); it('reveal="hover" adds the opacity/group-hover classes', () => { const { container } = render(() => ( {}} /> )); const bar = container.firstElementChild as HTMLElement; expect(bar.className).toContain('opacity-0'); expect(bar.className).toContain('group-hover:opacity-100'); }); it('reveal="always" (default) does not add the hover classes', () => { const { container } = render(() => ( {}} /> )); const bar = container.firstElementChild as HTMLElement; expect(bar.className).not.toContain('opacity-0'); expect(bar.className).not.toContain('group-hover:opacity-100'); }); }); describe('MessageAvatar', () => { it('renders an img when src is set', () => { const { container } = render(() => ( )); const img = container.querySelector('img'); expect(img).toBeTruthy(); expect(img).toHaveAttribute('src', 'https://example.com/a.png'); expect(img).toHaveAttribute('alt', 'Ada'); }); it('renders the fallback text when there is no src', () => { const { container, getByText } = render(() => ( )); expect(container.querySelector('img')).toBeFalsy(); expect(getByText('AD')).toBeInTheDocument(); }); });