fix(web): пинг участника показывается только при наведении
Постоянная подпись в плитке занимала место и была не нужна: задержка теперь живёт в подсказке индикатора связи («Отличное соединение, 46 мс — задержка до голосового сервера»), а плитка осталась компактной. Тесты и e2e проверяют `title`/`aria-label` индикатора вместо видимого текста.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
<span
|
||||
role="img"
|
||||
@@ -42,34 +43,6 @@ export function VoiceQualityDot({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Точка качества с видимой подписью: «Отличное соединение, 42 мс» в плитке
|
||||
* участника. Пинг обновляется раз в секунду вместе с состоянием связи.
|
||||
*/
|
||||
export function VoiceQualityBadge({
|
||||
quality,
|
||||
latencyMs,
|
||||
}: {
|
||||
quality: VoiceQuality | null;
|
||||
latencyMs?: number | undefined;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (quality === null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 text-xs text-fg-muted"
|
||||
data-testid="voice-quality-label"
|
||||
data-latency={latencyMs ?? ''}
|
||||
title={t('voice.latencyHint')}
|
||||
>
|
||||
<VoiceQualityDot quality={quality} latencyMs={latencyMs} />
|
||||
<span className="tabular-nums">{qualityLabel(t, quality, latencyMs)}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Иконка состояния участника: перечёркнутая, когда флаг выключен.
|
||||
* Подпись обязательна — по ней состояние читают и тесты, и скринридер.
|
||||
|
||||
@@ -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({
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium" title={occupant.name}>
|
||||
{occupant.name}
|
||||
</span>
|
||||
<VoiceQualityBadge quality={occupant.quality} latencyMs={latencies[occupant.userId]} />
|
||||
<VoiceQualityDot quality={occupant.quality} latencyMs={latencies[occupant.userId]} />
|
||||
{occupant.isLocal ? (
|
||||
<>
|
||||
{/* Статистика своей публикации: разрешение, fps, кодек, канал (7.14). */}
|
||||
|
||||
@@ -346,7 +346,7 @@
|
||||
}
|
||||
},
|
||||
"latency": "{{ms}} ms",
|
||||
"latencyHint": "Participant ping to the instance",
|
||||
"latencyHint": "latency to the voice server",
|
||||
"latencyUnknown": "ping unknown"
|
||||
},
|
||||
"friends": {
|
||||
|
||||
@@ -346,7 +346,7 @@
|
||||
}
|
||||
},
|
||||
"latency": "{{ms}} мс",
|
||||
"latencyHint": "Пинг участника до инстанса",
|
||||
"latencyHint": "задержка до голосового сервера",
|
||||
"latencyUnknown": "пинг неизвестен"
|
||||
},
|
||||
"friends": {
|
||||
|
||||
+16
-10
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user