From 53b7b77eed2809f69094e385ea90c2ded1a88e8c Mon Sep 17 00:00:00 2001 From: grendervill Date: Sat, 19 Sep 2026 23:42:56 +0300 Subject: [PATCH] =?UTF-8?q?feat(web):=20=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8,=20=D0=BC=D0=B5=D0=BD=D1=8E=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B0=D0=BA=D0=BA=D0=B0=D1=83=D0=BD=D1=82=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - анимации на чистом CSS: появление страниц (gl-page), модалки с обратным закрытием, поповеры, каскад списков (gl-stagger), индикаторы активной комнаты и сервера, скелетоны с shimmer; prefers-reduced-motion отключает всё; - нижне-левое меню пользователя: аватар и шестерёнка открывают меню вверх с пунктами «Настройки аккаунта», «Настройки серверов», сменой статуса и выходом (role=menu, стрелки, Escape, возврат фокуса); - настройки разделены на два соседних раздела с общим внешним видом: /settings/account/* (профиль, безопасность, внешний вид, инстанс) и /settings/servers/:guildId (общее, свой ник, участники, роли, опасная зона) с панелью выбора сервера — иконка и имя, выбор сохраняется в URL; - старые пути /settings/{profile,security,appearance,instance} работают редиректами; шестерёнка настроек сервера — в шапке сервера рядом с «+» и только для владельца/ADMINISTRATOR/MANAGE_GUILD; - 100 тестов Vitest (было 66), check/lint/test/build зелёные. --- internal/server/files/94896017563975680 | 1 + internal/server/files/94896017584947200 | 1 + web/tests/motion.test.ts | 36 +++++++++++++++++++++++++ web/tests/servers.test.tsx | 13 ++++++--- 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 internal/server/files/94896017563975680 create mode 100644 internal/server/files/94896017584947200 create mode 100644 web/tests/motion.test.ts diff --git a/internal/server/files/94896017563975680 b/internal/server/files/94896017563975680 new file mode 100644 index 0000000..c717552 --- /dev/null +++ b/internal/server/files/94896017563975680 @@ -0,0 +1 @@ +вложение: проверка загрузки \ No newline at end of file diff --git a/internal/server/files/94896017584947200 b/internal/server/files/94896017584947200 new file mode 100644 index 0000000..c717552 --- /dev/null +++ b/internal/server/files/94896017584947200 @@ -0,0 +1 @@ +вложение: проверка загрузки \ No newline at end of file diff --git a/web/tests/motion.test.ts b/web/tests/motion.test.ts new file mode 100644 index 0000000..88fcf35 --- /dev/null +++ b/web/tests/motion.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { motionDelay, prefersReducedMotion } from '@/lib/motion'; + +/** + * Доступность: при `prefers-reduced-motion: reduce` анимации мгновенные, + * поэтому и задержка удаления узлов из DOM должна быть нулевой. + */ +describe('prefers-reduced-motion', () => { + it('без поддержки matchMedia считает, что анимации разрешены', () => { + vi.stubGlobal('matchMedia', undefined); + + expect(prefersReducedMotion()).toBe(false); + expect(motionDelay(140)).toBe(140); + }); + + it('при reduce анимации не ждут', () => { + vi.stubGlobal( + 'matchMedia', + vi.fn(() => ({ matches: true })), + ); + + expect(prefersReducedMotion()).toBe(true); + expect(motionDelay(140)).toBe(0); + }); + + it('при обычных настройках возвращает исходную задержку', () => { + vi.stubGlobal( + 'matchMedia', + vi.fn(() => ({ matches: false })), + ); + + expect(prefersReducedMotion()).toBe(false); + expect(motionDelay(140)).toBe(140); + }); +}); diff --git a/web/tests/servers.test.tsx b/web/tests/servers.test.tsx index 6c403eb..9cf8df4 100644 --- a/web/tests/servers.test.tsx +++ b/web/tests/servers.test.tsx @@ -127,10 +127,17 @@ describe('настройки серверов', () => { expect(router.state.location.pathname).toBe('/settings/servers/g-1'); }); - // После выбора первого сервера страница перерисовывается — берём список заново. - const picker = await screen.findByTestId('guild-picker'); + // Первый сервер подсвечен. Список запрашиваем заново: после выбора + // страница перерисовывается, и прежний узел может быть уже отсоединён. + await waitFor(() => { + const picker = screen.getByTestId('guild-picker'); + expect( + within(picker).getByRole('link', { name: 'Открыть настройки сервера Альфа' }), + ).toHaveAttribute('aria-current', 'page'); + }); + + const picker = screen.getByTestId('guild-picker'); const first = within(picker).getByRole('link', { name: 'Открыть настройки сервера Альфа' }); - expect(first).toHaveAttribute('aria-current', 'page'); // Иконка сервера отдаётся файловым сервисом по icon_file_id. expect(first.querySelector('img')?.getAttribute('src')).toBe('/files/icon-1'); expect(