From cc16c18b1336a821e989dd4c1c0b39e79bc159de Mon Sep 17 00:00:00 2001 From: grendervill Date: Sun, 20 Sep 2026 22:30:24 +0300 Subject: [PATCH] =?UTF-8?q?fix(web):=20=D0=BF=D0=B8=D0=BD=D0=B3=20=D1=83?= =?UTF-8?q?=D1=87=D0=B0=D1=81=D1=82=D0=BD=D0=B8=D0=BA=D0=B0=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=B7=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Постоянная подпись в плитке занимала место и была не нужна: задержка теперь живёт в подсказке индикатора связи («Отличное соединение, 46 мс — задержка до голосового сервера»), а плитка осталась компактной. Тесты и e2e проверяют `title`/`aria-label` индикатора вместо видимого текста. --- web/e2e/voice.spec.ts | 14 ++++----- web/src/components/voice/VoiceIndicators.tsx | 31 ++------------------ web/src/components/voice/VoiceRoomPanel.tsx | 4 +-- web/src/i18n/locales/en.json | 2 +- web/src/i18n/locales/ru.json | 2 +- web/tests/voice.test.tsx | 26 +++++++++------- 6 files changed, 29 insertions(+), 50 deletions(-) diff --git a/web/e2e/voice.spec.ts b/web/e2e/voice.spec.ts index d795ffc..31197cd 100644 --- a/web/e2e/voice.spec.ts +++ b/web/e2e/voice.spec.ts @@ -340,14 +340,14 @@ test.describe('голос: два клиента в одной комнате', await expect(session.page.getByTestId(`voice-tile-${fixture.guestId}`)).toBeVisible(); } - // Пинг участника виден в плитке и обновляется: своё значение измеряется - // по статистике WebRTC, чужое приходит по data-каналу LiveKit. + // Пинг участника показывается при наведении на индикатор связи: своё + // значение измеряется по статистике WebRTC, чужое приходит по data-каналу. for (const session of [adminSession, guestSession]) { - const ownLabel = session.page + const ownQuality = session.page .getByTestId(`voice-tile-${session === adminSession ? fixture.adminId : fixture.guestId}`) - .getByTestId('voice-quality-label'); + .getByTestId('voice-quality'); await expect - .poll(async () => (await ownLabel.textContent()) ?? '', { timeout: 30_000 }) + .poll(async () => (await ownQuality.getAttribute('title')) ?? '', { timeout: 30_000 }) .toMatch(/\d+ мс/u); } // Каждый видит пинг другого участника. @@ -356,8 +356,8 @@ test.describe('голос: два клиента в одной комнате', async () => (await adminSession.page .getByTestId(`voice-tile-${fixture.guestId}`) - .getByTestId('voice-quality-label') - .textContent()) ?? '', + .getByTestId('voice-quality') + .getAttribute('title')) ?? '', { timeout: 30_000 }, ) .toMatch(/\d+ мс/u); diff --git a/web/src/components/voice/VoiceIndicators.tsx b/web/src/components/voice/VoiceIndicators.tsx index 4e75716..b1ffb6e 100644 --- a/web/src/components/voice/VoiceIndicators.tsx +++ b/web/src/components/voice/VoiceIndicators.tsx @@ -28,7 +28,8 @@ export function VoiceQualityDot({ if (quality === null) { return null; } - const label = qualityLabel(t, quality, latencyMs); + // Пинг показываем только в подсказке: в плитке он не занимает место. + const label = `${qualityLabel(t, quality, latencyMs)} — ${t('voice.latencyHint')}`; return ( - - {qualityLabel(t, quality, latencyMs)} - - ); -} - /** * Иконка состояния участника: перечёркнутая, когда флаг выключен. * Подпись обязательна — по ней состояние читают и тесты, и скринридер. diff --git a/web/src/components/voice/VoiceRoomPanel.tsx b/web/src/components/voice/VoiceRoomPanel.tsx index b31a028..1e0121b 100644 --- a/web/src/components/voice/VoiceRoomPanel.tsx +++ b/web/src/components/voice/VoiceRoomPanel.tsx @@ -6,7 +6,7 @@ import { Avatar } from '@/components/ui/Avatar'; import { Menu, MenuGroupLabel, MenuItem } from '@/components/ui/Menu'; import { Card, Button } from '@/components/ui/primitives'; import { RemoteAudio, RemoteVideo } from '@/components/voice/RemoteAudio'; -import { VoiceFlags, VoiceQualityBadge } from '@/components/voice/VoiceIndicators'; +import { VoiceFlags, VoiceQualityDot } from '@/components/voice/VoiceIndicators'; import { VoiceOccupantMenu } from '@/components/voice/VoiceOccupantMenu'; import { VoiceSoundboard } from '@/components/voice/VoiceSoundboard'; import { VoiceStatsButton } from '@/components/voice/VoiceStatsButton'; @@ -110,7 +110,7 @@ function VoiceTile({ {occupant.name} - + {occupant.isLocal ? ( <> {/* Статистика своей публикации: разрешение, fps, кодек, канал (7.14). */} diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 5492ed8..b643bd2 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -346,7 +346,7 @@ } }, "latency": "{{ms}} ms", - "latencyHint": "Participant ping to the instance", + "latencyHint": "latency to the voice server", "latencyUnknown": "ping unknown" }, "friends": { diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 01e6e52..204c896 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -346,7 +346,7 @@ } }, "latency": "{{ms}} мс", - "latencyHint": "Пинг участника до инстанса", + "latencyHint": "задержка до голосового сервера", "latencyUnknown": "пинг неизвестен" }, "friends": { diff --git a/web/tests/voice.test.tsx b/web/tests/voice.test.tsx index 4e3b0b1..dbb44a8 100644 --- a/web/tests/voice.test.tsx +++ b/web/tests/voice.test.tsx @@ -198,7 +198,7 @@ describe('голосовая комната: вход', () => { expect(voice.joinCount()).toBe(0); }); - it('показывает пинг участника рядом с качеством связи', { timeout: 20_000 }, async () => { + it('показывает пинг участника в подсказке индикатора связи', { timeout: 20_000 }, async () => { await bootVoice({ seated: [{ user_id: 'user-2', channel_id: 'c-voice' }] }); await clickChannel('Голосовая'); @@ -211,19 +211,24 @@ describe('голосовая комната: вход', () => { ['pair', { type: 'candidate-pair', state: 'succeeded', currentRoundTripTime: 0.042 }], ]); // Первый такт может прийтись на момент, когда микрофон ещё не опубликован, - // поэтому ждём следующего (интервал публикации пинга — секунда). + // поэтому ждём следующего (интервал публикации пинга — секунда). Пинг виден + // только при наведении: он попадает в подсказку индикатора связи. + const ownQuality = () => + within(screen.getByTestId('voice-tile-user-1')).getByTestId('voice-quality'); await waitFor( () => { - expect( - within(screen.getByTestId('voice-tile-user-1')).getByTestId('voice-quality-label'), - ).toHaveTextContent('42 мс'); + expect(ownQuality()).toHaveAttribute( + 'title', + expect.stringContaining('Отличное соединение, 42 мс'), + ); }, { timeout: 5_000 }, ); - const ownLabel = within(screen.getByTestId('voice-tile-user-1')).getByTestId( - 'voice-quality-label', + expect(ownQuality()).toHaveAttribute('data-latency', '42'); + expect(ownQuality()).toHaveAttribute( + 'aria-label', + expect.stringContaining('задержка до голосового сервера'), ); - expect(ownLabel).toHaveTextContent('Отличное соединение, 42 мс'); // Второй участник подключается к комнате и присылает свой пинг. const remote = room.addRemote('user-2', 'Bob'); @@ -237,8 +242,9 @@ describe('голосовая комната: вход', () => { const remoteTile = await screen.findByTestId('voice-tile-user-2'); await waitFor( () => { - expect(within(remoteTile).getByTestId('voice-quality-label')).toHaveTextContent( - 'Отличное соединение, 128 мс', + expect(within(remoteTile).getByTestId('voice-quality')).toHaveAttribute( + 'title', + expect.stringContaining('Отличное соединение, 128 мс'), ); }, { timeout: 5_000 },