feat(web): вход с 2FA сразу, панель пользователя, модальные настройки, markdown
По правкам пользователя: - вход: поле «Код 2FA или резервный код» видно сразу, принимает 14-символьные резервные коды (раньше влезало 6 цифр), пустое значение не ломает вход; - нижняя панель: одна подложка, слева аватар и имя с мини-профилем и выбором статуса, справа микрофон, камера, наушники (переключатели, стрелка с выбором устройства и громкости, переход в «Аудио-видео») и шестерёнка; - кнопка «Покинуть сервер» из сайдбара убрана (выход — в «Опасной зоне»); - настройки открываются модальным окном со списком пунктов; «Профиль» имеет вкладки «Основной» и «Сервер» (список серверов, ник и «Применить ко всем серверам»), аватар загружается и удаляется, есть часовой пояс (МСК по умолчанию); - markdown: добавлены ==выделение==, __жирный__, ***оба***, ||спойлер||, #канал; - двойной клик по сообщению ставит ответ (не срабатывает на ссылках, кнопках и при выделенном тексте); - 219 тестов Vitest (было 176), check/lint/test/build зелёные.
This commit is contained in:
+53
-25
@@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { findRequest, installFetch, json, makeUser, recordedRequests, renderApp } from './helpers';
|
||||
import type { FetchRoute } from './helpers';
|
||||
|
||||
const user = makeUser();
|
||||
const user = makeUser({ bio: 'Пишу код и тесты' });
|
||||
|
||||
/** Базовые ответы для оболочки приложения без серверов. */
|
||||
function baseRoutes(extra: FetchRoute[] = []): FetchRoute[] {
|
||||
@@ -17,7 +17,7 @@ function baseRoutes(extra: FetchRoute[] = []): FetchRoute[] {
|
||||
];
|
||||
}
|
||||
|
||||
/** Открывает нижне-левое меню пользователя кликом по аватару/имени. */
|
||||
/** Открывает мини-профиль кликом по аватару/имени. */
|
||||
async function openMenu() {
|
||||
const trigger = await screen.findByLabelText('Открыть меню пользователя');
|
||||
await userEvent.click(trigger);
|
||||
@@ -25,55 +25,82 @@ async function openMenu() {
|
||||
return { trigger, menu };
|
||||
}
|
||||
|
||||
/** Открывает пункт меню и возвращает окно настроек. */
|
||||
async function openSettings(item: string) {
|
||||
const { menu } = await openMenu();
|
||||
await userEvent.click(within(menu).getByRole('menuitem', { name: item }));
|
||||
return screen.findByRole('dialog', { name: 'Настройки' });
|
||||
}
|
||||
|
||||
describe('меню пользователя', () => {
|
||||
it('открывается по клику на аватар и содержит все пункты', async () => {
|
||||
it('мини-профиль открывается по клику на аватар и показывает данные и статусы', async () => {
|
||||
installFetch(baseRoutes());
|
||||
|
||||
renderApp('/app/empty');
|
||||
const { menu } = await openMenu();
|
||||
|
||||
expect(within(menu).getByRole('menuitem', { name: 'Настройки аккаунта' })).toBeVisible();
|
||||
expect(within(menu).getByRole('menuitem', { name: 'Настройки серверов' })).toBeVisible();
|
||||
// Аватар, имя, логин и «О себе».
|
||||
expect(within(menu).getByText('Alice')).toBeVisible();
|
||||
expect(within(menu).getByText('@alice')).toBeVisible();
|
||||
expect(within(menu).getByText('Пишу код и тесты')).toBeVisible();
|
||||
|
||||
// Статусы с подписями и цветными точками.
|
||||
expect(
|
||||
within(menu).getByRole('menuitemradio', { name: 'В сети', checked: true }),
|
||||
).toBeVisible();
|
||||
expect(within(menu).getByRole('menuitemradio', { name: 'Не беспокоить' })).toBeVisible();
|
||||
expect(within(menu).getByRole('menuitemradio', { name: 'Неактивен' })).toBeVisible();
|
||||
expect(within(menu).getByRole('menuitemradio', { name: 'Невидимый' })).toBeVisible();
|
||||
expect(menu.querySelectorAll('[role="menuitemradio"] .rounded-full')).toHaveLength(4);
|
||||
|
||||
expect(within(menu).getByRole('menuitem', { name: 'Настройки аккаунта' })).toBeVisible();
|
||||
expect(within(menu).getByRole('menuitem', { name: 'Настройки серверов' })).toBeVisible();
|
||||
expect(within(menu).getByRole('menuitem', { name: 'Выйти' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('шестерёнка открывает то же меню', async () => {
|
||||
it('шестерёнка открывает окно настроек', async () => {
|
||||
installFetch(baseRoutes());
|
||||
|
||||
renderApp('/app/empty');
|
||||
await userEvent.click(await screen.findByLabelText('Настройки пользователя'));
|
||||
|
||||
expect(await screen.findByRole('menu', { name: 'Меню пользователя' })).toBeVisible();
|
||||
const dialog = await screen.findByRole('dialog', { name: 'Настройки' });
|
||||
expect(within(dialog).getByRole('tab', { name: 'Основной' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
);
|
||||
// Мини-профиль при этом не открывается.
|
||||
expect(screen.queryByRole('menu', { name: 'Меню пользователя' })).toBeNull();
|
||||
});
|
||||
|
||||
it('пункт «Настройки аккаунта» ведёт в /settings/account/profile', async () => {
|
||||
it('пункт «Настройки аккаунта» открывает окно на «Профиль → Основной»', async () => {
|
||||
installFetch(baseRoutes());
|
||||
|
||||
const { router } = renderApp('/app/empty');
|
||||
const { menu } = await openMenu();
|
||||
await userEvent.click(within(menu).getByRole('menuitem', { name: 'Настройки аккаунта' }));
|
||||
const dialog = await openSettings('Настройки аккаунта');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(router.state.location.pathname).toBe('/settings/account/profile');
|
||||
});
|
||||
expect(await screen.findByRole('heading', { name: 'Профиль' })).toBeVisible();
|
||||
expect(within(dialog).getByRole('tab', { name: 'Основной' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
);
|
||||
expect(within(dialog).getByRole('heading', { name: 'Профиль' })).toBeVisible();
|
||||
// Из приложения никуда не уходим: настройки — модальное окно.
|
||||
expect(router.state.location.pathname).toBe('/app/empty');
|
||||
});
|
||||
|
||||
it('пункт «Настройки серверов» ведёт в /settings/servers', async () => {
|
||||
it('пункт «Настройки серверов» открывает окно на «Профиль → Сервер»', async () => {
|
||||
installFetch(baseRoutes());
|
||||
|
||||
const { router } = renderApp('/app/empty');
|
||||
const { menu } = await openMenu();
|
||||
await userEvent.click(within(menu).getByRole('menuitem', { name: 'Настройки серверов' }));
|
||||
renderApp('/app/empty');
|
||||
const dialog = await openSettings('Настройки серверов');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(router.state.location.pathname).toBe('/settings/servers');
|
||||
});
|
||||
expect(await screen.findByText('У вас пока нет серверов.')).toBeVisible();
|
||||
expect(within(dialog).getByRole('tab', { name: 'Сервер' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
);
|
||||
expect(
|
||||
within(screen.getByTestId('settings-content')).getByText('Пока нет ни одного сервера'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it('смена статуса уходит в PATCH /users/@me и отмечает текущий статус', async () => {
|
||||
@@ -159,15 +186,16 @@ describe('меню пользователя', () => {
|
||||
trigger.focus();
|
||||
await userEvent.keyboard('{Enter}');
|
||||
|
||||
const first = await screen.findByRole('menuitem', { name: 'Настройки аккаунта' });
|
||||
// Первый пункт мини-профиля — выбор статуса.
|
||||
const first = await screen.findByRole('menuitemradio', { name: 'В сети' });
|
||||
await waitFor(() => {
|
||||
expect(first).toHaveFocus();
|
||||
});
|
||||
|
||||
await userEvent.keyboard('{ArrowDown}');
|
||||
expect(screen.getByRole('menuitem', { name: 'Настройки серверов' })).toHaveFocus();
|
||||
expect(screen.getByRole('menuitemradio', { name: 'Неактивен' })).toHaveFocus();
|
||||
|
||||
await userEvent.keyboard('{ArrowUp}');
|
||||
expect(screen.getByRole('menuitem', { name: 'Настройки аккаунта' })).toHaveFocus();
|
||||
expect(screen.getByRole('menuitemradio', { name: 'В сети' })).toHaveFocus();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user