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

239 lines
7.7 KiB
TypeScript
Raw Normal View History

import { describe, expect, it } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { Relationships } from '@/api/friends';
import {
findRequest,
installFetch,
installGatewaySocket,
json,
makeReadySnapshot,
makeUser,
renderApp,
type FetchRoute,
} from './helpers';
/**
* Групповые личные беседы (AGENT.md 7.8, Фаза 7): отображение в сайдбаре,
* создание из списка друзей и шапка беседы с числом участников.
*/
const user = makeUser();
const instance = {
instance: {
name: 'glchat-test',
registration_enabled: true,
allow_guild_creation: true,
max_guilds_per_user: 5,
main_guild_id: 'g-main',
},
};
const friends: Relationships = {
friends: [
{
user_id: 'user-2',
username: 'bob',
display_name: 'Боб',
status: 'online',
visible_status: 'online',
is_instance_admin: false,
badges: [],
timezone: 'Europe/Moscow',
relationship: 'friend',
},
{
user_id: 'user-3',
username: 'carol',
display_name: 'Кэрол',
status: 'offline',
visible_status: 'offline',
is_instance_admin: false,
badges: [],
timezone: 'Europe/Moscow',
relationship: 'friend',
},
],
incoming: [],
outgoing: [],
blocked: [],
};
/** Групповая беседа в REST-форме (`GET /users/@me/channels`). */
function groupChannel(id = 'dm-group-1', memberCount = 3) {
return {
id,
type: 'dm',
is_group: true,
name: 'Игровая',
member_count: memberCount,
owner_id: user.id,
can_send: true,
can_view: true,
recipients: [
{
user_id: user.id,
username: 'alice',
display_name: 'Alice',
status: 'online',
visible_status: 'online',
is_owner: true,
is_self: true,
},
{
user_id: 'user-2',
username: 'bob',
display_name: 'Боб',
status: 'online',
visible_status: 'online',
is_owner: false,
is_self: false,
},
{
user_id: 'user-3',
username: 'carol',
display_name: 'Кэрол',
status: 'offline',
visible_status: 'offline',
is_owner: false,
is_self: false,
},
],
};
}
function baseRoutes(channels: unknown[], extra: FetchRoute[] = []): FetchRoute[] {
return [
...extra,
{ match: '/api/v1/users/@me/relationships', response: () => json(friends) },
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) },
{ match: '/api/v1/users/@me/channels', response: () => json({ channels }) },
{ match: '/api/v1/users/@me', response: () => json({ user }) },
{ match: '/api/v1/instance', response: () => json(instance) },
];
}
describe('групповые беседы', () => {
it('группа показана в сайдбаре с числом участников и без точки статуса', async () => {
const channel = groupChannel();
installFetch(
baseRoutes(
[channel],
[
{
match: `/api/v1/channels/${channel.id}/messages`,
response: () => json({ messages: [] }),
},
{ match: `/api/v1/channels/${channel.id}/pins`, response: () => json({ messages: [] }) },
{
match: `/api/v1/channels/${channel.id}/ack`,
method: 'POST',
response: () => json({ ok: true }),
},
],
),
);
const gateway = installGatewaySocket(makeReadySnapshot());
// Сайдбар раздела «Друзья» живёт в оболочке приложения: открываем беседу,
// чтобы он отрисовался (как в реальном переходе).
renderApp(`/app/friends/${channel.id}`);
await gateway.greet();
const link = await screen.findByTestId('dm-link-dm-group-1');
expect(link).toHaveTextContent('Игровая');
expect(screen.getByTestId('dm-members-dm-group-1')).toHaveTextContent('3');
// У группы нет статуса собеседника: точки рядом с аватаром быть не должно.
expect(screen.queryByTestId('dm-status-dm-group-1')).toBeNull();
});
it('шапка групповой беседы показывает число участников', async () => {
const channel = groupChannel();
installFetch(
baseRoutes(
[channel],
[
{
match: `/api/v1/channels/${channel.id}/messages`,
response: () => json({ messages: [] }),
},
{ match: `/api/v1/channels/${channel.id}/pins`, response: () => json({ messages: [] }) },
{
match: `/api/v1/channels/${channel.id}/ack`,
method: 'POST',
response: () => json({ ok: true }),
},
],
),
);
const gateway = installGatewaySocket(makeReadySnapshot());
renderApp(`/app/friends/${channel.id}`);
await gateway.greet();
expect(await screen.findByTestId('dm-group-header')).toBeVisible();
expect(screen.getByTestId('dm-member-count')).toHaveTextContent('Участников: 3');
});
it('создаёт группу из двух друзей и открывает её', async () => {
const created = groupChannel('dm-group-2');
const fetchMock = installFetch(
baseRoutes(
[],
[
{
match: '/api/v1/users/@me/channels/group',
method: 'POST',
response: () => json({ channel: created }),
},
],
),
);
const gateway = installGatewaySocket(makeReadySnapshot());
const visitor = userEvent.setup();
const { router } = renderApp('/app/friends');
await gateway.greet();
await screen.findByTestId('dm-sidebar-empty');
await visitor.click(await screen.findByTestId('group-dm-open'));
await visitor.type(screen.getByLabelText('Название группы'), 'Игровая');
await visitor.click(screen.getByRole('checkbox', { name: 'Боб' }));
await visitor.click(screen.getByRole('checkbox', { name: 'Кэрол' }));
const submit = screen.getByRole('button', { name: 'Создать' });
// С одним участником группа не создаётся: кнопка включается со второго.
expect(submit).toBeEnabled();
await visitor.click(submit);
await waitFor(() => {
expect(findRequest(fetchMock, { url: '/channels/group', method: 'POST' })).toBeDefined();
});
const request = findRequest(fetchMock, { url: '/channels/group', method: 'POST' });
expect(request?.body).toEqual({
name: 'Игровая',
recipient_ids: ['user-2', 'user-3'],
});
await waitFor(() => {
expect(router.state.location.pathname).toBe('/app/friends/dm-group-2');
});
// Беседа сразу попала в список — отдельный запрос не нужен.
expect(await screen.findByTestId('dm-link-dm-group-2')).toBeVisible();
});
it('без двух выбранных друзей кнопка создания выключена', async () => {
installFetch(baseRoutes([]));
const gateway = installGatewaySocket(makeReadySnapshot());
const visitor = userEvent.setup();
renderApp('/app/friends');
await gateway.greet();
await visitor.click(await screen.findByTestId('group-dm-open'));
await visitor.click(screen.getByRole('checkbox', { name: 'Боб' }));
expect(screen.getByRole('button', { name: 'Создать' })).toBeDisabled();
});
});