fix(ui): ограничить полные маршруты с сохранением индексов
This commit is contained in:
1 parent
82f620ac0b
commit
a16bfae805
8 files changed
+95
-18
No files matched your search
@@ -1,8 +1,8 @@
|
|||||||
"use client"
|
"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, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import {
|
import {
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
useTable,
|
useTable,
|
||||||
@@ -76,13 +76,14 @@ function RouteOptimizerFullRoutesDataGrid({
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: "index",
|
id: "index",
|
||||||
|
size: 112,
|
||||||
header: () => <span className="text-[11px] font-medium text-muted-foreground"># Маршрут</span>,
|
header: () => <span className="text-[11px] font-medium text-muted-foreground"># Маршрут</span>,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const isBest = row.original.id === bestId || row.original._index === 0
|
const isBest = row.original.id === bestId || row.original._index === 0
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[10px] font-mono text-muted-foreground w-4">
|
<span className="text-[10px] font-mono text-muted-foreground min-w-6 tabular-nums">
|
||||||
{row.original._index + 1}
|
{row.original._index + 1}
|
||||||
</span>
|
</span>
|
||||||
{isBest && (
|
{isBest && (
|
||||||
@@ -97,6 +98,7 @@ function RouteOptimizerFullRoutesDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "wanJh",
|
id: "wanJh",
|
||||||
|
size: 264,
|
||||||
header: () => <span className="text-[11px] font-medium text-muted-foreground">WAN → JH</span>,
|
header: () => <span className="text-[11px] font-medium text-muted-foreground">WAN → JH</span>,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -118,6 +120,7 @@ function RouteOptimizerFullRoutesDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "jhExit",
|
id: "jhExit",
|
||||||
|
size: 240,
|
||||||
header: () => <span className="text-[11px] font-medium text-muted-foreground">JH → Exit</span>,
|
header: () => <span className="text-[11px] font-medium text-muted-foreground">JH → Exit</span>,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -139,6 +142,7 @@ function RouteOptimizerFullRoutesDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "totalPing",
|
id: "totalPing",
|
||||||
|
size: 144,
|
||||||
accessorFn: (row) => row.hw.pingMs + row.je.pingMs,
|
accessorFn: (row) => row.hw.pingMs + row.je.pingMs,
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataGridSortHeader column={column} title="Ping модели" className="mx-auto" />
|
<DataGridSortHeader column={column} title="Ping модели" className="mx-auto" />
|
||||||
@@ -168,6 +172,7 @@ function RouteOptimizerFullRoutesDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "bw",
|
id: "bw",
|
||||||
|
size: 144,
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="text-[11px] font-medium text-muted-foreground block text-center">
|
<span className="text-[11px] font-medium text-muted-foreground block text-center">
|
||||||
BW (мин)
|
BW (мин)
|
||||||
@@ -192,6 +197,7 @@ function RouteOptimizerFullRoutesDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "score",
|
id: "score",
|
||||||
|
size: 80,
|
||||||
accessorKey: "score",
|
accessorKey: "score",
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataGridSortHeader column={column} title="Score" className="mx-auto" />
|
<DataGridSortHeader column={column} title="Score" className="mx-auto" />
|
||||||
@@ -209,6 +215,7 @@ function RouteOptimizerFullRoutesDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "prob",
|
id: "prob",
|
||||||
|
size: 104,
|
||||||
accessorKey: "probabilityOptimal",
|
accessorKey: "probabilityOptimal",
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataGridSortHeader column={column} title="Индекс" className="mx-auto" />
|
<DataGridSortHeader column={column} title="Индекс" className="mx-auto" />
|
||||||
@@ -229,6 +236,7 @@ function RouteOptimizerFullRoutesDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "confidence",
|
id: "confidence",
|
||||||
|
size: 176,
|
||||||
accessorKey: "confidence",
|
accessorKey: "confidence",
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataGridSortHeader column={column} title="Категория модели" className="mx-auto" />
|
<DataGridSortHeader column={column} title="Категория модели" className="mx-auto" />
|
||||||
@@ -250,28 +258,41 @@ function RouteOptimizerFullRoutesDataGrid({
|
|||||||
|
|
||||||
const table = useTable({
|
const table = useTable({
|
||||||
features: dataGridFeatures,
|
features: dataGridFeatures,
|
||||||
manualPagination: true,
|
manualPagination: false,
|
||||||
|
autoResetPageIndex: false,
|
||||||
|
initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["index"], end: [] } },
|
||||||
data: indexed,
|
data: indexed,
|
||||||
columns,
|
columns,
|
||||||
getRowId: (row) => row.id,
|
getRowId: (row) => row.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { pageIndex, pageSize } = table.state.pagination
|
||||||
|
useEffect(() => {
|
||||||
|
const lastPage = Math.max(0, Math.ceil(indexed.length / pageSize) - 1)
|
||||||
|
if (pageIndex > lastPage) table.setPageIndex(lastPage)
|
||||||
|
}, [indexed.length, pageIndex, pageSize, table])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<section aria-label="Полные маршруты оптимизатора" className="min-w-0">
|
||||||
<DataGridShell
|
<DataGridShell
|
||||||
table={table}
|
table={table}
|
||||||
recordCount={visibleRoutes.length}
|
recordCount={visibleRoutes.length}
|
||||||
|
pagination={expanded && (visibleRoutes.length > 50 || pageSize !== 50)}
|
||||||
tableClassNames={{
|
tableClassNames={{
|
||||||
headerRow: "border-b bg-muted/20",
|
headerRow: "border-b bg-muted/20",
|
||||||
bodyRow: cn("group/row hover:bg-muted/30", "has-[[data-route-best=true]]:bg-success/5"),
|
bodyRow: cn("group/row hover:bg-muted/30", "has-[[data-route-best=true]]:bg-success/5"),
|
||||||
}}
|
}}
|
||||||
tableLayout={{ dense: true }}
|
tableLayout={{ dense: true, width: "fixed", columnsPinnable: true }}
|
||||||
/>
|
>
|
||||||
|
<DataGridContainer border={false}>
|
||||||
|
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
|
||||||
|
</DataGridContainer>
|
||||||
|
</DataGridShell>
|
||||||
{routes.length > 5 && (
|
{routes.length > 5 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => { setExpanded((v) => !v); table.setPageIndex(0) }}
|
||||||
className="w-full py-2 text-xs text-muted-foreground hover:text-foreground transition-colors border-t flex items-center justify-center gap-1"
|
className="w-full min-h-11 py-2 text-xs text-muted-foreground hover:text-foreground transition-colors border-t flex items-center justify-center gap-1"
|
||||||
>
|
>
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<>
|
<>
|
||||||
@@ -281,12 +302,12 @@ function RouteOptimizerFullRoutesDataGrid({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ChevronDownIcon aria-hidden="true" className="size-3" />
|
<ChevronDownIcon aria-hidden="true" className="size-3" />
|
||||||
Показать все {routes.length} комбинаций
|
{routes.length > 50 ? `Открыть все ${routes.length} комбинаций` : `Показать все ${routes.length} комбинаций`}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
## Оставшиеся специализированные grids
|
## Оставшиеся специализированные grids
|
||||||
|
|
||||||
Аудит12.3.36 от2026-10-04: точный поиск `manualPagination: true` в components/data-grids —8 файлов после12.3.53. Это не8 доказанных дефектов: есть фиксированный источник, demo, неподключённый компонент и ограниченные срезы. Файлы ниже сопоставлены с действующими JSX-потребителями; ограничения установлены чтением кода, не нагрузочной приёмкой API.
|
Аудит12.3.36 от2026-10-04: точный поиск `manualPagination: true` в components/data-grids —7 файлов после12.3.57. Это не7 доказанных дефектов: есть фиксированный источник, demo, неподключённый компонент и ограниченные срезы. Файлы ниже сопоставлены с действующими JSX-потребителями; ограничения установлены чтением кода, не нагрузочной приёмкой API.
|
||||||
|
|
||||||
| Файл | Источник и необходимая проверка |
|
| Файл | Источник и необходимая проверка |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
| [gre-pools-data-grid.tsx](../components/data-grids/gre-pools-data-grid.tsx) | В live displayPools=[]; только mockGrePools в demo. Приоритета live1000 нет. |
|
| [gre-pools-data-grid.tsx](../components/data-grids/gre-pools-data-grid.tsx) | В live displayPools=[]; только mockGrePools в demo. Приоритета live1000 нет. |
|
||||||
| [probes-schedule-data-grid.tsx](../components/data-grids/probes-schedule-data-grid.tsx) | Только demo ScheduleTab/INIT_RULES3; live использует ProbesSpeedSchedule. Demo список расширяется пользователем; не доказательство live API. |
|
| [probes-schedule-data-grid.tsx](../components/data-grids/probes-schedule-data-grid.tsx) | Только demo ScheduleTab/INIT_RULES3; live использует ProbesSpeedSchedule. Demo список расширяется пользователем; не доказательство live API. |
|
||||||
| [route-optimizer-comm-recs-data-grid.tsx](../components/data-grids/route-optimizer-comm-recs-data-grid.tsx) | Live commRecs slice(0,24) на HomeCard, не на страницу. Проверить количество HomeCard и честность ограничения рекомендаций. |
|
| [route-optimizer-comm-recs-data-grid.tsx](../components/data-grids/route-optimizer-comm-recs-data-grid.tsx) | Live commRecs slice(0,24) на HomeCard, не на страницу. Проверить количество HomeCard и честность ограничения рекомендаций. |
|
||||||
| [route-optimizer-full-routes-data-grid.tsx](../components/data-grids/route-optimizer-full-routes-data-grid.tsx) | Live сочетания WAN×jumpHost×exitNode на HomeCard; ограничение DOM не устраняет стоимость расчёта полного массива. |
|
|
||||||
| [route-optimizer-wan-matrix-data-grid.tsx](../components/data-grids/route-optimizer-wan-matrix-data-grid.tsx) | Live сочетания WAN×jumpHost на HomeCard; динамические столбцы/много HomeCard требуют собственного профиля. |
|
| [route-optimizer-wan-matrix-data-grid.tsx](../components/data-grids/route-optimizer-wan-matrix-data-grid.tsx) | Live сочетания WAN×jumpHost на HomeCard; динамические столбцы/много HomeCard требуют собственного профиля. |
|
||||||
| [subusers-data-grid.tsx](../components/data-grids/subusers-data-grid.tsx) | Поиск импортов/JSX в app/components не нашёл потребителя: компонент экспортируется, но не подключён. Не доказывает дефект экрана /users. |
|
| [subusers-data-grid.tsx](../components/data-grids/subusers-data-grid.tsx) | Поиск импортов/JSX в app/components не нашёл потребителя: компонент экспортируется, но не подключён. Не доказывает дефект экрана /users. |
|
||||||
| [traffic-flows-data-grid.tsx](../components/data-grids/traffic-flows-data-grid.tsx) | FlowAnalyticsPanel/conversationsList: backend traffic-flow-analytics.ts top=min(50,max(10,settings.topN)), slice(0,top). Это top slice, не полный каталог/серверная pagination. Проверить сообщение об ограничении50 и связывание с графиками. |
|
| [traffic-flows-data-grid.tsx](../components/data-grids/traffic-flows-data-grid.tsx) | FlowAnalyticsPanel/conversationsList: backend traffic-flow-analytics.ts top=min(50,max(10,settings.topN)), slice(0,top). Это top slice, не полный каталог/серверная pagination. Проверить сообщение об ограничении50 и связывание с графиками. |
|
||||||
@@ -107,3 +106,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич
|
|||||||
12.3.55:1000 HomeRouterCard →5/1870 page DOM/1606ms dev ready, full search/URL/refresh/cache/clamp/320px/axe/pin preserved.23+8 main/8 context/type/lint/build. Полный CPU расчёт и1000 native device options остаются; далее1000 full routes.
|
12.3.55:1000 HomeRouterCard →5/1870 page DOM/1606ms dev ready, full search/URL/refresh/cache/clamp/320px/axe/pin preserved.23+8 main/8 context/type/lint/build. Полный CPU расчёт и1000 native device options остаются; далее1000 full routes.
|
||||||
|
|
||||||
12.3.56:WAN10×JH10×Exit10/21 servers/4261 bytes →1000 expanded full-route rows/33049 table DOM/33780 page DOM/3738ms dev ready.1 browser/test lint; baseline, далее12.3.57.
|
12.3.56:WAN10×JH10×Exit10/21 servers/4261 bytes →1000 expanded full-route rows/33049 table DOM/33780 page DOM/3738ms dev ready.1 browser/test lint; baseline, далее12.3.57.
|
||||||
|
|
||||||
|
12.3.57:1000 full routes→50 rows/1699 table nodes/2461 page DOM/1853ms dev ready.6+1 browser/type/lint/build, global sort/index/page20/refresh/clamp/320px/axe.7 manualPagination:true, далее WAN matrix/CPU/24 commRecs.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# План реализации UX/UI по пятичасовым окнам
|
# План реализации UX/UI по пятичасовым окнам
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-04: 12.3.56 baseline1000 full routes сохранён:33049 table DOM/33780 page DOM/3738ms dev ready;1 browser/test lint. Далее12.3.57 — client pagination раскрытого списка/полная сортировка/refresh/clamp/320px; затем WAN matrix/общая регрессия. Внешняя12.5 отдельно.
|
Актуальное продолжение 2026-10-04: 12.3.57 принят локально:1000 full routes→50/1699 table DOM/2461 page DOM, global sort/индекс/page20/refresh/clamp/320px/axe.6+1 browser/type/lint/build. Далее12.3.58 — профиль WAN matrix/динамических столбцов и CPU комбинирования; честность24 рекомендаций/полная регрессия после delta.7 specialized manualPagination:true остаются. Внешняя12.5 отдельно.
|
||||||
|
|
||||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-04. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-04. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# UX/UI: журнал реализации
|
# UX/UI: журнал реализации
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-04: 12.3.56 baseline1000 full routes сохранён:33049 table DOM/33780 page DOM/3738ms dev ready;1 browser/test lint. Далее12.3.57 — client pagination раскрытого списка/полная сортировка/refresh/clamp/320px; затем WAN matrix/общая регрессия. Внешняя12.5 отдельно.
|
Актуальное продолжение 2026-10-04: 12.3.57 принят локально:1000 full routes→50/1699 table DOM/2461 page DOM, global sort/индекс/page20/refresh/clamp/320px/axe.6+1 browser/type/lint/build. Далее12.3.58 — профиль WAN matrix/динамических столбцов и CPU комбинирования; честность24 рекомендаций/полная регрессия после delta.7 specialized manualPagination:true остаются. Внешняя12.5 отдельно.
|
||||||
|
|
||||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения.
|
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения.
|
||||||
|
|
||||||
|
|||||||
@@ -651,3 +651,12 @@ OptimizerHomesList показывает5 карточек на страницу,
|
|||||||
## 12.3.56 — исходный профиль1000 полных маршрутов (2026-10-04)
|
## 12.3.56 — исходный профиль1000 полных маршрутов (2026-10-04)
|
||||||
|
|
||||||
Полный валидный servers DTO21 устройство/4261 bytes:WAN10×JH10×Exit10 создаёт1000 full routes.1 browser/test lint passed; раскрытый список1000 rows/33049 table DOM/33780 page DOM/3738ms dev ready. [Профиль](ux-ui/acceptance/optimizer-routes-baseline-2026-10-04.json) сохранён. Это baseline, не performance acceptance; dev latency не production benchmark. Продуктовый код не менялся, build повторно не нужен. Далее12.3.57 ограничить DOM раскрытого списка50/page без потери global sort/индекса/best/refresh/clamp; затем WAN matrix/CPU combinatorics/24 commRecs и общая регрессия. Внешняя12.5 отдельно. Локальный коммит без push.
|
Полный валидный servers DTO21 устройство/4261 bytes:WAN10×JH10×Exit10 создаёт1000 full routes.1 browser/test lint passed; раскрытый список1000 rows/33049 table DOM/33780 page DOM/3738ms dev ready. [Профиль](ux-ui/acceptance/optimizer-routes-baseline-2026-10-04.json) сохранён. Это baseline, не performance acceptance; dev latency не production benchmark. Продуктовый код не менялся, build повторно не нужен. Далее12.3.57 ограничить DOM раскрытого списка50/page без потери global sort/индекса/best/refresh/clamp; затем WAN matrix/CPU combinatorics/24 commRecs и общая регрессия. Внешняя12.5 отдельно. Локальный коммит без push.
|
||||||
|
|
||||||
|
|
||||||
|
## 12.3.57 — страницы полных маршрутов оптимизатора (2026-10-04)
|
||||||
|
|
||||||
|
ReUI client pagination50 только раскрытого списка, autoResetPageIndex=false/clamp, fixed widths/ScrollArea, закреплённый исходный номер маршрута112px. _index/bestId вычислены по полному исходному списку, не странице/sort. Сворачивание/раскрытие явно reset page0; для1000 текст «Открыть все1000 комбинаций» не обещает одновременный DOM. Для<=50 прежний текст сохранён. Toggle44px. Расчёт модели/сети и полный массив не изменены.
|
||||||
|
|
||||||
|
6/6 профильных table/pin/empty/unknown cases и финальный large1/1 passed. Первые ошибочные expectations теста считали min index уникальным; исправлены по существующим равным значениям. Проверены50 initial/счётчик номера51/refresh/page20 951–1000, collapse5/open50, глобальный min sort включает номера за исходной page1 при ties,320px/pinning/axe/shrink1000→1/no writes.21 devices/WAN10×JH10×Exit10/4261 bytes →50 rows/1699 table DOM/2461 page DOM/1853ms dev ready вместо33049/33780. TypeScript без incremental/ESLint grid+test/production build passed. [Профиль](ux-ui/acceptance/optimizer-routes-pages-2026-10-04.json), [320px](ux-ui/screens/optimizer-routes-pages-320.png) сохранены и просмотрены. Dev latency не production benchmark.
|
||||||
|
|
||||||
|
Осталось7 manualPagination:true specialized grids. Следующий12.3.58 — WAN matrix/динамические JH columns/CPU combinatorics. Срез commRecs24/восстановление вкладки карточки/native device options1000 и полный массив в памяти остаются. Общая main/context после всех delta и внешняя12.5 не закрыты. Локальный коммит без push.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"combinations": 1000,
|
||||||
|
"servers": 21,
|
||||||
|
"bytes": 4261,
|
||||||
|
"rendered": 50,
|
||||||
|
"tableNodes": 1699,
|
||||||
|
"pageNodes": 2461,
|
||||||
|
"readyMs": 1853
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -1,26 +1,63 @@
|
|||||||
import { test, expect } from "@playwright/test"
|
import { test, expect } from "@playwright/test"
|
||||||
import { writeFileSync } from "node:fs"
|
import { writeFileSync } from "node:fs"
|
||||||
|
import axe from "axe-core"
|
||||||
import { optimizerServersSchema } from "../../lib/route-optimizer-response"
|
import { optimizerServersSchema } from "../../lib/route-optimizer-response"
|
||||||
|
|
||||||
test("Оптимизатор1000 маршрутов: WAN10 × JH10 × Exit10", async ({ page }, info) => {
|
test("Оптимизатор1000 маршрутов: WAN10 × JH10 × Exit10", async ({ page }, info) => {
|
||||||
test.setTimeout(90000)
|
test.setTimeout(90000)
|
||||||
const common = { host: "192.0.2.1", enabled: true, status: "online", latency: 10, country: "RU", site: "TEST", model: "CHR" }
|
const common = { host: "192.0.2.1", enabled: true, status: "online", latency: 10, country: "RU", site: "TEST", model: "CHR" }
|
||||||
const servers = optimizerServersSchema.parse([
|
let servers = optimizerServersSchema.parse([
|
||||||
{ ...common, id: 1, name: "Домашний роутер", type: "home-router", wanUplinks: Array.from({ length: 10 }, (_, i) => ({ id: `wan${i+1}`, name: `WAN${i+1}`, isp: "ISP", iface: `ether${i+1}`, ip: `198.51.100.${i+1}`, maxDl: 100, maxUl: 100 })) },
|
{ ...common, id: 1, name: "Домашний роутер", type: "home-router", wanUplinks: Array.from({ length: 10 }, (_, i) => ({ id: `wan${i+1}`, name: `WAN${i+1}`, isp: "ISP", iface: `ether${i+1}`, ip: `198.51.100.${i+1}`, maxDl: 100, maxUl: 100 })) },
|
||||||
...Array.from({ length: 10 }, (_, i) => ({ ...common, id: i+2, name: `Jump ${i+1}`, type: "jump-host" })),
|
...Array.from({ length: 10 }, (_, i) => ({ ...common, id: i+2, name: `Jump ${i+1}`, type: "jump-host" })),
|
||||||
...Array.from({ length: 10 }, (_, i) => ({ ...common, id: i+12, name: `Exit ${i+1}`, type: "exit-node" })),
|
...Array.from({ length: 10 }, (_, i) => ({ ...common, id: i+12, name: `Exit ${i+1}`, type: "exit-node" })),
|
||||||
])
|
])
|
||||||
|
let reads = 0
|
||||||
|
const writes: string[] = []
|
||||||
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
|
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
|
||||||
await page.route("**/api/**", r => {
|
await page.route("**/api/**", r => {
|
||||||
const path = new URL(r.request().url()).pathname
|
const path = new URL(r.request().url()).pathname
|
||||||
|
if (path === "/api/servers") reads++
|
||||||
|
if (r.request().method() !== "GET" && !path.endsWith("/preview")) writes.push(path)
|
||||||
return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? servers : path === "/api/filters/rules" ? { rulesets: [] } : path === "/api/uptime/speed-probes" ? { probes: [] } : path === "/api/ospf/catalog" ? { interfaces: [], instances: [], neighbors: [], bfdSessions: [], routes: [], failures: [] } : path.endsWith("/ospf/optimize/preview") ? { serverId: 1, serverName: "Домашний роутер", pingWeight: 60, interfacesTotal: 0, changedCount: 0, unchangedCount: 0, interfaces: [], changes: [] } : {} })
|
return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? servers : path === "/api/filters/rules" ? { rulesets: [] } : path === "/api/uptime/speed-probes" ? { probes: [] } : path === "/api/ospf/catalog" ? { interfaces: [], instances: [], neighbors: [], bfdSessions: [], routes: [], failures: [] } : path.endsWith("/ospf/optimize/preview") ? { serverId: 1, serverName: "Домашний роутер", pingWeight: 60, interfacesTotal: 0, changedCount: 0, unchangedCount: 0, interfaces: [], changes: [] } : {} })
|
||||||
})
|
})
|
||||||
const start = Date.now(); await page.goto("/route-optimizer?server=1&keep=1")
|
const start = Date.now(); await page.goto("/route-optimizer?server=1&keep=1")
|
||||||
await page.getByRole("button", { name: "Маршруты (1000)", exact: true }).click()
|
await page.getByRole("button", { name: "Маршруты (1000)", exact: true }).click()
|
||||||
const table = page.getByRole("table").filter({ hasText: "# Маршрут" })
|
const table = page.getByRole("table").filter({ hasText: "# Маршрут" })
|
||||||
await expect(table.locator("tr[data-row-id]")).toHaveCount(5)
|
await expect(table.locator("tr[data-row-id]")).toHaveCount(5)
|
||||||
await page.getByRole("button", { name: "Показать все 1000 комбинаций", exact: true }).click()
|
await page.getByRole("button", { name: "Открыть все 1000 комбинаций", exact: true }).click()
|
||||||
await expect(table.locator("tr[data-row-id]")).toHaveCount(1000)
|
await expect(table.locator("tr[data-row-id]")).toHaveCount(50)
|
||||||
const profile = { combinations: 1000, servers: servers.length, bytes: Buffer.byteLength(JSON.stringify(servers)), rendered: await table.locator("tr[data-row-id]").count(), tableNodes: await table.locator("*").count(), pageNodes: await page.locator("*").count(), readyMs: Date.now()-start }
|
const profile = { combinations: 1000, servers: servers.length, bytes: Buffer.byteLength(JSON.stringify(servers)), rendered: await table.locator("tr[data-row-id]").count(), tableNodes: await table.locator("*").count(), pageNodes: await page.locator("*").count(), readyMs: Date.now()-start }
|
||||||
console.log("Optimizer full routes", JSON.stringify(profile)); writeFileSync(info.outputPath("optimizer-routes-profile.json"), JSON.stringify(profile, null, 2))
|
console.log("Optimizer full routes", JSON.stringify(profile)); writeFileSync(info.outputPath("optimizer-routes-profile.json"), JSON.stringify(profile, null, 2))
|
||||||
|
const root = page.getByRole("region", { name: "Полные маршруты оптимизатора" })
|
||||||
|
const rows = table.locator("tr[data-row-id]")
|
||||||
|
const next = root.getByRole("button", { name: "Следующая страница", exact: true })
|
||||||
|
await next.click(); const previous = await rows.first().getAttribute("data-row-id")
|
||||||
|
await expect(rows.first().getByRole("cell").first()).toContainText("51")
|
||||||
|
const before = reads; await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||||
|
await expect.poll(() => reads).toBeGreaterThan(before); await expect(rows.first()).toHaveAttribute("data-row-id", previous!)
|
||||||
|
for (let i=1; i<19; i++) await next.click()
|
||||||
|
await expect(next).toBeDisabled(); await expect(rows.first().getByRole("cell").first()).toContainText("951")
|
||||||
|
const minimum = await rows.last().getByRole("cell").nth(6).innerText()
|
||||||
|
await expect(rows.last().getByRole("cell").first()).toContainText("1000")
|
||||||
|
await root.getByRole("button", { name: "Свернуть", exact: true }).click(); await expect(rows).toHaveCount(5)
|
||||||
|
await root.getByRole("button", { name: "Открыть все 1000 комбинаций", exact: true }).click(); await expect(rows).toHaveCount(50)
|
||||||
|
await table.getByRole("button", { name: "Индекс", exact: true }).click()
|
||||||
|
await table.getByRole("button", { name: "Индекс", exact: true }).click()
|
||||||
|
await expect(rows.first().getByRole("cell").nth(6)).toHaveText(minimum)
|
||||||
|
expect(Number(await rows.last().getByRole("cell").first().innerText())).toBeGreaterThan(50)
|
||||||
|
await page.setViewportSize({ width: 320, height: 1000 })
|
||||||
|
const scroll = root.locator('[data-slot="scroll-area-viewport"]')
|
||||||
|
await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth })
|
||||||
|
expect(await rows.first().getByRole("cell").first().evaluate(el => { const a=el.getBoundingClientRect(), v=el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(); return a.left>=v.left-1 && a.right<=v.right+1 })).toBe(true)
|
||||||
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(320)
|
||||||
|
await scroll.evaluate(el => { el.scrollLeft = 0 })
|
||||||
|
await root.evaluate(el => el.scrollIntoView({ block: "start" }))
|
||||||
|
await page.addStyleTag({ content: "nextjs-portal{display:none!important}" })
|
||||||
|
await page.screenshot({ path: info.outputPath("optimizer-routes-pages-320.png"), animations: "disabled" })
|
||||||
|
await page.addScriptTag({ content: axe.source })
|
||||||
|
expect((await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[aria-label="Полные маршруты оптимизатора"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))).violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) }))).toEqual([])
|
||||||
|
await next.click(); servers = servers.filter(server => server.id === 1 || server.id === 2 || server.id === 12).map(server => server.id === 1 ? { ...server, wanUplinks: server.wanUplinks!.slice(0, 1) } : server)
|
||||||
|
await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||||
|
await expect(rows).toHaveCount(1); await expect(rows.first().getByRole("cell").first()).toContainText("1")
|
||||||
|
expect(writes).toEqual([])
|
||||||
})
|
})
|
||||||
Reference in new issue
Block a user