import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { SystemMessage } from './SystemMessage'; import type { DmChannel, MessageWithUser, User } from '@backspace/shared'; // SpaceInviteCard is unrelated to the cases under test but is imported by // SystemMessage; stub its store hooks so the import graph resolves cleanly. vi.mock('../../stores/spaceStore', () => ({ useSpaceStore: (selector: (s: unknown) => unknown) => selector({ joinByCode: vi.fn() }), getApiForOrigin: vi.fn(), })); vi.mock('../../api/client', () => ({ api: {}, createApiClient: vi.fn(), })); const actor: User = { id: 'U1', username: 'heidi', displayName: 'Heidi', avatar: null, banner: null, accentColor: null, avatarColor: 'mint', bio: null, status: 'online', customStatus: null, isAdmin: false, createdAt: 0, homeUserId: null, homeInstance: null, replicatedInstances: [], }; function buildMessage(content: object, userId = 'U1'): MessageWithUser { return { id: 'M1', channelId: '', userId, user: actor, content: JSON.stringify(content), type: 'system', createdAt: 1, editedAt: null, replyToId: null, replyTo: null, attachments: [], embeds: [], reactions: [], mentions: [], everyoneMentioned: false, pinnedAt: null, } as unknown as MessageWithUser; } const dm: Pick = { members: [actor] }; function renderSM(message: MessageWithUser, dmArg: Pick | null) { return render( , ); } describe('SystemMessage — name_changed', () => { it('newName="Cool Group" with resolvable actor → "✎ Heidi renamed the group to \\"Cool Group\\""', () => { const msg = buildMessage({ event: 'name_changed', oldName: null, newName: 'Cool Group' }); renderSM(msg, dm); expect(screen.getByText('✎')).toBeDefined(); expect(screen.getByText(/Heidi renamed the group to "Cool Group"/)).toBeDefined(); }); it('newName=null (cleared) with resolvable actor → "✎ Heidi cleared the group name"', () => { const msg = buildMessage({ event: 'name_changed', oldName: 'Old', newName: null }); renderSM(msg, dm); expect(screen.getByText('✎')).toBeDefined(); expect(screen.getByText(/Heidi cleared the group name/)).toBeDefined(); }); it('unresolvable actor (member missing from roster) → "✎ Unknown renamed …"', () => { const msg = buildMessage({ event: 'name_changed', oldName: null, newName: 'X' }, 'GHOST'); renderSM(msg, dm); // dm.members has only U1, not GHOST expect(screen.getByText(/Unknown renamed the group to "X"/)).toBeDefined(); }); }); describe('SystemMessage — icon_changed', () => { it('resolvable actor → "🖼 Heidi updated the group icon"', () => { const msg = buildMessage({ event: 'icon_changed' }); renderSM(msg, dm); // The 🖼 character is U+1F5BC (FRAME WITH PICTURE), not 🖼️ (with VS-16). expect(screen.getByText('\u{1F5BC}')).toBeDefined(); expect(screen.getByText(/Heidi updated the group icon/)).toBeDefined(); }); }); describe('SystemMessage — message_pinned (space channel, no roster)', () => { it('names the actor from message.user and offers both links', async () => { const { useUIStore } = await import('../../stores/uiStore'); const msg = buildMessage({ event: 'message_pinned', messageId: 'M9' }); renderSM(msg, null); expect(screen.getByText(/Heidi pinned/)).toBeDefined(); const before = useUIStore.getState().pinsPanelRequest; screen.getByText('a message').click(); expect(useUIStore.getState().jumpToMessageRequest).toBe('M9'); screen.getByText('See all pinned messages.').click(); expect(useUIStore.getState().pinsPanelRequest).toBe(before + 1); }); it('falls back to the plain renderer when messageId is missing', () => { // A malformed payload must not throw — it degrades to the generic branch. const msg = buildMessage({ event: 'message_pinned' }); expect(() => renderSM(msg, null)).not.toThrow(); }); });