From 7a01e8d545abbc55f933597a884add898e7e2820 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 28 Sep 2026 16:49:56 +0700 Subject: [PATCH] =?UTF-8?q?test(ui):=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D1=82=D1=8C=20=D0=BE=D0=B6=D0=B8=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20SSE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 9 ++++++++- docs/ux-ui-stage-8.md | 2 ++ tests/e2e/live-stream-fixture.ts | 8 ++++---- tests/e2e/traffic-flow.spec.ts | 22 ++++++++++++++++++++++ tests/e2e/traffic-live.spec.ts | 20 ++++++++++++++++++-- 6 files changed, 55 insertions(+), 8 deletions(-) diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 5257d0b4..96a730a7 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -88,7 +88,7 @@ flowchart LR Ближайший оставшийся объём: -1. Молчащий SSE и CRLF/heartbeat проверены в блоках 8.6–8.7. Одиночные точки графика исправлены в 8.8. Далее — ожидание первого sample. +1. Молчащий SSE и CRLF/heartbeat проверены в блоках 8.6–8.7. Одиночные точки графика исправлены в 8.8; ожидание первого sample проверено в 8.9. Следующий шаг — месячный отчёт и доступность. 2. Итоговая приёмка этапа 8: решение по архивным серверам/месяцам, ReUI/accessibility checklist. Промежуточная матрица проверок сохранена в [этапе 8](ux-ui-stage-8.md). 3. После фиксации результата этапа 8 — этап 9. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 2a763c8b..7e11d823 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -24,13 +24,20 @@ | 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже | | 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже | | 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже | -| 8 | Трафик и статистика | В работе: выполнены блоки 8.1–8.8; одиночные точки графика исправлены, далее оставшийся аудит | +| 8 | Трафик и статистика | В работе: выполнены блоки 8.1–8.9; ожидание первого SSE проверено, далее месячный отчёт и доступность | | 9 | VPN, Firewall и Filters | Не начат | | 10 | Маршрутизация, диагностика, карта и терминал | Не начат | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | +## Блок 8.9 — ожидание первого SSE sample (2026-09-28) + +- Управляемый ReadableStream умеет открываться без первого sample. Добавлены flow/серверные сценарии: до первого сообщения нет фиктивной live-метки или времени live-измерения; отображается подтверждённая REST-история. +- Heartbeat в ожидании не отменяет 15-секундный таймаут. После таймаута история и её отметка остаются; первый валидный sample восстанавливает live. Flow дополнительно проверяет изменение скорости 1→7 Мбит/с, источника API→live и времени получения. Сервер проверяет появление первого измерения 17 Мбит/с. +- Все 28 flow/серверных SSE-E2E прошли; lint, tsc --noEmit --incremental false и diff-check чистые. Production-код не менялся, production build не повторялся. Имитация начинается с открытого Response; отдельная задержка HTTP-заголовков в этом блоке не проверяется. +- Далее: зафиксировать итоговый объём месячного отчёта (архивные серверы/прошлые месяцы) и выполнить оставшийся ReUI/accessibility checklist перед завершением этапа 8. Реальные RouterOS/IPFIX/JWT не проверены. Использовано 32% пятичасового и 20% недельного лимита. Локальный коммит без push. + ## Блок 8.8 — одиночные точки основного графика (2026-09-28) - Общий TrafficRxTxChart показывает маркер только для валидной точки без валидных соседей в том же направлении. Одиночный ноль и точки между пропусками видны; непрерывные серии сохраняют прежний вид без маркеров. RX/TX используют одинаковое правило и существующие цвета темы. diff --git a/docs/ux-ui-stage-8.md b/docs/ux-ui-stage-8.md index 34d9b003..b6056893 100644 --- a/docs/ux-ui-stage-8.md +++ b/docs/ux-ui-stage-8.md @@ -48,3 +48,5 @@ Обновление 8.7 (2026-09-28): LF/CRLF и комментарий перед event поддержаны в обоих hook. Два потоковых сценария проверяют разделитель между chunk и heartbeat, не продлевающий свежесть. Все 26 профильных E2E прошли; далее одиночная точка графика и остальные ограничения, перечисленные выше. Обновление 8.8 (2026-09-28): одиночные точки RX/TX, включая нули между пропусками, получили маркеры. Итоговые 10 тестов деталей и build/lint прошли; перед финальным подключением TX проверены 38 сценариев потребителей. Далее — первый SSE sample и оставшаяся приёмка. + +Обновление 8.9 (2026-09-28): проверено ожидание первого sample в открытом SSE для flow и сервера, heartbeat без sample, таймаут и переход с истории на live. 28 профильных E2E, lint/TypeScript прошли; production-код не менялся. Далее — решение по месячному отчёту и доступность. diff --git a/tests/e2e/live-stream-fixture.ts b/tests/e2e/live-stream-fixture.ts index 41eaad72..f37a0efb 100644 --- a/tests/e2e/live-stream-fixture.ts +++ b/tests/e2e/live-stream-fixture.ts @@ -1,9 +1,9 @@ import type { Page } from '@playwright/test' // Keep a response open; the browser test controls sample delivery independently of HTTP routes. -export async function installOpenLiveStream(page: Page, path: string, sample: unknown) { +export async function installOpenLiveStream(page: Page, path: string, sample: unknown, sendImmediately = true) { await page.clock.install() - await page.addInitScript(({ path, sample }) => { + await page.addInitScript(({ path, sample, sendImmediately }) => { const originalFetch = window.fetch.bind(window) window.fetch = async (input, init) => { const url = new URL(input instanceof Request ? input.url : String(input), location.href) @@ -18,12 +18,12 @@ export async function installOpenLiveStream(page: Page, path: string, sample: un } const heartbeat = () => controller.enqueue(encode(': heartbeat\r\n\r\n')) Object.assign(window, { sendLiveSample: send, sendLiveHeartbeat: heartbeat }) - send() + if (sendImmediately) send() }, }) return new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } }) } - }, { path, sample }) + }, { path, sample, sendImmediately }) } export async function resumeLiveStream(page: Page) { diff --git a/tests/e2e/traffic-flow.spec.ts b/tests/e2e/traffic-flow.spec.ts index 3f736af8..a9caea0d 100644 --- a/tests/e2e/traffic-flow.spec.ts +++ b/tests/e2e/traffic-flow.spec.ts @@ -343,3 +343,25 @@ test('Flow: молчащий SSE сохраняет данные и восста await page.clock.fastForward(15_001) await expect(page.getByText('Нет обновлений live более 15 секунд', { exact: true })).toHaveCount(0) }) + + +test('Flow: ожидание первого sample использует историю без фиктивного live', async ({ page }) => { + await installOpenLiveStream(page, '/api/traffic/flow/live', { bpsNow: 7000000, bytes: 200, packets: 2, conversations: 1, uniqueSrc: 1, uniqueDst: 1, topProto: 'TCP', rxSeries: [1], txSeries: [2], applications: [], protocols: [], sources: [], destinations: [], interfaces: [], conversationsList: [], ifaces: [], live: true }, false) + await setup(page) + const receipt = page.getByLabel('Получение аналитики потоков') + await expect(receipt).toContainText('Получено (API)') + await expect(page.getByText('live', { exact: true })).toHaveCount(0) + await expect(page.getByLabel('Скорость потоков')).toContainText('1.00 Мбит/с') + const stamp = await receipt.locator('time').getAttribute('datetime') + await page.clock.fastForward(10_000) + await sendLiveHeartbeat(page) + await page.clock.fastForward(5_001) + await expect(page.getByText('Нет обновлений live более 15 секунд', { exact: true })).toBeVisible() + await expect(page.getByText('История · live недоступен', { exact: true }).filter({ visible: true })).toHaveCount(2) + await expect(receipt.locator('time')).toHaveAttribute('datetime', stamp!) + await resumeLiveStream(page) + await expect(page.getByText('live', { exact: true }).filter({ visible: true })).toHaveCount(3) + await expect(page.getByLabel('Скорость потоков')).toContainText('7.00 Мбит/с') + await expect(receipt).toContainText('Получено (live-поток)') + await expect(receipt.locator('time')).not.toHaveAttribute('datetime', stamp!) +}) diff --git a/tests/e2e/traffic-live.spec.ts b/tests/e2e/traffic-live.spec.ts index 740d382c..7ebdc4a7 100644 --- a/tests/e2e/traffic-live.spec.ts +++ b/tests/e2e/traffic-live.spec.ts @@ -1,7 +1,7 @@ import { installOpenLiveStream, resumeLiveStream, sendLiveHeartbeat } from "./live-stream-fixture" import { test, expect } from '@playwright/test' -for (const variant of ['closed', 'invalid', 'http-error', 'silent'] as const) { +for (const variant of ['closed', 'invalid', 'http-error', 'silent', 'awaiting'] as const) { test(`Трафик: SSE ${variant} не выдаёт старое измерение за live`, async ({ page }) => { const server = { id: '1', name: 'Тестовый сервер', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, rxPeak: 5, txPeak: 6, rxTotal: 100, txTotal: 200, sessions: 1, rxSeries: [1, 2], txSeries: [2, 3] } await page.route('**/health', route => route.fulfill({ json: { ok: true } })) @@ -23,12 +23,28 @@ for (const variant of ['closed', 'invalid', 'http-error', 'silent'] as const) { : path === '/api/servers' ? [] : {} return route.fulfill({ json }) }) - if (variant === 'silent') await installOpenLiveStream(page, '/api/traffic/servers/1/live', { rxMbps: 17, txMbps: 19, at: '2026-09-23T12:00:00Z' }) + if (variant === 'silent' || variant === 'awaiting') await installOpenLiveStream(page, '/api/traffic/servers/1/live', { rxMbps: 17, txMbps: 19, at: '2026-09-23T12:00:00Z' }, variant !== 'awaiting') const received = new Date('2026-09-28T06:00:00Z') await page.clock.setFixedTime(received) await page.goto('/traffic') const summary = page.getByLabel('Скорость выбранного сервера') await expect(summary).toBeVisible() + if (variant === 'awaiting') { + const measurement = page.getByLabel('Последнее измерение сервера') + await expect(summary.getByText('История · ожидание live', { exact: true }).filter({ visible: true })).toHaveCount(2) + await expect(measurement).toHaveCount(0) + await expect(summary).not.toContainText('17.0') + await page.clock.fastForward(10_000) + await sendLiveHeartbeat(page) + await page.clock.fastForward(5_001) + await expect(summary.getByText('История · live недоступен', { exact: true }).filter({ visible: true })).toHaveCount(2) + await expect(measurement).toHaveCount(0) + await resumeLiveStream(page) + await expect(summary.getByText('live', { exact: true }).filter({ visible: true })).toHaveCount(2) + await expect(summary).toContainText('17.0') + await expect(measurement).toBeVisible() + return + } if (variant === 'silent') { await expect(summary.getByText('live', { exact: true }).filter({ visible: true })).toHaveCount(2) const stamp = await page.getByLabel('Последнее измерение сервера').locator('time').first().getAttribute('datetime')