import { beforeEach, describe, expect, it, vi } from 'vitest'; import { act, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { QueryClientProvider } from '@tanstack/react-query'; vi.mock('livekit-client', async () => { const fake = await import('./fakeLiveKit'); return { Room: fake.FakeRoom, RoomEvent: fake.RoomEvent, ConnectionQuality: fake.ConnectionQuality, Track: fake.Track, VideoQuality: fake.VideoQuality, }; }); import { FakeRoom, VideoQuality } from './fakeLiveKit'; import { createTestQueryClient, installFetch, json, makeUser, resetStores, type FetchRoute, } from './helpers'; import { makeVoiceState, voiceRoutes } from './voiceRoutes'; import type { Channel } from '@/api/types'; import { VoiceRoomPanel } from '@/components/voice/VoiceRoomPanel'; import { setQueryClient } from '@/lib/queryClient'; import { resetSoundboardEngine } from '@/lib/soundboard'; import { useSessionStore } from '@/stores/session'; import { useVoiceStore } from '@/stores/voice'; import { useVoiceConnectionStore } from '@/stores/voiceConnection'; /** * Статистика своей публикации, выбор качества видео и пресетов демонстрации * экрана (AGENT.md 7.14). Значения берутся из отчёта WebRTC подставного SDK, * опрос идёт раз в секунду и останавливается при закрытии попапа. */ const voiceChannel: Channel = { id: 'c-voice', name: 'Голосовая', type: 'voice', position: 0 }; /** Отчёт WebRTC: кодек, отправка видео, обратный канал (RTT, потери). */ function statsReport(bytesSent: number, timestamp: number): [string, unknown][] { return [ ['codec-1', { type: 'codec', id: 'codec-1', mimeType: 'video/VP8' }], [ 'out-1', { type: 'outbound-rtp', kind: 'video', frameWidth: 1920, frameHeight: 1080, framesPerSecond: 30, bytesSent, packetsSent: 197, codecId: 'codec-1', timestamp, }, ], [ 'in-1', { type: 'remote-inbound-rtp', kind: 'video', roundTripTime: 0.042, jitter: 0.003, packetsLost: 3, }, ], ]; } /** Рендер панели комнаты с поднятой голосовой сессией и включённой камерой. */ async function bootPanel() { const queryClient = createTestQueryClient(); setQueryClient(queryClient); resetStores(); resetSoundboardEngine(); useSessionStore.setState({ 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, member_count: 1, my_role_ids: [], my_permissions: ['VIEW_GUILD'], channels: [voiceChannel], emojis: [], sounds: [], }, ], voiceStates: { 'g-1': [makeVoiceState({ user_id: 'user-1', channel_id: 'c-voice' })] }, }); const voice = voiceRoutes({ channelIds: ['c-voice'], states: [makeVoiceState({ user_id: 'user-1', channel_id: 'c-voice' })], }); installFetch([ { match: '/api/v1/users/@me', response: () => json({ user: makeUser() }) }, { match: '/api/v1/meta', response: () => json({ features: { voice_enabled: true } }) }, { match: '/api/v1/guilds/g-1/members', response: () => json({ members: [] }) }, { match: '/api/v1/guilds/g-1/roles', response: () => json({ roles: [] }) }, ...voice.routes, ] as FetchRoute[]); render( , ); await act(async () => { await useVoiceConnectionStore.getState().join('g-1', 'c-voice'); await useVoiceConnectionStore.getState().setCameraEnabled(true); }); return { room: FakeRoom.latest() }; } beforeEach(() => { FakeRoom.reset(); }); describe('статистика соединения', () => { it('показывает данные SDK и обновляет их раз в секунду', async () => { const { room } = await bootPanel(); room.setStats(statsReport(0, 1_000)); await userEvent.click(await screen.findByTestId('voice-stats-button')); const panel = await screen.findByTestId('voice-stats-panel'); // Разрешение, fps и кодек приходят из отчёта; битрейт — по двум замерам. expect(panel).toHaveTextContent('1920×1080'); expect(panel).toHaveTextContent('30 fps'); expect(panel).toHaveTextContent('VP8'); expect(panel).toHaveTextContent('42 мс'); expect(panel).toHaveTextContent('3 мс'); expect(panel).toHaveTextContent('1.5%'); room.setStats(statsReport(125_000, 2_000)); await waitFor( () => { expect(screen.getByTestId('voice-stats-panel')).toHaveTextContent('1000 кбит/с'); }, { timeout: 2_500 }, ); }); it('показывает прочерк, когда SDK не отдал поле', async () => { const { room } = await bootPanel(); room.setStats([]); await userEvent.click(await screen.findByTestId('voice-stats-button')); const panel = await screen.findByTestId('voice-stats-panel'); expect(within(panel).getAllByText('—').length).toBeGreaterThanOrEqual(5); }); it('останавливает опрос при закрытии попапа', async () => { const { room } = await bootPanel(); room.setStats(statsReport(0, 1_000)); await userEvent.click(await screen.findByTestId('voice-stats-button')); await screen.findByTestId('voice-stats-panel'); const track = room.localParticipant.getTrackPublication('camera')?.track; const before = track?.statsRequests ?? 0; expect(before).toBeGreaterThan(0); await userEvent.click(screen.getByTestId('voice-stats-button')); await waitFor(() => { expect(screen.queryByTestId('voice-stats-panel')).toBeNull(); }); // Ждём больше периода опроса: счётчик обращений к SDK не должен расти. await new Promise((resolve) => setTimeout(resolve, 1_200)); expect(track?.statsRequests).toBe(before); }); it('передаёт SDK выбранное качество публикации', async () => { const { room } = await bootPanel(); await userEvent.click(await screen.findByTestId('voice-quality-button')); await userEvent.click(await screen.findByTestId('voice-publish-quality-low')); await waitFor(() => { expect(useVoiceStore.getState().videoQuality).toBe('low'); }); const track = room.localParticipant.getTrackPublication('camera')?.track; expect(track?.publishingQuality).toContain(VideoQuality.LOW); }); it('публикует экран с выбранным разрешением и fps', async () => { const { room } = await bootPanel(); await userEvent.click(await screen.findByTestId('voice-screen-button')); await userEvent.click(await screen.findByTestId('voice-screen-preset-720p30')); await waitFor(() => { expect(useVoiceConnectionStore.getState().screenEnabled).toBe(true); }); expect(useVoiceStore.getState().screenSharePreset).toBe('720p30'); expect(room.localParticipant.screenOptions.at(-1)).toEqual({ enabled: true, options: { resolution: { width: 1280, height: 720, frameRate: 30 } }, }); }); });