239 lines
7.7 KiB
TypeScript
239 lines
7.7 KiB
TypeScript
|
|
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();
|
||
|
|
});
|
||
|
|
});
|