ca34d05377
Установщик падал на 'TLS_ENABLED: unbound variable': расчёт LIVEKIT_PUBLIC_URL стоял выше определения TLS_ENABLED. Значение перенесено после блока TLS, dry-run проходит.
223 lines
8.3 KiB
TypeScript
223 lines
8.3 KiB
TypeScript
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('Кэрол присоединился к серверу');
|
||
});
|
||
});
|
||
});
|