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") })