test(ui): проверить поздний фрагмент открытого потока

This commit is contained in:
Denozordec committed 2026-09-28 11:12:06 +07:00
1 parent 53bca894bb
commit 7318c5286b
3 files changed
+73 -3

No files matched your search

+2 -2
View File
@@ -84,11 +84,11 @@ flowchart LR
Подробные планы: [этап 6](ux-ui-stage-6.md), [этап 7](ux-ui-stage-7.md), [этап 8](ux-ui-stage-8.md).
Для этапа 8 уже выполнены блоки периодов/URL статистики, ошибок и гонок трафика, месячного отчёта, адаптивности, неизвестных метрик, пустой истории и разрывов графиков. Проверены смена flow-сервера, отмена SSE до ответа и смена backend через настройки. Точный состав тестов и оговорки — в журнале, блоки 8.1–8.4М.
Для этапа 8 уже выполнены блоки периодов/URL статистики, ошибок и гонок трафика, месячного отчёта, адаптивности, неизвестных метрик, пустой истории и разрывов графиков. Проверены смена flow-сервера, отмена SSE до ответа, позднее чтение открытого SSE на управляемом ReadableStream и смена backend через настройки. Точный состав тестов и оговорки — в журнале, блоки 8.1–8.4Н.
Ближайший оставшийся объём:
1. Проверка задержанного чтения внутри уже открытого SSE-потока.
1. Задержанное чтение открытого SSE проверено в блоке 8.4Н; продолжить со свежести данных ниже.
2. Свежесть данных трафика: ясное различие времени получения, истории и текущего live-измерения.
3. Сводная приёмка этапа 8 и оставшихся ограничений (включая выбор архивных серверов месячного отчёта), профильная регрессия и ReUI checklist.
4. После фиксации результата этапа 8 — этап 9.
+8 -1
View File
@@ -24,13 +24,20 @@
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
| 8 | Трафик и статистика | В работе: выполнены блоки 8.1–8.4М; далее задержка внутри SSE, свежесть и приёмка |
| 8 | Трафик и статистика | В работе: выполнены блоки 8.1–8.4Н; далее свежесть данных и приёмка |
| 9 | VPN, Firewall и Filters | Не начат |
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
## Блок 8.4Н — поздний фрагмент открытого SSE (2026-09-28)
- Добавлен браузерный сценарий с управляемым ReadableStream: первый sample отображается, второй reader.read остаётся ожидающим, пользователь выбирает второй сервер. Проверены abort старой подписки, получение нового sample и завершение старого read после abort. Поздний фрагмент не заменяет данные второго сервера.
- Граница: fetch/ReadableStream подменены в браузере только для flow/live; поток намеренно игнорирует отмену, чтобы проверить защиту после await reader.read. Остальные HTTP-запросы используют прежние route-моки. Это не проверка реального сетевого транспорта или IPFIX/RouterOS.
- Все 20 flow-E2E прошли; ESLint теста, TypeScript (tsc --noEmit) и diff-check чистые. В первом прогоне исправлена ошибка тестового макета: Response должен создаваться до блокировки stream через getReader. Production-код не менялся, build не повторялся.
- Общий план синхронизирован. Следующий шаг: свежесть REST/SSE данных (разделять время получения и время измерения), затем сводная приёмка этапа 8. Этап остаётся открытым. На контрольной точке использовано 24% пятичасового и 4% недельного лимита; локальный коммит без push.
## Блок 8.4М — разрывы основного графика (2026-09-24)
- Существующий E2E пропусков дополнен проверкой SVG основного RX/TX: при [1, 2, null, 3, 4] каждая линия содержит два самостоятельных сегмента. Проверка прошла вместе со Sparkline; lint/diff-check чистые. Production-код не менялся.
+63
View File
@@ -222,3 +222,66 @@ test('Flow: смена сервера отменяет старую SSE-подп
await expect(page.getByText('SSE текущего сервера', { exact: true }).filter({ visible: true })).toBeVisible()
await expect(page.getByLabel('Скорость потоков')).toContainText('7.00 Мбит/с')
})
test('Flow: позднее чтение открытого SSE не заменяет данные нового сервера', async ({ page }) => {
await setup(page)
await page.getByRole('button', { name: '15м', exact: true }).click()
await expect(page.getByText('Категория 15m', { exact: true }).filter({ visible: true })).toBeVisible()
// Deliberately let the mock stream deliver a chunk after abort. This exercises
// the post-read guard, independently of the browser transport's cancellation.
await page.evaluate(() => {
const originalFetch = window.fetch.bind(window)
const state = { pending: false, aborted: false, delivered: false, release: () => {} }
Object.assign(window, { delayedFlowStream: state })
window.fetch = async (input, init) => {
const url = new URL(input instanceof Request ? input.url : String(input), location.href)
if (url.pathname !== '/api/traffic/flow/live') return originalFetch(input, init)
const old = url.searchParams.get('serverId') === '1'
const sample = (late = false) => ({
bpsNow: old ? (late ? 99000000 : 1000000) : 7000000,
bytes: 200, packets: 2, conversations: 1, uniqueSrc: 1, uniqueDst: 1,
topProto: 'TCP', topCategory: old ? (late ? 'Поздний фрагмент' : 'Открытый поток первого') : 'Открытый поток второго',
rxSeries: [1], txSeries: [2], applications: [], protocols: [], sources: [],
destinations: [], interfaces: [], conversationsList: [], ifaces: [], live: true,
})
const encode = (late = false) => new TextEncoder().encode('event: sample\ndata: ' + JSON.stringify(sample(late)) + '\n\n')
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(encode())
if (old) state.release = () => controller.enqueue(encode(true))
},
})
const response = new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } })
if (old) {
init?.signal?.addEventListener('abort', () => { state.aborted = true })
const reader = stream.getReader()
const read = reader.read.bind(reader)
let calls = 0
reader.read = async () => {
const delayed = ++calls === 2
if (delayed) state.pending = true
const result = await read()
if (delayed) { state.pending = false; state.delivered = true }
return result
}
stream.getReader = (() => reader) as typeof stream.getReader
}
return response
}
})
await page.getByRole('button', { name: '5м', exact: true }).click()
await expect(page.getByText('Открытый поток первого', { exact: true }).filter({ visible: true })).toBeVisible()
const streamState = () => page.evaluate(() => {
const state = (window as unknown as { delayedFlowStream: { pending: boolean; aborted: boolean; delivered: boolean } }).delayedFlowStream
return { pending: state.pending, aborted: state.aborted, delivered: state.delivered }
})
await expect.poll(streamState).toEqual({ pending: true, aborted: false, delivered: false })
await page.getByRole('button').filter({ hasText: 'Экспортёр второй' }).click()
await expect(page.getByText('Открытый поток второго', { exact: true }).filter({ visible: true })).toBeVisible()
await expect.poll(streamState).toEqual({ pending: true, aborted: true, delivered: false })
await page.evaluate(() => (window as unknown as { delayedFlowStream: { release: () => void } }).delayedFlowStream.release())
await expect.poll(streamState).toEqual({ pending: false, aborted: true, delivered: true })
await expect(page.getByText('Поздний фрагмент', { exact: true })).toHaveCount(0)
await expect(page.getByText('Открытый поток второго', { exact: true }).filter({ visible: true })).toBeVisible()
await expect(page.getByLabel('Скорость потоков')).toContainText('7.00 Мбит/с')
})