fix(web): clarify Genius processing progress and queue actions
This commit is contained in:
1 parent
0a59059738
commit
17b6dd2041
5 files changed
+95
-7
No files matched your search
@@ -0,0 +1,67 @@
|
||||
import { AuthSession } from '@pipu/core';
|
||||
import { createI18n } from '@pipu/i18n';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { afterEach, expect, it } from 'vitest';
|
||||
import { SessionProvider } from '../../app/session';
|
||||
import { GeniusPanel } from './GeniusPanel';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const initial = {
|
||||
configured: true, paused: false, running: true,
|
||||
total: 100, pending: 80, matched: 17, lyrics: 15, annotations: 5, images: 17,
|
||||
unmatched: 2, errors: 1, last_error: '', cooldown_until: '2000-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
async function fixture(overrides: Partial<typeof initial> = {}) {
|
||||
let data = { ...initial, ...overrides };
|
||||
const actions: string[] = [];
|
||||
const session = new AuthSession({ baseUrl: 'http://localhost', clientType: 'web', fetch: async (url, init) => {
|
||||
const request = new Request(url, init);
|
||||
if (request.method === 'POST') {
|
||||
const { action } = await request.json() as { action: string };
|
||||
actions.push(action);
|
||||
data = { ...data, paused: action === 'pause' };
|
||||
}
|
||||
return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } });
|
||||
} });
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } });
|
||||
client.setQueryData(['genius-stats'], data);
|
||||
const i18n = await createI18n('ru');
|
||||
render(<I18nextProvider i18n={i18n}><SessionProvider session={session}><QueryClientProvider client={client}>
|
||||
<GeniusPanel />
|
||||
</QueryClientProvider></SessionProvider></I18nextProvider>);
|
||||
return { actions, user: userEvent.setup() };
|
||||
}
|
||||
|
||||
it.each<[Partial<typeof initial>, string]>([
|
||||
[{}, 'Получение данных из Genius…'],
|
||||
[{ running: false }, 'Очередь ожидает обработки'],
|
||||
[{ paused: true }, 'Обработка приостановлена'],
|
||||
[{ cooldown_until: '2099-01-01T00:00:00Z' }, 'Ожидание после ограничения Genius'],
|
||||
[{ running: false, pending: 0 }, 'Ожидание новых треков'],
|
||||
])('shows queue state and progress for %j', async (overrides, status) => {
|
||||
await fixture(overrides);
|
||||
expect(screen.getByRole('status')).toHaveTextContent(status);
|
||||
const progress = screen.getByRole('progressbar', { name: 'Прогресс обработки Genius' });
|
||||
expect(progress).toHaveAttribute('max', '100');
|
||||
expect(progress).toHaveAttribute('value', String(100 - (overrides.pending ?? 80)));
|
||||
});
|
||||
|
||||
it('pauses and resumes the existing queue without requesting a retry', async () => {
|
||||
const { user, actions } = await fixture();
|
||||
await user.click(screen.getByRole('button', { name: 'Приостановить обработку' }));
|
||||
await user.click(await screen.findByRole('button', { name: 'Продолжить обработку' }));
|
||||
expect(actions).toEqual(['pause', 'start']);
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('value', '20');
|
||||
});
|
||||
|
||||
it('explains the retry scope and sends the retry action', async () => {
|
||||
const { user, actions } = await fixture();
|
||||
expect(screen.getByText(/Повтор добавит в очередь только/)).toBeVisible();
|
||||
await user.click(screen.getByRole('button', { name: 'Добавить проблемные треки в очередь' }));
|
||||
expect(actions).toEqual(['retry']);
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import s from '../page.module.css';
|
||||
import g from '../genius.module.css';
|
||||
|
||||
export function GeniusPanel() {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const session = useSession();
|
||||
const queries = useQueryClient();
|
||||
const errorText = useErrorText();
|
||||
@@ -24,16 +24,23 @@ export function GeniusPanel() {
|
||||
});
|
||||
const data = stats.data;
|
||||
const cooling = data && Date.parse(data.cooldown_until) > Date.now();
|
||||
const state = !data ? t('genius.loading') : !data.configured ? t('genius.disabled') : data.paused ? t('genius.paused') : cooling ? t('genius.cooldown') : data.running ? t('genius.running') : t('genius.waiting');
|
||||
const state = !data ? t('genius.loading') : !data.configured ? t('genius.disabled') : data.paused ? t('genius.paused') : cooling ? t('genius.cooldown') : data.running ? t('genius.running') : data.pending > 0 ? t('genius.queued') : t('genius.waiting');
|
||||
const processed = data ? Math.max(0, data.total - data.pending) : 0;
|
||||
const formatNumber = (value: number) => value.toLocaleString(i18n.language);
|
||||
return <section className={s.section} aria-label="Genius">
|
||||
<div className={s.head}><h2>Genius</h2><span role="status">{state}</span></div>
|
||||
<p className={s.subtitle}>{t('genius.description')}</p>
|
||||
<FormError>{stats.error ? errorText(stats.error) : control.error ? errorText(control.error) : null}</FormError>
|
||||
{data && <>
|
||||
{!data.configured && <p>{t('genius.configure')}</p>}
|
||||
{data.total > 0 && <div className={g.progress}>
|
||||
<span>{t('genius.progress', { processed: formatNumber(processed), total: formatNumber(data.total) })}</span>
|
||||
<progress aria-label={t('genius.progress_label')} value={processed} max={data.total} />
|
||||
<p className={s.subtitle}>{t('genius.progress_hint')}</p>
|
||||
</div>}
|
||||
<dl className={g.stats}>
|
||||
{(['total', 'pending', 'matched', 'lyrics', 'annotations', 'images', 'unmatched', 'errors'] as const).map((key) =>
|
||||
<div key={key}><dt>{t(`genius.${key}`)}</dt><dd>{data[key].toLocaleString()}</dd></div>)}
|
||||
<div key={key}><dt>{t(`genius.${key}`)}</dt><dd>{formatNumber(data[key])}</dd></div>)}
|
||||
</dl>
|
||||
{data.last_error && <p role="alert">{t('genius.last_error')}: {data.last_error === 'http_401' ? t('genius.token_error') : data.last_error === 'http_403' ? t('genius.access_error') : data.last_error === 'http_429' ? t('genius.cooldown') : data.last_error === 'lyrics_unavailable' ? t('genius.no_lyrics') : t('genius.request_error')}</p>}
|
||||
{cooling && <p>{t('genius.resume_at')}: {new Date(data.cooldown_until).toLocaleString()}</p>}
|
||||
@@ -41,6 +48,8 @@ export function GeniusPanel() {
|
||||
<Button disabled={!data.configured || control.isPending} onClick={() => control.mutate(data.paused ? 'start' : 'pause')}>{t(data.paused ? 'genius.start' : 'genius.pause')}</Button>
|
||||
<Button disabled={!data.configured || control.isPending || data.errors + data.unmatched === 0} onClick={() => control.mutate('retry')}>{t('genius.retry')}</Button>
|
||||
</div>
|
||||
<p className={s.subtitle}>{t('genius.controls_hint')}</p>
|
||||
<p className={s.subtitle}>{t('genius.retry_hint')}</p>
|
||||
</>}
|
||||
</section>;
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
.stats dt { color: var(--text2); font-size: 13px; }
|
||||
.stats dd { margin: 6px 0 0; font-size: 25px; font-variant-numeric: tabular-nums; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
.progress { display: grid; gap: 8px; font-variant-numeric: tabular-nums; }
|
||||
.progress progress { display: block; width: 100%; height: 8px; accent-color: var(--accent); }
|
||||
.lyrics { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.8; }
|
||||
.annotation { border-top: 1px solid var(--line); padding-top: 16px; overflow-wrap: anywhere; }
|
||||
.annotation summary { cursor: pointer; line-height: 1.6; }
|
||||
|
||||
Reference in new issue
Block a user