From e5cf05759e94fc23311a64b1fe76b8713c7ead1b Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 04:19:56 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B8=D1=81=D1=82=D0=BE=D1=80=D0=B8=D0=B8=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uptime-speed-history-data-grid.tsx | 24 +++++++-- docs/ux-ui-performance.md | 9 ++-- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 10 +++- docs/ux-ui-stage-12.md | 8 +++ .../ux-ui/screens/large-speed-history-320.png | Bin 0 -> 39197 bytes tests/e2e/large-speed-history.spec.ts | 51 ++++++++++++++++++ 7 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 docs/ux-ui/screens/large-speed-history-320.png create mode 100644 tests/e2e/large-speed-history.spec.ts diff --git a/components/data-grids/uptime-speed-history-data-grid.tsx b/components/data-grids/uptime-speed-history-data-grid.tsx index 709c0045..bd4408eb 100644 --- a/components/data-grids/uptime-speed-history-data-grid.tsx +++ b/components/data-grids/uptime-speed-history-data-grid.tsx @@ -1,8 +1,8 @@ "use client" -import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" +import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" -import { useMemo } from "react" +import { useMemo, useEffect } from "react" import { type ColumnDef, useTable, @@ -223,19 +223,33 @@ function UptimeSpeedHistoryDataGrid({ runs, servers }: UptimeSpeedHistoryDataGri const table = useTable({ features: dataGridFeatures, - manualPagination: true, + manualPagination: false, + autoResetPageIndex: false, data: runs, columns, getRowId: (row) => row.id, - initialState: { sorting: [{ id: "startedAt", desc: true }] }, + initialState: { pagination: { pageIndex: 0, pageSize: 50 }, sorting: [{ id: "startedAt", desc: true }] }, }) + const { pageIndex, pageSize } = table.state.pagination + useEffect(() => { + const lastPage = Math.max(0, Math.ceil(runs.length / pageSize) - 1) + if (pageIndex > lastPage) table.setPageIndex(lastPage) + }, [runs.length, pageSize, pageIndex, table]) + return ( 50 || pageSize !== 50} tableClassNames={{ bodyRow: cn("group/row text-xs") }} - /> + > + + + + + + ) } diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index d457f072..43dcfa44 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -31,7 +31,7 @@ | [ospf-bfd-data-grid.tsx](../components/data-grids/ospf-bfd-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [ospf-neighbors-data-grid.tsx](../components/data-grids/ospf-neighbors-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [ospf-routes-data-grid.tsx](../components/data-grids/ospf-routes-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | -| [probes-schedule-data-grid.tsx](../components/data-grids/probes-schedule-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | +| [probes-schedule-data-grid.tsx](../components/data-grids/probes-schedule-data-grid.tsx) | Только demo ScheduleTab/INIT_RULES3; live использует ProbesSpeedProbesDataGrid, проверить его consumer1000 | | [recursive-routes-data-grid.tsx](../components/data-grids/recursive-routes-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [route-optimizer-comm-recs-data-grid.tsx](../components/data-grids/route-optimizer-comm-recs-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [route-optimizer-full-routes-data-grid.tsx](../components/data-grids/route-optimizer-full-routes-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | @@ -39,13 +39,12 @@ | [route-optimizer-wan-matrix-data-grid.tsx](../components/data-grids/route-optimizer-wan-matrix-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [subusers-data-grid.tsx](../components/data-grids/subusers-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [traffic-flows-data-grid.tsx](../components/data-grids/traffic-flows-data-grid.tsx) | FlowAnalyticsPanel получает conversationsList с backend cap50; API page/limit здесь нет | -| [uptime-speed-history-data-grid.tsx](../components/data-grids/uptime-speed-history-data-grid.tsx) | Реальный GET runs ограничен200; локальные новые run тоже ограничены20, проверить200 вместо выдуманного1000 | | [users-data-grid.tsx](../components/data-grids/users-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [vxlan-data-grid.tsx](../components/data-grids/vxlan-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [wireguard-data-grid.tsx](../components/data-grids/wireguard-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [wireguard-peers-grid.tsx](../components/data-grids/wireguard-peers-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | -Осталось25 специализированных grids; ни один из оставшегося перечня не принят этим документом на больших данных. CompactDataGrid дополнительно проверен на повторном получении100 active probes: страница сохраняется; сокращение100→1 ограничивает индекс. Speed probes и uptime resources приняты в блоках12.3.7–12.3.9 и исключены из оставшегося перечня. Следующие большие профили: probes-schedule, затем сетевые/admin grids с учётом границ API. Общий журнал: [ux-ui-progress.md](ux-ui-progress.md). +Осталось24 специализированных grids; ни один из оставшегося перечня не принят этим документом на больших данных. CompactDataGrid дополнительно проверен на повторном получении100 active probes: страница сохраняется; сокращение100→1 ограничивает индекс. Speed probes и uptime resources приняты в блоках12.3.7–12.3.9 и исключены из оставшегося перечня. Следующие большие профили: probes-schedule, затем сетевые/admin grids с учётом границ API. Общий журнал: [ux-ui-progress.md](ux-ui-progress.md). 12.3.6:1000 speed probes baseline —1000 rendered rows/23590 DOM nodes/3741ms dev ready; профиль collector-probes4 cases и ESLint прошли. Далее12.3.7 ограничение DOM speed grid; этот baseline не означает принятую производительность. @@ -59,8 +58,10 @@ ## Уточнение ограниченных источников перед общей регрессией -Статический аудит исходников на c4110fa: schedulerGridRows строится из SCHEDULER_JOB_KEYS (11), поэтому отсутствие client pagination не означает1000 строк. TrafficFlowsDataGrid получает analytics.conversationsList, а buildFlowAnalyticsUncached задаёт top=min(50,max(10,settings.topN)) и slice(0,top); page/limit в этом пути нет. Старый /traffic/flows listFlowTalkers — другой источник с settings.topN, его нельзя смешивать с текущей таблицей analytics. Uptime speed history GET /uptime/speed-test/runs использует DB limit200; frontend локальный run список ограничен20. Эти ограничения установлены чтением кода, не нагрузочным испытанием PG. Остальные строки карты требуют проверки по своим потребителям;25 manualPagination:true файлов не означают25 одинаковых дефектов. +Статический аудит исходников на c4110fa: schedulerGridRows строится из SCHEDULER_JOB_KEYS (11), поэтому отсутствие client pagination не означает1000 строк. TrafficFlowsDataGrid получает analytics.conversationsList, а buildFlowAnalyticsUncached задаёт top=min(50,max(10,settings.topN)) и slice(0,top); page/limit в этом пути нет. Старый /traffic/flows listFlowTalkers — другой источник с settings.topN, его нельзя смешивать с текущей таблицей analytics. Uptime speed history GET /uptime/speed-test/runs использует DB limit200; frontend локальный run список ограничен20. Эти ограничения установлены чтением кода, не нагрузочным испытанием PG. Остальные строки карты требуют проверки по своим потребителям;После history-профиля осталось24 manualPagination:true файла; они не означают24 одинаковых дефекта. Statistics pivot в backend/src/services/statistics-aggregate.ts ограничен PIVOT_ROW_CAP=50 и PIVOT_COL_CAP=15; при превышении добавляются агрегаты __other__. Проверять фактическую границу51×16, прокрутку и переходы к срезам, а не произвольную матрицу1000×1000. Это статический аудит источника; browser evidence широкой pivot ещё требуется. 12.3.13: pivot51×16/17311 JSON bytes принята в описанных локальных границах.50 rows/2574 nodes desktop и2313 mobile; крайние колонки доступны через штатный DataGridScrollArea, агрегаты Other без ложных действий. Sort/page2/два измерения/Back/reload/320px/axe проверены. Фактический cap backend исключает потребность в произвольной матрице1000×1000; реальная скорость PG не измерялась. + +12.3.14: history200/73075 JSON bytes —200→50 rows,7583→2361 DOM nodes; final dev ready1271ms. Global TX sort/page4/refresh page2/200→1/320px/ScrollArea/axe проверены. Это DOM приёмка, не принятие legacy null→0 rates и общей очистки двух GET при ошибке reloadSpeedData. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 6df26471..4ae2f944 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-04 -Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. Следующий блок12.3.14:history200, затем probes-schedule и итоговая матрица;12.4/12.5 ещё не завершены. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. +Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. Следующий блок12.3.15:live диагностическое расписание1000, затем admin/network grids и отдельная достоверность history;12.4/12.5 ещё не завершены. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 4dbc568d..e5058a45 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -29,13 +29,21 @@ | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | -| 12 | Итоговая доступность, адаптивность, производительность и регрессия | В работе;12.1/12.2.1–12.2.10 и12.3.1–12.3.5 и12.3.7–12.3.12 приняты в описанных локальных границах;12.4.1 main регрессия и профильный повтор записаны;12.4.2 context205 и unit28 прошли;12.3.13 wide pivot принята локально | +| 12 | Итоговая доступность, адаптивность, производительность и регрессия | В работе;12.1/12.2.1–12.2.10 и12.3.1–12.3.5 и12.3.7–12.3.12 приняты в описанных локальных границах;12.4.1 main регрессия и профильный повтор записаны;12.4.2 context205 и unit28 прошли;12.3.13 wide pivot и12.3.14 history200 приняты локально | +## 12.3.14 — история200 speed-test запусков (2026-10-04) + +История GET runs имеет backend limit200. Baseline200 rows/7583 DOM nodes/1339ms dev ready на fixture73075 JSON bytes. Штатная client pagination50, autoResetPageIndex=false и clamp при сокращении дают50 rows/2361 nodes/1271ms в финальном прогоне. ReUI ScrollArea открывает крайние колонки на320px, footer остаётся вне области прокрутки. Правила запуска сетевых операций не менялись. + +Проверены page4 с первой записью, глобальная числовая сортировка TX, refresh на page2 без сброса,200→1/clamp, scroll/reflow320, tagged axe grid и отсутствие POST при refresh Speed.54 existing uptime browser прошли; новый профиль первоначально использовал неверное имя refresh («Обновить данные» вместо «Обновить»), после исправления прошёл полностью. Typecheck/ESLint и production frontend/contracts build34 страницы прошли. Снимок просмотрен. Повтор полного main/context после этой delta не выполнялся; отчёты12.4 сохраняют границы HEAD. + +Статический аудит следующего grid: ProbesScheduleDataGrid используется только в demo ScheduleTab (INIT_RULES3), live использует уже ограниченный ProbesSpeedProbesDataGrid. Следующий блок12.3.15 — проверить1000 speed probes непосредственно в live расписании диагностики, refresh/error cache/уменьшение; не создавать вымышленное live GRE расписание. Затем admin/network grids. Отдельное ограничение достоверности истории: reloadSpeedData очищает обе коллекции при отказе одного GET, backend legacy rates использует null→0. Эта приёмка ограничивает DOM, не принимает указанное поведение данных; оно требует отдельного fullstack блока после профилей. Локальный коммит, без push. + ## 12.3.13 — широкая pivot и доступные действия ячеек (2026-10-04) Профиль фактического backend cap51×16 (50 строк/15 колонок плюс Other) воспроизвёл отсутствие горизонтальной области прокрутки. CompactDataGrid получает отдельную horizontalScroll опцию со штатными DataGridContainer/DataGridScrollArea/DataGridTable; включена только у StatisticsPivotGrid. Pagination остаётся за пределами ScrollArea через DataGridShell. Primitive/theme не менялись. Агрегаты Other теперь текст, не кнопки без действия; имя обычной кнопки содержит оба измерения, значение и действие. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index a4736805..b4412801 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -249,3 +249,11 @@ Fixture17311 JSON bytes,50 строк в DOM; после исправления2 24 browser statistics/large/migrated-tables прошли; финальные2 pivot повторены после типизации injected axe. Tagged WCAG axe области grid прошёл на320/1440; снимки просмотрены. Typecheck/ESLint и production frontend/contracts34 страницы/backend build прошли. Main/context отчёты12.4.1/12.4.2 относятся к предыдущему production HEAD; эта локальная delta проверена профильными24+2, общая регрессия после неё не повторялась. Следующий блок12.3.14: проверить200 speed-test history по реальному backend cap, ограничить строки DOM и сохранить сортировку/страницы/320px. Затем probes-schedule и оставшиеся специализированные grids;12.4 итоговая матрица/12.5 внешняя приёмка ещё не завершены. Локальный коммит без push. + +## 12.3.14 — история200 speed-test запусков (2026-10-04) + +История GET runs имеет backend limit200. Baseline200 rows/7583 DOM nodes/1339ms dev ready на fixture73075 JSON bytes. Штатная client pagination50, autoResetPageIndex=false и clamp при сокращении дают50 rows/2361 nodes/1271ms в финальном прогоне. ReUI ScrollArea открывает крайние колонки на320px, footer остаётся вне области прокрутки. Правила запуска сетевых операций не менялись. + +Проверены page4 с первой записью, глобальная числовая сортировка TX, refresh на page2 без сброса,200→1/clamp, scroll/reflow320, tagged axe grid и отсутствие POST при refresh Speed.54 existing uptime browser прошли; новый профиль первоначально использовал неверное имя refresh («Обновить данные» вместо «Обновить»), после исправления прошёл полностью. Typecheck/ESLint и production frontend/contracts build34 страницы прошли. Снимок просмотрен. Повтор полного main/context после этой delta не выполнялся; отчёты12.4 сохраняют границы HEAD. + +Статический аудит следующего grid: ProbesScheduleDataGrid используется только в demo ScheduleTab (INIT_RULES3), live использует уже ограниченный ProbesSpeedProbesDataGrid. Следующий блок12.3.15 — проверить1000 speed probes непосредственно в live расписании диагностики, refresh/error cache/уменьшение; не создавать вымышленное live GRE расписание. Затем admin/network grids. Отдельное ограничение достоверности истории: reloadSpeedData очищает обе коллекции при отказе одного GET, backend legacy rates использует null→0. Эта приёмка ограничивает DOM, не принимает указанное поведение данных; оно требует отдельного fullstack блока после профилей. Локальный коммит, без push. diff --git a/docs/ux-ui/screens/large-speed-history-320.png b/docs/ux-ui/screens/large-speed-history-320.png new file mode 100644 index 0000000000000000000000000000000000000000..5e7e3ffea9edd285774f016894ad7c1f45e4fd98 GIT binary patch literal 39197 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 EKW@3+NdN!< literal 0 HcmV?d00001 diff --git a/tests/e2e/large-speed-history.spec.ts b/tests/e2e/large-speed-history.spec.ts new file mode 100644 index 00000000..98fe2981 --- /dev/null +++ b/tests/e2e/large-speed-history.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from "@playwright/test" +import axe from "axe-core" +import { mkdirSync } from "node:fs" + +test("Speed history200: DOM, сортировка, refresh, уменьшение и320px", async ({ page }, info) => { + const servers = [1, 2].map(id => ({ id, name: id === 1 ? "Источник" : "Назначение", host: "192.0.2." + id, type: "exit-node", enabled: true, country: "RU", site: "TEST", sessions: 0, status: "online", wanUplinks: [] })) + let runs = Array.from({ length: 200 }, (_, i) => ({ id: String(i + 1), srcServerId: "1", dstServerId: "2", srcInterface: "ether1", dstInterface: "ether1", srcInterfaceAddress: "10.0.0.1", dstInterfaceAddress: "10.0.0.2", protocol: "tcp", direction: "both", durationSec: 10, txAvgMbps: 200 - i, rxAvgMbps: i, status: "done", error: null, afterBtPing: { rttMs: 0, lossPct: 0, error: null }, createdAt: new Date(Date.parse("2026-10-03T00:00:00Z") + i * 60000).toISOString() })) + let calls = 0; const writes: string[] = [] + await page.route("**/health", r => r.fulfill({ json: { ok: true } })) + await page.route("**/api/**", r => { + const path = new URL(r.request().url()).pathname + if (r.request().method() !== "GET") writes.push(path) + if (path === "/api/uptime/speed-test/runs") calls++ + return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? servers : path === "/api/uptime/overview" ? { probes: [], resources: [] } : path === "/api/uptime/speed-probes" ? { probes: [] } : path === "/api/uptime/speed-test/runs" ? { runs } : {} }) + }) + const start = Date.now(); await page.goto("/uptime?tab=speed") + const rows = page.locator("tr[data-row-id]") + await expect(page.locator('tr[data-row-id="200"]')).toBeVisible() + const sample = { fixtureRows: 200, fixtureBytes: Buffer.byteLength(JSON.stringify(runs)), renderedRows: await rows.count(), domNodes: await page.locator("*").count(), readyMs: Date.now() - start } + console.log(JSON.stringify(sample)); await info.attach("speed-history-metrics", { body: JSON.stringify(sample), contentType: "application/json" }) + expect(sample.renderedRows).toBe(50); expect(sample.domNodes).toBeLessThan(4000) + await page.getByRole("button", { name: "Перейти на страницу 4", exact: true }).click() + await expect(rows.last()).toHaveAttribute("data-row-id", "1") + await page.getByRole("button", { name: "Перейти на страницу 1", exact: true }).click() + await page.getByRole("table").getByRole("button", { name: "TX avg", exact: true }).click() + await expect(rows.first()).toHaveAttribute("data-row-id", "1") + await page.getByRole("button", { name: "Следующая страница", exact: true }).click() + await expect(rows.first()).toHaveAttribute("data-row-id", "51") + const before = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect.poll(() => calls).toBeGreaterThan(before) + await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeEnabled() + await expect(page.getByText("51–100 из 200", { exact: true })).toBeVisible() + await expect(rows.first()).toHaveAttribute("data-row-id", "51") + await page.setViewportSize({ width: 320, height: 1000 }) + const scroll = page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]') + await expect(scroll).toHaveCount(1) + expect(await scroll.evaluate(el => el.scrollWidth > el.clientWidth)).toBe(true) + await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth }) + await expect.poll(() => scroll.evaluate(el => el.scrollLeft)).toBeGreaterThan(0) + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await page.addScriptTag({ content: axe.source }) + const audit = await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[data-slot="data-grid"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } })) + expect(audit.violations).toEqual([]) + await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }); mkdirSync("docs/ux-ui/screens", { recursive: true }) + await page.screenshot({ path: "docs/ux-ui/screens/large-speed-history-320.png", animations: "disabled" }) + runs = [runs[199]]; const previous = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect.poll(() => calls).toBeGreaterThan(previous) + await expect(rows).toHaveCount(1); await expect(rows.first()).toHaveAttribute("data-row-id", "200") + await expect(page.getByRole("button", { name: "Следующая страница", exact: true })).toHaveCount(0) + expect(writes).toEqual([]) +})