Files
glchat/web/tests/systemMessages.test.tsx
T

223 lines
8.3 KiB
TypeScript
Raw Normal View History

import { beforeEach, describe, expect, it } from 'vitest';
import { render, screen, waitFor, within } from '@testing-library/react';
import type { Message } from '@/api/messages';
import { MessageList } from '@/components/chat/MessageList';
import type { AuthorDirectory } from '@/components/chat/useAuthorDirectory';
import { parseSystemMessage, systemMessageUserId } from '@/lib/systemMessage';
import { makeMessage, makeUser, json, installFetch, installGatewaySocket, messagesRoutes, renderApp, resetStores } from './helpers';
/**
* Системные сообщения (AGENT.md §7.6): сервер пишет их с `type: "system"`,
* а в `content` — JSON о событии. Лента показывает человекочитаемую строку
* с именем участника, а неизвестное событие и мусорный JSON — как есть.
*/
const authors: AuthorDirectory = {
resolve: (userId) => ({
id: userId ?? '',
name: userId === 'user-2' ? 'Боб' : 'неизвестный',
avatarFileId: undefined,
colorHex: null,
isInstanceAdmin: false,
isMember: userId === 'user-2',
}),
resolveMention: (userId) => ({ name: userId === 'user-2' ? 'Боб' : userId, isMe: false }),
resolveMemberName: (userId) => (userId === 'user-2' ? 'Боб' : null),
loaded: true,
};
function systemMessage(content: string, id = 'm-1'): Message {
return makeMessage({
id,
channel_id: 'c-1',
content,
type: 'system',
});
}
function renderList(messages: Message[]) {
return render(
<MessageList
channelId="c-1"
channelName="общий"
messages={messages}
status="ready"
error={null}
hasMore={false}
loadingMore={false}
unreadAnchorId={null}
highlightId={null}
authors={authors}
currentUserId="user-1"
canManageMessages
canSend
onReload={() => undefined}
onLoadMore={() => undefined}
onReply={() => undefined}
/>,
);
}
beforeEach(() => {
resetStores();
});
describe('системные сообщения: разбор payload', () => {
it('читает события участника и комнаты', () => {
expect(parseSystemMessage('{"event":"member_join","user_id":"123"}')).toEqual({
kind: 'member_join',
userId: '123',
});
expect(parseSystemMessage('{"event":"member_kick","user_id":"123"}')).toEqual({
kind: 'member_kick',
userId: '123',
});
expect(parseSystemMessage('{"event":"channel_created","name":"общий","type":"text"}')).toEqual({
kind: 'channel_created',
name: 'общий',
channelType: 'text',
});
expect(systemMessageUserId('{"event":"member_kick","user_id":"123"}')).toBe('123');
expect(systemMessageUserId('{"event":"channel_created","name":"общий"}')).toBeNull();
});
it('не падает на мусоре и неизвестном событии', () => {
expect(parseSystemMessage('Аля вошла')).toBeNull();
expect(parseSystemMessage('{"event":"guild_boost"}')).toBeNull();
expect(parseSystemMessage('{"event":"member_join"}')).toBeNull();
expect(parseSystemMessage('"строка"')).toBeNull();
expect(parseSystemMessage('null')).toBeNull();
});
});
describe('системные сообщения: лента', () => {
it('member_join — «{имя} присоединился к серверу»', () => {
renderList([systemMessage('{"event":"member_join","user_id":"user-2"}')]);
const body = screen.getByTestId('message-m-1');
expect(body).toHaveTextContent('Боб присоединился к серверу');
// Сырой JSON в ленте не показываем.
expect(body.textContent).not.toContain('member_join');
});
it('member_kick — «{имя} покинул сервер (исключён)»', () => {
renderList([systemMessage('{"event":"member_kick","user_id":"user-2"}')]);
expect(screen.getByTestId('message-m-1')).toHaveTextContent(
'Боб покинул сервер (исключён)',
);
});
it('channel_created — комната и голосовая комната', () => {
renderList([
systemMessage('{"event":"channel_created","name":"общий","type":"text"}', 'm-1'),
systemMessage('{"event":"channel_created","name":"Голосовая","type":"voice"}', 'm-2'),
]);
expect(screen.getByTestId('message-m-1')).toHaveTextContent('Комната #общий создана');
expect(screen.getByTestId('message-m-2')).toHaveTextContent(
'Голосовая комната Голосовая создана',
);
});
it('неизвестное событие и невалидный JSON показываются как есть', () => {
renderList([
systemMessage('{"event":"guild_boost","level":2}', 'm-1'),
systemMessage('Аля вошла', 'm-2'),
]);
expect(screen.getByTestId('message-m-1')).toHaveTextContent('{"event":"guild_boost","level":2}');
expect(screen.getByTestId('message-m-2')).toHaveTextContent('Аля вошла');
});
it('неизвестный участник подписан «участник»', () => {
renderList([systemMessage('{"event":"member_join","user_id":"user-9"}')]);
expect(screen.getByTestId('message-m-1')).toHaveTextContent('участник присоединился к серверу');
});
it('остаётся центрированной строкой без аватара и действий', () => {
renderList([systemMessage('{"event":"member_join","user_id":"user-2"}')]);
const body = screen.getByTestId('message-m-1');
expect(body).toHaveClass('items-center', 'text-center');
expect(body).toHaveAttribute('data-message-type', 'system');
expect(within(body).queryByRole('button')).toBeNull();
expect(screen.getByTestId('message-group').querySelector('.inline-flex.shrink-0')).toBeNull();
});
});
describe('системные сообщения: имя через справочник авторов', () => {
it('добирает имя участника по GET /users/{id}', async () => {
const user = makeUser();
const snapshot = {
user: {
id: 'user-1',
username: 'alice',
display_name: 'Alice',
is_instance_admin: false,
badges: [],
},
guilds: [
{
id: 'g-1',
name: 'Сервер',
owner_id: 'user-1',
is_main: true,
channels: [{ id: 'c-1', name: 'общий', type: 'text', position: 0 }],
roles: [],
member_ids: ['user-1'],
my_role_ids: [],
my_permissions: ['SEND_MESSAGES'],
emojis: [],
},
],
dm_channels: [],
read_states: [],
session_id: 'session-1',
heartbeat_interval_ms: 45_000,
};
installFetch([
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) },
{ match: '/api/v1/users/@me', response: () => json({ user }) },
{ match: '/api/v1/instance', response: () => json({ instance: { name: 'glchat-test' } }) },
// Участника нет в списке сервера — имя приходит из GET /users/{id}.
{ match: '/api/v1/guilds/g-1/members', response: () => json({ members: [] }) },
{ match: '/api/v1/guilds/g-1/roles', response: () => json({ roles: [] }) },
{
match: '/api/v1/users/user-9',
response: () =>
json({
user: {
id: 'user-9',
username: 'carol',
display_name: 'Кэрол',
is_instance_admin: false,
badges: [],
},
}),
},
{ match: '/api/v1/meta', response: () => json({ max_upload_size: 26_214_400 }) },
...messagesRoutes('c-1', [
makeMessage({
id: 'm-1',
channel_id: 'c-1',
type: 'system',
content: '{"event":"member_join","user_id":"user-9"}',
}),
]),
]);
const gateway = installGatewaySocket(snapshot);
renderApp('/app/g-1/c-1');
await screen.findByTestId('guild-rail');
await gateway.greet();
await waitFor(() => {
expect(screen.getByTestId('message-m-1')).toHaveTextContent('Кэрол присоединился к серверу');
});
});
});