Files
glchat/web/tests/systemMessages.test.tsx
T
grendervill ca34d05377 fix(install): публичный адрес LiveKit вычисляется после TLS_ENABLED
Установщик падал на 'TLS_ENABLED: unbound variable': расчёт LIVEKIT_PUBLIC_URL
стоял выше определения TLS_ENABLED. Значение перенесено после блока TLS,
dry-run проходит.
2026-09-20 18:13:04 +03:00

223 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('Кэрол присоединился к серверу');
});
});
});