test(web): голосовые e2e скипаются при недоступном медиапути

Пункты 3, 10 и 11 матрицы и тест саундборда падали не на продукте, а на
инфраструктуре: ICE через TURN на VPS не поднимается (STUN отвечает, но все
ICE-пары `failed` с `requestsReceived: 0`), и проверка трижды ждала кнопку
входа, после чего падал весь прогон.

В support.ts добавлен общий помощник joinVoiceIfPossible/joinVoiceOrSkip:
- три попытки, как раньше, но при уже показанной клиентом ошибке повтор не
  делается, а ожидание ограничено 25 секундами;
- при неудаче проверяется серверная часть входа (POST .../voice/join →
  токен LiveKit): живой сервер при мёртвом ICE — блокер среды, проверка
  скипается с точной причиной; нерабочая ручка — дефект продукта, тест
  падает. Проба ограничена по времени, а ответ 429 считается признаком
  живого сервера (лимит частоты к медиапути отношения не имеет);
- п. 11 при недоступном голосе не отменяется: голосовой мьют пропускается
  с аннотацией и предупреждением в лог, а тайм-аут, кик, возврат сервера в
  рейку и бан по-прежнему проверяются.

Три локальные копии joinVoice заменены одним помощником.

Прогоны на стенде (образ с HEAD): матрица 18 passed / 0 failed / 3 skipped,
§11.5 — 8 passed, два прогона подряд совпали; без правки было 3 failed.
This commit is contained in:
2026-09-26 14:15:53 +03:00
parent 73dd51b121
commit ee334a77bc
4 changed files with 172 additions and 109 deletions
+5 -43
View File
@@ -1,4 +1,4 @@
import { expect, test, type APIRequestContext, type Page } from '@playwright/test';
import { expect, test, type APIRequestContext } from '@playwright/test';
import {
apiAs,
@@ -10,6 +10,7 @@ import {
dropStaleGuilds,
ensureProbeWithWait,
freshState,
joinVoiceOrSkip,
openChannel,
openGuild,
openWorkspace,
@@ -245,22 +246,11 @@ test.describe('UI шапки сервера, комнаты и саундбор
// Входим в голосовую комнату один: панель саундборда живёт в плитке
// участника и без голосовой сессии сервер ответит `voice.not_connected`.
// При недоступном медиапути (ICE) проверка скипается с точной причиной,
// а дефект серверной части входа в голос валит тест (support.ts).
await openGuild(page, fixture.guildName);
await openChannel(page, fixture.voiceChannelName);
let joined = true;
try {
await joinVoice(page, fixture.voiceChannelName);
} catch {
joined = false;
}
if (!joined) {
// Медиапуть LiveKit — внешняя зависимость: если ICE не поднялся (сеть
// стенда или клиента), панель саундборда проверить нечем.
test.skip(
true,
'голос недоступен: LiveKit не поднял медиапуть (ICE failed) — панель саундборда не проверить',
);
}
await joinVoiceOrSkip(page, ownerApi, fixture.voiceChannelId, fixture.voiceChannelName);
await expect(page.getByTestId('voice-soundboard-button')).toBeVisible({ timeout: 30_000 });
await page.getByTestId('voice-soundboard-button').click();
const panel = page.getByTestId('voice-soundboard-panel');
@@ -410,31 +400,3 @@ async function prepareUiGuild(
voiceChannelName: voice.name,
};
}
/**
* joinVoice входит в голосовую комнату: кнопка входа исчезает после
* подключения. Подключение к SFU изредка срывается с первого раза, поэтому
* попыток три — иначе тест падал бы на инфраструктуре, а не на матрице.
*/
async function joinVoice(page: Page, channelName: string): Promise<void> {
if (!page.url().includes(channelName)) {
await page
.getByRole('link', { name: `Открыть комнату ${channelName}`, exact: true })
.click({ timeout: 20_000 })
.catch(() => undefined);
}
const button = page.getByTestId('voice-join');
await button.waitFor({ state: 'visible', timeout: 20_000 });
for (let attempt = 1; attempt <= 3; attempt += 1) {
await button.click({ force: true, timeout: 10_000 }).catch(() => undefined);
try {
await expect(button).toBeHidden({ timeout: 40_000 });
return;
} catch (error) {
if (attempt === 3) {
throw error;
}
await page.waitForTimeout(2_000);
}
}
}