fix(web): follow server job log output automatically
ci / ts / ts (packages + web) (push) Successful in 2m14s
ci / ts / ts (packages + web) (push) Successful in 2m14s
This commit is contained in:
1 parent
24707a0bfc
commit
7fa7ee2c7d
2 files changed
+60
-1
No files matched your search
@@ -9,6 +9,52 @@ async function login(page: Page, username = 'mobile-admin', target = '/') {
|
||||
await expect(page.getByRole('button', { name: 'Меню профиля' })).toBeVisible();
|
||||
}
|
||||
|
||||
test('job log follows new output and pauses while reading older lines', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.evaluate(() => {
|
||||
let lines = 100;
|
||||
window.addEventListener('test-job-lines', (event) => { lines = (event as CustomEvent<number>).detail; });
|
||||
const original = window.fetch;
|
||||
window.fetch = async (input, init) => {
|
||||
const response = await original(input, init);
|
||||
const request = new Request(input, init);
|
||||
if (request.method === 'GET' && new URL(request.url).pathname === '/api/v1/admin/server/updates') {
|
||||
const data = await response.json();
|
||||
data.job = {
|
||||
id: 'scroll-job', kind: 'update', state: 'running', target: 'main',
|
||||
started_at: '2026-10-22T09:00:00Z', finished_at: '',
|
||||
log: Array.from({ length: lines }, (_, i) => `Build line ${i + 1}`).join('\n'),
|
||||
};
|
||||
return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
return response;
|
||||
};
|
||||
history.pushState({}, '', '/admin/updates');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
});
|
||||
const log = page.getByLabel('Журнал выполнения');
|
||||
const bottomGap = () => log.evaluate((el) => el.scrollHeight - el.clientHeight - el.scrollTop);
|
||||
await expect(log).toContainText('Build line 100');
|
||||
expect(await log.evaluate((el) => el.scrollHeight > el.clientHeight)).toBe(true);
|
||||
await expect.poll(bottomGap).toBeLessThanOrEqual(4);
|
||||
|
||||
await page.evaluate(() => window.dispatchEvent(new CustomEvent('test-job-lines', { detail: 120 })));
|
||||
await expect(log).toContainText('Build line 120');
|
||||
await expect.poll(bottomGap).toBeLessThanOrEqual(4);
|
||||
|
||||
await log.evaluate((el) => { el.scrollTop = 100; });
|
||||
await expect.poll(() => log.evaluate((el) => el.scrollTop)).toBe(100);
|
||||
await page.evaluate(() => window.dispatchEvent(new CustomEvent('test-job-lines', { detail: 140 })));
|
||||
await expect(log).toContainText('Build line 140');
|
||||
expect(await log.evaluate((el) => el.scrollTop)).toBe(100);
|
||||
|
||||
await log.evaluate((el) => { el.scrollTop = el.scrollHeight; });
|
||||
await expect.poll(bottomGap).toBeLessThanOrEqual(4);
|
||||
await page.evaluate(() => window.dispatchEvent(new CustomEvent('test-job-lines', { detail: 160 })));
|
||||
await expect(log).toContainText('Build line 160');
|
||||
await expect.poll(bottomGap).toBeLessThanOrEqual(4);
|
||||
});
|
||||
|
||||
test('admin selects repository and version, confirms and sees persistent job log', async ({ page }) => {
|
||||
await login(page, 'mobile-admin', '/admin/updates');
|
||||
await expect(page.getByRole('heading', { name: 'Обновления', exact: true })).toBeVisible();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { components } from '@pipu/api-client';
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import s from './management.module.css';
|
||||
|
||||
@@ -6,6 +7,15 @@ export type Job = components['schemas']['ServerJob'];
|
||||
|
||||
export function JobPanel({ job, reconnecting }: { job: Job | null | undefined; reconnecting?: boolean }) {
|
||||
const { t } = useTranslation('admin');
|
||||
const logRef = useRef<HTMLPreElement>(null);
|
||||
const followLog = useRef(true);
|
||||
useLayoutEffect(() => {
|
||||
followLog.current = true;
|
||||
}, [job?.id]);
|
||||
useLayoutEffect(() => {
|
||||
const log = logRef.current;
|
||||
if (log && followLog.current) log.scrollTop = log.scrollHeight;
|
||||
}, [job?.id, job?.log]);
|
||||
if (!job) return null;
|
||||
return <section className={s.card} aria-label={t('management.job')}>
|
||||
<div className={s.cardHead}>
|
||||
@@ -15,6 +25,9 @@ export function JobPanel({ job, reconnecting }: { job: Job | null | undefined; r
|
||||
<p className={s.note}>{t('management.started')}: {new Date(job.started_at).toLocaleString()}
|
||||
{job.finished_at && <> · {t('management.finished')}: {new Date(job.finished_at).toLocaleString()}</>}</p>
|
||||
{job.state === 'running' && <p className={s.note}>{t(reconnecting ? 'management.reconnecting' : 'management.running_hint')}</p>}
|
||||
<pre className={s.log} tabIndex={0} aria-label={t('management.log')}>{job.log || t('management.waiting')}</pre>
|
||||
<pre ref={logRef} className={s.log} tabIndex={0} aria-label={t('management.log')} onScroll={(event) => {
|
||||
const log = event.currentTarget;
|
||||
followLog.current = log.scrollHeight - log.clientHeight - log.scrollTop <= 4;
|
||||
}}>{job.log || t('management.waiting')}</pre>
|
||||
</section>;
|
||||
}
|
||||
Reference in new issue
Block a user