fix(ui): ограничить списки деталей OSPF без потери данных
This commit is contained in:
1 parent
8d86c5cbb0
commit
086f118204
9 files changed
+110
-11
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { OspfDetailList } from "@/components/ospf-detail-list"
|
||||||
import { OspfTopologyScope } from "@/components/ospf-topology-scope"
|
import { OspfTopologyScope } from "@/components/ospf-topology-scope"
|
||||||
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion"
|
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion"
|
||||||
import { deriveOspfRouterIdentity } from "@/lib/ospf-router-identity"
|
import { deriveOspfRouterIdentity } from "@/lib/ospf-router-identity"
|
||||||
@@ -559,7 +560,7 @@ function NodeDetailPanel({
|
|||||||
<p className="text-sm font-mono font-semibold text-foreground truncate leading-tight">{node.label}</p>
|
<p className="text-sm font-mono font-semibold text-foreground truncate leading-tight">{node.label}</p>
|
||||||
<p className="text-[10px] font-mono text-muted-foreground mt-0.5">Router-ID: {routerId}</p>
|
<p className="text-[10px] font-mono text-muted-foreground mt-0.5">Router-ID: {routerId}</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" aria-label="Закрыть" onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors mt-0.5 shrink-0">
|
<button type="button" aria-label="Закрыть" onClick={onClose} className="flex min-h-11 min-w-11 items-center justify-center text-muted-foreground hover:text-foreground transition-colors shrink-0">
|
||||||
<XIcon className="size-3.5" />
|
<XIcon className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -578,8 +579,7 @@ function NodeDetailPanel({
|
|||||||
<div className="px-4 py-3 border-b border-border">
|
<div className="px-4 py-3 border-b border-border">
|
||||||
<p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Соседи ({adjacencies.length})</p>
|
<p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Соседи ({adjacencies.length})</p>
|
||||||
{adjacencies.length === 0 && <p className="text-[11px] text-muted-foreground">Нет активных adjacency</p>}
|
{adjacencies.length === 0 && <p className="text-[11px] text-muted-foreground">Нет активных adjacency</p>}
|
||||||
<div className="flex flex-col gap-0 divide-y divide-border">
|
<OspfDetailList key={`neighbors-${nodeId}`} label="Соседи узла OSPF" items={adjacencies} getSearchValue={adj => `${adj.peerLabel} ${adj.peerId} ${adj.iface} ${adj.state}`} renderItem={(adj, i) => {
|
||||||
{adjacencies.map((adj, i) => {
|
|
||||||
const ec = EDGE_COLOR[adj.state] ?? "var(--muted-foreground)"
|
const ec = EDGE_COLOR[adj.state] ?? "var(--muted-foreground)"
|
||||||
return (
|
return (
|
||||||
<div key={i} className="flex items-start gap-2.5 py-2.5">
|
<div key={i} className="flex items-start gap-2.5 py-2.5">
|
||||||
@@ -592,8 +592,7 @@ function NodeDetailPanel({
|
|||||||
<span className="text-[10px] font-mono font-semibold shrink-0 mt-0.5" style={{ color: ec }}>{adj.state}</span>
|
<span className="text-[10px] font-mono font-semibold shrink-0 mt-0.5" style={{ color: ec }}>{adj.state}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
}} />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 py-3">
|
<div className="px-4 py-3">
|
||||||
@@ -601,8 +600,7 @@ function NodeDetailPanel({
|
|||||||
Интерфейсы OSPF{ifaces.length > 0 && <span className="ml-1 text-muted-foreground">({ifaces.length})</span>}
|
Интерфейсы OSPF{ifaces.length > 0 && <span className="ml-1 text-muted-foreground">({ifaces.length})</span>}
|
||||||
</p>
|
</p>
|
||||||
{ifaces.length === 0 && <p className="text-[11px] text-muted-foreground">Нет данных</p>}
|
{ifaces.length === 0 && <p className="text-[11px] text-muted-foreground">Нет данных</p>}
|
||||||
<div className="flex flex-col gap-0 divide-y divide-border">
|
<OspfDetailList key={`interfaces-${nodeId}`} label="Интерфейсы узла OSPF" items={ifaces} getSearchValue={iface => `${iface.interfaceName} ${iface.area} ${iface.cost ?? ""}`} renderItem={iface => (
|
||||||
{ifaces.map(iface => (
|
|
||||||
<div key={iface.key} className="flex items-center gap-2 py-2">
|
<div key={iface.key} className="flex items-center gap-2 py-2">
|
||||||
<span className={cn("size-1.5 rounded-full shrink-0", iface.active ? "bg-emerald-500" : "bg-muted-foreground/40")} />
|
<span className={cn("size-1.5 rounded-full shrink-0", iface.active ? "bg-emerald-500" : "bg-muted-foreground/40")} />
|
||||||
<code className="text-[11px] font-mono flex-1 min-w-0 truncate" style={{ color: "var(--muted-foreground)" }}>{iface.interfaceName}</code>
|
<code className="text-[11px] font-mono flex-1 min-w-0 truncate" style={{ color: "var(--muted-foreground)" }}>{iface.interfaceName}</code>
|
||||||
@@ -611,8 +609,7 @@ function NodeDetailPanel({
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] font-mono shrink-0" style={{ color: "var(--info)" }}>cost {iface.cost ?? "—"}</span>
|
<span className="text-[10px] font-mono shrink-0" style={{ color: "var(--info)" }}>cost {iface.cost ?? "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)} />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useMemo, useState, type ReactNode } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
|
||||||
|
export function OspfDetailList<T>({ items, label, getSearchValue, renderItem }: {
|
||||||
|
items: T[]; label: string; getSearchValue: (item: T) => string
|
||||||
|
renderItem: (item: T, index: number) => ReactNode
|
||||||
|
}) {
|
||||||
|
const [query, setQuery] = useState("")
|
||||||
|
const [page, setPage] = useState(0)
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const value = query.trim().toLowerCase()
|
||||||
|
return value ? items.filter(item => getSearchValue(item).toLowerCase().includes(value)) : items
|
||||||
|
}, [items, query, getSearchValue])
|
||||||
|
const pages = Math.max(1, Math.ceil(filtered.length / 50))
|
||||||
|
if (page >= pages) setPage(pages - 1)
|
||||||
|
const current = Math.min(page, pages - 1)
|
||||||
|
const controlled = items.length > 50 || !!query
|
||||||
|
return <section aria-label={label} className="min-w-0">
|
||||||
|
{controlled && <div className="flex min-w-0 flex-col gap-2 mb-2">
|
||||||
|
<Input aria-label={`Поиск: ${label}`} placeholder="Поиск во всём списке…" value={query} onChange={event => { setQuery(event.target.value); setPage(0) }} />
|
||||||
|
<p role="status" className="text-xs text-muted-foreground">Показаны {filtered.length ? current * 50 + 1 : 0}–{Math.min((current + 1) * 50, filtered.length)} из {filtered.length}. Всего {items.length}.</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button type="button" variant="outline" className="min-h-11" aria-label={`Предыдущая страница: ${label}`} disabled={!current} onClick={() => setPage(current - 1)}>Назад</Button>
|
||||||
|
<Button type="button" variant="outline" className="min-h-11" aria-label={`Следующая страница: ${label}`} disabled={current >= pages - 1} onClick={() => setPage(current + 1)}>Далее</Button>
|
||||||
|
</div>
|
||||||
|
{!filtered.length && <p className="text-xs text-muted-foreground">По вашему запросу ничего не найдено.</p>}
|
||||||
|
</div>}
|
||||||
|
<div className="flex flex-col gap-0 divide-y divide-border">{filtered.slice(current * 50, (current + 1) * 50).map((item, index) => renderItem(item, current * 50 + index))}</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
@@ -96,3 +96,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич
|
|||||||
12.3.49: OSPF1000 neighbors→50 rows,1001 SVG nodes/12015 descendants/13597 page nodes/2827ms dev ready. URL page20/search/global Cost sort/refresh/cache/clamp/320px/axe приняты;22 browser/type/lint/build. Весь граф не принят, KPI1000 переносится на320px, большой node detail вне scope; далее12.3.50–51.9 manualPagination:true остаются.
|
12.3.49: OSPF1000 neighbors→50 rows,1001 SVG nodes/12015 descendants/13597 page nodes/2827ms dev ready. URL page20/search/global Cost sort/refresh/cache/clamp/320px/axe приняты;22 browser/type/lint/build. Весь граф не принят, KPI1000 переносится на320px, большой node detail вне scope; далее12.3.50–51.9 manualPagination:true остаются.
|
||||||
|
|
||||||
12.3.50: OSPF1000 neighbors → scope8 связей/9 узлов, выбранный remote1000:2 SVG nodes/27 descendants/1630 page DOM.26+3 main/8 context/type/lint/build. Полный источник и статус/поиск/URL сохранены; большие node details и читаемость parallel edges не приняты. Далее12.3.51.
|
12.3.50: OSPF1000 neighbors → scope8 связей/9 узлов, выбранный remote1000:2 SVG nodes/27 descendants/1630 page DOM.26+3 main/8 context/type/lint/build. Полный источник и статус/поиск/URL сохранены; большие node details и читаемость parallel edges не приняты. Далее12.3.51.
|
||||||
|
|
||||||
|
12.3.51: OSPF node=1/1000 neighbors+1000 interfaces →50+50 rows/635 panel nodes/2335 page DOM.4+15+1 browser/type/lint/build. Search/full counts/refresh/clamp/320px/axe приняты; parallel link labels далее12.3.52.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# План реализации UX/UI по пятичасовым окнам
|
# План реализации UX/UI по пятичасовым окнам
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-04: 12.3.50 принят локально: OSPF topology показывает до8 связей/9 узлов, полный статус и поиск/URL.26+3 main/8 context, types/lint/build; полный источник1000 сохранён. Далее12.3.51 — большие детали узла; затем читаемость параллельных связей и optimizer. Общая регрессия после delta и внешняя12.5 остаются.
|
Актуальное продолжение 2026-10-04: 12.3.51 принят локально: большие детали OSPF по50 neighbors/interfaces, поиск полного списка/refresh/clamp/320px/axe.4+15+1 browser/type/lint/build. Далее12.3.52 — подписи параллельных связей; затем optimizer и общая main/context после delta. Внешняя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.50 принят локально: OSPF topology показывает до8 связей/9 узлов, полный статус и поиск/URL.26+3 main/8 context, types/lint/build; полный источник1000 сохранён. Далее12.3.51 — большие детали узла; затем читаемость параллельных связей и optimizer. Общая регрессия после delta и внешняя12.5 остаются.
|
Актуальное продолжение 2026-10-04: 12.3.51 принят локально: большие детали OSPF по50 neighbors/interfaces, поиск полного списка/refresh/clamp/320px/axe.4+15+1 browser/type/lint/build. Далее12.3.52 — подписи параллельных связей; затем optimizer и общая main/context после delta. Внешняя12.5 отдельно.
|
||||||
|
|
||||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения.
|
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения.
|
||||||
|
|
||||||
|
|||||||
@@ -603,3 +603,12 @@ OspfTopologyScope использует полный источник: до8 inci
|
|||||||
26/26 профильных main,8/8 context и финальные3/3 large прошли. Fixture1000 neighbors/1000 interfaces/1001 nodes/583875 bytes →50 rows/2 SVG nodes/27 descendants/1630 page nodes/1531ms dev ready для URL remote1000. Проверены page2/clamp после уменьшения1000→13→12, полная hidden Full-связь при первых Down, выбор remote1000 из всего каталога/keep, close/focus,320px/axe, bounded parallel1000. TypeScript без incremental, ESLint страницы/компонента/теста и production build прошли. [Профиль](ux-ui/acceptance/ospf-topology-scope-2026-10-04.json), [срез1440px](ux-ui/screens/ospf-topology-scope-1440.png), [320px](ux-ui/screens/ospf-topology-scope-320.png) сохранены и просмотрены. Dev latency не production benchmark. На большой топологии SVG доступен через горизонтальный scroll; все узлы доступны через поиск.
|
26/26 профильных main,8/8 context и финальные3/3 large прошли. Fixture1000 neighbors/1000 interfaces/1001 nodes/583875 bytes →50 rows/2 SVG nodes/27 descendants/1630 page nodes/1531ms dev ready для URL remote1000. Проверены page2/clamp после уменьшения1000→13→12, полная hidden Full-связь при первых Down, выбор remote1000 из всего каталога/keep, close/focus,320px/axe, bounded parallel1000. TypeScript без incremental, ESLint страницы/компонента/теста и production build прошли. [Профиль](ux-ui/acceptance/ospf-topology-scope-2026-10-04.json), [срез1440px](ux-ui/screens/ospf-topology-scope-1440.png), [320px](ux-ui/screens/ospf-topology-scope-320.png) сохранены и просмотрены. Dev latency не production benchmark. На большой топологии SVG доступен через горизонтальный scroll; все узлы доступны через поиск.
|
||||||
|
|
||||||
Это не приёмка всех вариантов графа: параллельные edges ограничены по DOM, но их подписи на общей линии могут перекрываться. Большая NodeDetailPanel ещё рендерит все1000 neighbors/interfaces — следующий12.3.51. Затем параллельные связи/optimizer/общая main-context регрессия после delta.9 manualPagination:true specialized grids остаются. Внешние RouterOS/PG/интеграции12.5 не проверены. Локальный коммит без push.
|
Это не приёмка всех вариантов графа: параллельные edges ограничены по DOM, но их подписи на общей линии могут перекрываться. Большая NodeDetailPanel ещё рендерит все1000 neighbors/interfaces — следующий12.3.51. Затем параллельные связи/optimizer/общая main-context регрессия после delta.9 manualPagination:true specialized grids остаются. Внешние RouterOS/PG/интеграции12.5 не проверены. Локальный коммит без push.
|
||||||
|
|
||||||
|
|
||||||
|
## 12.3.51 — большие детали узла OSPF (2026-10-04)
|
||||||
|
|
||||||
|
OspfDetailList: client pages50 отдельно для соседей/интерфейсов, поиск всего списка по peer/Router ID/interface/state и interface/area/cost; full counts сохраняются. Refresh не сбрасывает ручную страницу, shrink ограничивает её, новый nodeId пересоздаёт контекст списка. Close44px, Escape/focus прежние; сетевые правила не изменены.
|
||||||
|
|
||||||
|
4/4 large,15/15 profile keyboard/layout/URL/state/identity и финальный detail1/1 прошли. Полный fixture1000 neighbors/1000 interfaces →50+50 записей/635 panel DOM/2335 page DOM для URL node=1. Поиск ether1000/полный cost1000, no-match, page2/refresh, shrink1000→1/keep/node,320px/кнопки44/axe проверены. TypeScript без incremental, полный ESLint страницы/компонента/теста и production build прошли. [Профиль](ux-ui/acceptance/ospf-detail-2026-10-04.json), [панель320px](ux-ui/screens/ospf-large-detail-320.png) сохранены и просмотрены. Каталог не обрезается в памяти.
|
||||||
|
|
||||||
|
Следующий12.3.52: визуальная группировка параллельных links без подмены cost/state; затем optimizer и общая регрессия после всех delta.9 specialized manualPagination:true остаются. Внешние RouterOS/PG/интеграции12.5 не проверены. Локальный коммит без push.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"neighbors": 1000,
|
||||||
|
"interfaces": 1000,
|
||||||
|
"renderedNeighbors": 50,
|
||||||
|
"renderedInterfaces": 50,
|
||||||
|
"panelNodes": 635,
|
||||||
|
"pageNodes": 2335
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -144,3 +144,53 @@ test("OSPF: тысяча параллельных связей не обходи
|
|||||||
await expect(page.locator("[data-ospf-node]")).toHaveCount(2)
|
await expect(page.locator("[data-ospf-node]")).toHaveCount(2)
|
||||||
expect(await page.getByRole("group", { name: "Топология OSPF", exact: true }).locator("*").count()).toBeLessThan(200)
|
expect(await page.getByRole("group", { name: "Топология OSPF", exact: true }).locator("*").count()).toBeLessThan(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test("OSPF детали1000: полные списки, поиск, страницы и сохранение узла", async ({ page }, info) => {
|
||||||
|
test.setTimeout(90000)
|
||||||
|
const host = { serverId: 1, serverName: "Роутер 1", serverSite: "", serverCountry: "RU" }
|
||||||
|
let neighbors = Array.from({ length: 1000 }, (_, i) => ({ ...host, id: `n${i + 1}`, address: `10.${Math.floor(i / 256)}.${i % 256}.1`, routerId: `10.${Math.floor(i / 256)}.${i % 256}.1`, instance: "default", area: "backbone", areaId: "0.0.0.0", interface: `ether${i + 1}`, state: "Full", uptime: "1h", stateChanges: 0, priority: 1 }))
|
||||||
|
const interfaces = neighbors.map((n, i) => ({ ...host, id: `i${i + 1}`, instance: "default", area: "backbone", areaId: "0.0.0.0", interface: n.interface, cost: i + 1, type: "broadcast", disabled: false, inactive: false, priority: 1, helloInterval: 10, deadInterval: 40, useBfd: false }))
|
||||||
|
const catalog = ospfCatalogSchema.parse({ neighbors, interfaces, instances: [{ ...host, id: "instance1", name: "default", routerId: "192.0.2.1", version: 2, disabled: false, inactive: false, redistribute: "" }], bfdSessions: [], routes: [], failures: [] })
|
||||||
|
|
||||||
|
let currentInterfaces = interfaces
|
||||||
|
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
|
||||||
|
await page.route("**/api/**", r => {
|
||||||
|
const path = new URL(r.request().url()).pathname
|
||||||
|
return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/ospf/catalog" ? { ...catalog, neighbors, interfaces: currentInterfaces } : path === "/api/servers" ? [{ id: 1, name: "Роутер 1", host: "192.0.2.1", enabled: true, country: "RU", status: null }] : {} })
|
||||||
|
})
|
||||||
|
await page.goto("/ospf?tab=neighbors&server=1&node=1&keep=1")
|
||||||
|
const details = page.getByRole("region", { name: "Детали узла OSPF" })
|
||||||
|
const peers = details.getByRole("region", { name: "Соседи узла OSPF", exact: true })
|
||||||
|
const ports = details.getByRole("region", { name: "Интерфейсы узла OSPF", exact: true })
|
||||||
|
const peerRows = peers.locator(':scope > div').last().locator(':scope > div')
|
||||||
|
const portRows = ports.locator(':scope > div').last().locator(':scope > div')
|
||||||
|
await expect(peerRows).toHaveCount(50); await expect(portRows).toHaveCount(50)
|
||||||
|
await expect(peers.getByRole("status")).toContainText("1–50 из 1000. Всего 1000")
|
||||||
|
const profile = { neighbors: neighbors.length, interfaces: interfaces.length, renderedNeighbors: await peerRows.count(), renderedInterfaces: await portRows.count(), panelNodes: await details.locator("*").count(), pageNodes: await page.locator("*").count() }
|
||||||
|
console.log("OSPF detail", JSON.stringify(profile)); writeFileSync(info.outputPath("ospf-detail-profile.json"), JSON.stringify(profile, null, 2))
|
||||||
|
expect(profile.pageNodes).toBeLessThan(5000)
|
||||||
|
await peers.getByRole("button", { name: "Следующая страница: Соседи узла OSPF", exact: true }).click()
|
||||||
|
await expect(peerRows.first()).toContainText("ether51")
|
||||||
|
await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||||
|
await expect(peers.getByRole("status")).toContainText("51–100 из 1000")
|
||||||
|
const peerSearch = peers.getByRole("textbox", { name: "Поиск: Соседи узла OSPF" })
|
||||||
|
await peerSearch.fill("ether1000"); await expect(peerRows).toHaveCount(1); await expect(peerRows.first()).toContainText("10.3.231.1")
|
||||||
|
const portSearch = ports.getByRole("textbox", { name: "Поиск: Интерфейсы узла OSPF" })
|
||||||
|
await portSearch.fill("ether1000"); await expect(portRows).toHaveCount(1); await expect(portRows.first()).toContainText("cost 1000")
|
||||||
|
await portSearch.fill("несуществующий"); await expect(ports).toContainText("По вашему запросу ничего не найдено")
|
||||||
|
await peerSearch.fill(""); await portSearch.fill("")
|
||||||
|
await page.setViewportSize({ width: 320, height: 1000 }); await details.evaluate(el => el.scrollIntoView({ block: "start" }))
|
||||||
|
for (const button of await details.locator("button").all()) expect((await button.boundingBox())!.height).toBeGreaterThanOrEqual(44)
|
||||||
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(320)
|
||||||
|
await page.addStyleTag({ content: "nextjs-portal{display:none!important}" })
|
||||||
|
await page.screenshot({ path: info.outputPath("ospf-large-detail-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="Детали узла OSPF"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))).violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) }))).toEqual([])
|
||||||
|
await peers.getByRole("button", { name: "Следующая страница: Соседи узла OSPF", exact: true }).click()
|
||||||
|
neighbors = neighbors.slice(0, 1); currentInterfaces = interfaces.slice(0, 1)
|
||||||
|
await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||||
|
await expect(peerRows).toHaveCount(1); await expect(portRows).toHaveCount(1)
|
||||||
|
await expect(peerRows.first()).toContainText("ether1")
|
||||||
|
await expect(page).toHaveURL(url => url.searchParams.get("node") === "1" && url.searchParams.get("keep") === "1")
|
||||||
|
})
|
||||||
Reference in new issue
Block a user