Files
MikrotikManager/tests/e2e/large-ospf-neighbors.spec.ts
T

203 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from "@playwright/test"
import { writeFileSync } from "node:fs"
import axe from "axe-core"
import { ospfCatalogSchema } from "../../lib/ospf-response"
test("OSPF1000 соседей: профиль всего экрана с topology", 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 failed = false, reads = 0
const writes: string[] = []
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
await page.route("**/api/**", r => {
const p = new URL(r.request().url()).pathname
if (r.request().method() !== "GET") writes.push(p)
if (p === "/api/ospf/catalog") { reads++; return r.fulfill(failed ? { status: 503, json: { error: "Каталог OSPF недоступен1000" } } : { json: { ...catalog, neighbors } }) }
return r.fulfill({ json: p === "/api/auth/config" ? { required: false } : p === "/api/ospf/catalog" ? catalog : p === "/api/servers" ? [{ id: 1, name: "Роутер 1", host: "192.0.2.1", enabled: true, country: "RU", status: null }] : {} })
})
await page.setViewportSize({ width: 1440, height: 1000 })
const start = Date.now(); await page.goto("/ospf?tab=neighbors&server=1&node=remote-10.3.231.1&keep=1")
const rows = page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(50)
await expect(rows.first()).toHaveAttribute("data-row-id", "1-n951")
await expect(page.locator("[data-ospf-node]")).toHaveCount(2)
const profile = { count: neighbors.length, bytes: Buffer.byteLength(JSON.stringify(catalog)), rows: await rows.count(), graphNodes: await page.locator("[data-ospf-node]").count(), svgNodes: await page.getByRole("group", { name: "Топология OSPF", exact: true }).locator("*").count(), pageNodes: await page.locator("*").count(), readyMs: Date.now() - start }
console.log("OSPF neighbors topology", JSON.stringify(profile))
writeFileSync(info.outputPath("ospf-neighbors-profile.json"), JSON.stringify(profile, null, 2))
await info.attach("profile", { body: JSON.stringify(profile), contentType: "application/json" })
await page.addStyleTag({ content: "nextjs-portal{display:none!important}" })
await page.screenshot({ path: info.outputPath("ospf-neighbors-topology-1440.png"), animations: "disabled" })
const scope = page.getByRole("region", { name: "Область топологии OSPF" })
await expect(scope.getByRole("status")).toContainText("Показаны 2 из 1001 узлов")
await expect(scope.getByRole("status")).toContainText("Всего в топологии 1000 связей")
await scope.getByRole("textbox", { name: "Найти узел топологии" }).fill("10.0.0.1")
await scope.getByRole("button", { name: "Открыть узел топологии 10.0.0.1 (remote-10.0.0.1)", exact: true }).click()
await expect(page).toHaveURL(url => url.searchParams.get("node") === "remote-10.0.0.1" && url.searchParams.get("keep") === "1")
await expect(page.locator("[data-ospf-node]")).toHaveCount(2)
await scope.getByRole("textbox", { name: "Найти узел топологии" }).fill("10.3.231.1")
await scope.getByRole("button", { name: "Открыть узел топологии 10.3.231.1 (remote-10.3.231.1)", exact: true }).click()
await expect(rows.first()).toHaveAttribute("data-row-id", "1-n951")
const next = page.getByRole("button", { name: "Следующая страница", exact: true })
await expect(next).toBeDisabled()
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", "1-n951")
await page.getByRole("button", { name: "Предыдущая страница", exact: true }).click()
await expect(rows.first()).toHaveAttribute("data-row-id", "1-n901")
const search = page.getByPlaceholder("Поиск в текущем разделе OSPF…")
await search.fill("10.3.231.1"); await expect(rows).toHaveCount(1); await expect(rows.first()).toHaveAttribute("data-row-id", "1-n1000")
await expect(page).toHaveURL(url => url.searchParams.get("q") === "10.3.231.1" && url.searchParams.get("keep") === "1")
await page.reload(); await expect(rows).toHaveCount(1)
await search.fill(""); await expect(rows).toHaveCount(50)
await page.getByRole("table").getByRole("button", { name: "Cost", exact: true }).click()
await expect(rows.first()).toHaveAttribute("data-row-id", "1-n1000")
await next.click(); const previous = await rows.first().getAttribute("data-row-id")
failed = true; await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(page.getByRole("alert").filter({ hasText: "Не удалось загрузить OSPF" })).toContainText("Показаны последние полученные данные")
await expect(rows.first()).toHaveAttribute("data-row-id", previous!)
await expect(rows.first()).toContainText("Неизвестно")
await page.setViewportSize({ width: 320, height: 1000 })
const scroll = page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]')
await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth })
const pinned = rows.first().getByRole("cell").first()
expect(await pinned.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)
await rows.first().scrollIntoViewIfNeeded()
await page.addScriptTag({ content: axe.source })
expect((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"] } }))).violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) }))).toEqual([])
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-neighbors-topology-320.png"), animations: "disabled" })
failed = false; neighbors = [neighbors[999]]; await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(rows).toHaveCount(1); await expect(rows.first()).toHaveAttribute("data-row-id", "1-n1000")
expect(writes).toEqual([])
})
test("OSPF topology: полный статус, страницы связей и поиск скрытого узла", 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: [] })
neighbors = neighbors.map((n,i) => ({ ...n, state: i === 999 ? "Full" : "Down" }))
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 } : 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&keep=1")
const scope = page.getByRole("region", { name: "Область топологии OSPF" })
await expect(scope.getByRole("status")).toContainText("Показаны 9 из 1001 узлов. Связи узла: 1–8 из 1000")
await expect(page.locator("[data-ospf-node]")).toHaveCount(9)
const local = page.locator('[data-ospf-node="1"]')
await expect(local).toHaveAttribute("data-ospf-state", "Full")
await scope.getByRole("button", { name: "Следующая страница связей OSPF" }).click()
await expect(scope.getByRole("status")).toContainText("9–16 из 1000")
await expect(local).toHaveAttribute("data-ospf-state", "Full")
await expect(page.locator('[data-ospf-node="remote-10.0.8.1"]')).toHaveCount(1)
await scope.getByRole("button", { name: "Предыдущая страница связей OSPF" }).click()
await page.addStyleTag({ content: "nextjs-portal{display:none!important}" })
await page.screenshot({ path: info.outputPath("ospf-topology-scope-1440.png"), animations: "disabled" })
await page.setViewportSize({ width: 320, height: 1000 })
await page.screenshot({ path: info.outputPath("ospf-topology-scope-320.png"), animations: "disabled" })
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(320)
for (const button of await scope.locator("button").all()) expect((await button.boundingBox())!.height).toBeGreaterThanOrEqual(44)
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([])
const picker = scope.getByRole("textbox", { name: "Найти узел топологии" })
await picker.fill("10.3.231.1")
await scope.getByRole("button", { name: "Открыть узел топологии 10.3.231.1 (remote-10.3.231.1)", exact: true }).click()
await expect(page).toHaveURL(url => url.searchParams.get("node") === "remote-10.3.231.1" && url.searchParams.get("keep") === "1")
await expect(page.locator("[data-ospf-node]")).toHaveCount(2)
await page.getByRole("region", { name: "Детали узла OSPF" }).getByRole("button", { name: "Закрыть", exact: true }).click()
await expect(picker).toBeFocused()
await expect(scope.getByRole("status")).toContainText("1–8 из 1000")
await scope.getByRole("button", { name: "Следующая страница связей OSPF" }).click()
await scope.getByRole("button", { name: "Следующая страница связей OSPF" }).click()
neighbors = neighbors.slice(0, 13)
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(scope.getByRole("status")).toContainText("9–13 из 13")
await expect(scope.getByRole("button", { name: "Следующая страница связей OSPF" })).toBeDisabled()
neighbors = neighbors.slice(0, 12)
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(scope.getByRole("status")).toContainText("9–12 из 12")
})
test("OSPF: тысяча параллельных связей не обходит ограничение графа", async ({ page }, info) => {
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: [] })
neighbors = neighbors.map((n, index) => ({ ...n, routerId: "10.0.0.1", address: "10.0.0.1", state: index % 2 ? "Down" : "Full" }))
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 } : 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")
await expect(page.getByRole("region", { name: "Область топологии OSPF" }).getByRole("status")).toContainText("Показаны 2 из 2 узлов. Связи узла: 1–8 из 1000")
await expect(page.locator("[data-ospf-node]")).toHaveCount(2)
expect(await page.getByRole("group", { name: "Топология OSPF", exact: true }).locator("*").count()).toBeLessThan(200)
const visual = page.locator('[data-ospf-links="8"]')
await expect(visual).toHaveCount(1)
await expect(visual).toHaveAttribute("aria-label", /Full, cost 1; Down, cost 2/)
await expect(visual.locator("text")).toHaveText("8 связей")
await page.addStyleTag({ content: "nextjs-portal{display:none!important}" })
await page.screenshot({ path: info.outputPath("ospf-parallel-links-1440.png"), animations: "disabled" })
})
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")
})