Files
MikrotikManager/tests/e2e/collector-probes-tables.spec.ts
T

185 lines
15 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 { SCHEDULER_JOB_KEYS } from "../../lib/scheduler-settings"
import { test, expect, type Page } from "@playwright/test"
import axe from "axe-core"
import type { SpeedProbeApiRow } from "../../components/data-grids/probes-speed-probes-data-grid"
async function setup(page: Page, path: string, count = 12, grouped = false) {
const probes: SpeedProbeApiRow[] = Array.from({ length: count }, (_, i) => ({
id: String(i + 1), srcServerId: String(i + 1), dstServerId: String(count + 1), srcInterface: "ether1",
dstInterface: "ether1", protocol: "tcp", direction: "both", durationSec: "10", enabled: true,
lastRunAt: null, lastTxAvgMbps: null, lastRxAvgMbps: null, lastStatus: null, lastError: null,
}))
const jobs = SCHEDULER_JOB_KEYS.map(jobKey => ({ jobKey, enabled: true, intervalSec: 300, running: false, lastFinishedAt: null, lastStatus: null, lastDurationMs: 0, lastError: null }))
const calls: string[] = []
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
await page.route("**/api/**", route => {
const path = new URL(route.request().url()).pathname
if (route.request().method() === "POST") calls.push(path)
if (path === "/api/scheduler/status") return route.fulfill({ json: { jobs } })
if (path === "/api/scheduler/jobs/alert_engine/run-now") return route.fulfill({ json: { ok: true, result: { status: "ok", error: null, result: { v: 1, job: "alert_engine", sampledAt: "2026-10-03T00:00:00Z", rulesChecked: 0, standaloneFires: 0, groupFires: 0, skippedNoTelegram: false } }, status: { jobs } } })
return route.fulfill({ json: path === "/api/uptime/speed-probes" ? { probes }
: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? Array.from({length:count+1},(_,i)=>({id:i+1,name:i<count?`Источник ${i+1}`:"Цель",host:`192.0.2.${i%250+1}`,type:"exit-node",enabled:true,country:"RU",site:grouped?(i%2?"Площадка B":"Площадка A"):"TEST",asn:"",status:null,latency:null,os:"7",model:"CHR",sessions:0,wanUplinks:[]}))
: path === "/api/evobgp/settings" ? { enabled: false, baseUrl: "", secretConfigured: false }
: path === "/api/traffic/flow/settings" ? {
enabled: false, collectorIp: "10.0.0.1", flowListenPort: 2055, wgListenPort: 51820,
prefix: "10.0.0.0/24", publicEndpoint: "", hostPublicKey: "", hasHostPrivateKey: false,
hubServerId: null, retentionHours: 24, topN: 10, mapServiceMinSharePct: 1,
lastDatagramAt: null, lastExporterIp: null, lastError: null, packetsReceived: 0,
listenerBound: false, listenerAddress: null, peers: [],
}
: path === "/api/geoip" ? { ready: false, running: false, countryFile: "", asnFile: "", countryLoaded: false, asnLoaded: false, dir: "storage/geoip", settings: { enabled: false, updateIntervalSec: 86400, lastCheckAt: null, lastSuccessAt: null, lastError: null, countryBuildAt: null, asnBuildAt: null, updatedAt: "2026-09-14" } }
: path === "/api/scheduler/runs" ? { runs: [] }
: path.endsWith("/settings") || path.endsWith("/renew-settings") ? {
enabled: true, intervalSec: 300, retentionDays: 7, probeIntervalSec: 15, speedIntervalSec: 60,
resourcesEnabled: true, pingEnabled: true, speedEnabled: true, lastCollectedAt: null, lastDurationMs: 0, lastError: null, renewBeforeDays: 30, scheduler: { jobs: [] },
} : {} })
})
await page.goto(path)
return calls
}
test("Speed probes: 12 записей и сортировка источника", async ({ page }) => {
await setup(page, "/probes")
await page.getByRole("button", { name: "Расписание", exact: true }).click()
const rows = page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(12)
await page.getByRole("button", { name: "Источник", exact: true }).click()
await expect(rows.first()).toContainText("Источник 1")
await page.getByRole("button", { name: "Источник", exact: true }).click()
await expect(rows.first()).toContainText("Источник 12")
})
test("Сбор данных: все 11 задач и запуск выбранной задачи через API", async ({ page }) => {
const calls = await setup(page, "/data-collection")
const rows = page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(11)
await expect(rows.last()).toContainText("Оповещения")
await page.getByRole("button", { name: "Запустить: Оповещения", exact: true }).click()
await expect.poll(() => calls).toContain("/api/scheduler/jobs/alert_engine/run-now")
await expect(rows).toHaveCount(11)
await expect(page.getByText("Запуск подтверждён: Оповещения", { exact: true })).toBeVisible()
})
test("Демо-расписание: сортировка интервала и удаление выбранного правила", async ({ page }) => {
await page.addInitScript(() => localStorage.setItem("routerlists:data-source", "mock"))
await setup(page, "/probes")
await page.getByRole("button", { name: "Расписание", exact: true }).click()
const rows = page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(3)
await page.getByRole("button", { name: "Интервал", exact: true }).click()
await expect(rows.first()).toHaveAttribute("data-row-id", "r3")
await rows.first().getByRole("button", { name: "Удалить правило", exact: true }).click()
await expect(rows).toHaveCount(2)
await expect(page.locator('tr[data-row-id="r3"]')).toHaveCount(0)
})
test("Large speed probes: 50 строк, глобальная сортировка и контекст обновления", async ({ page }, info) => {
test.setTimeout(90000)
const start = Date.now()
const writes = await setup(page, "/probes", 1000)
await page.getByRole("button", { name: "Расписание", exact: true }).click()
const rows = page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(50)
const sample = { fixtureRows: 1000, renderedRows: await rows.count(), domNodes: await page.locator("*").count(), scheduleNodes: await page.getByRole("region", { name: "Пробы скорости", exact: true }).locator("*").count(), readyMs: Date.now() - start }
console.log(JSON.stringify(sample))
await info.attach("large-speed-probes-metrics", { body: JSON.stringify(sample), contentType: "application/json" })
expect(sample.scheduleNodes).toBeLessThan(2000)
expect(sample.domNodes).toBeLessThan(2500)
await page.getByRole("button", { name: "Следующая страница", exact: true }).click()
await expect(rows.first()).toHaveAttribute("data-row-id", "51")
await page.getByRole("button", { name: "Обновить пробы скорости", exact: true }).click()
await expect(page.getByRole("button", { name: "Обновить пробы скорости", exact: true })).toBeEnabled()
await expect(page.getByText("51–100 из 1000", { exact: true })).toBeVisible()
await page.getByRole("button", { name: "Источник", exact: true }).click()
await page.getByRole("button", { name: "Источник", exact: true }).click()
await page.getByRole("button", { name: "Перейти на страницу 1", exact: true }).click()
await expect(rows.first()).toHaveAttribute("data-row-id", "1000")
for (let i = 0; i < 3; i++) await page.getByRole("button", { name: "…", exact: true }).last().click()
await page.getByRole("button", { name: "Перейти на страницу 20", exact: true }).click()
await expect(page.getByText("951–1000 из 1000", { exact: true })).toBeVisible()
const panel = page.getByRole("region", { name: "Пробы скорости", exact: true })
const freshness = await panel.getByRole("status").textContent()
await page.route("**/api/uptime/speed-probes", route => route.fulfill({ status: 503, json: { error: "Большой каталог недоступен" } }))
await panel.getByRole("button", { name: "Обновить пробы скорости", exact: true }).click()
await expect(panel.getByRole("alert")).toContainText("Показан последний успешный список")
await expect(panel.getByRole("status")).toHaveText(freshness!)
await expect(rows).toHaveCount(50)
await expect(page.getByText("951–1000 из 1000", { exact: true })).toBeVisible()
await page.setViewportSize({ width: 320, height: 900 })
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
await page.route("**/api/uptime/speed-probes", route => route.fulfill({ json: { probes: [{ id: "1", srcServerId: "1", dstServerId: "1001", srcInterface: "ether1", dstInterface: "ether1", protocol: "tcp", direction: "both", durationSec: "10", enabled: true, lastRunAt: null, lastTxAvgMbps: null, lastRxAvgMbps: null, lastStatus: null, lastError: null }] } }))
await page.getByRole("button", { name: "Обновить пробы скорости", exact: true }).click()
await expect(rows).toHaveCount(1)
await expect(rows.first()).toHaveAttribute("data-row-id", "1")
await expect(panel.getByRole("alert")).toHaveCount(0)
expect(writes).toEqual([])
})
for (const theme of ["dark", "light"]) test(`Large server rail: URL, страницы, поиск и клавиатура на1001 устройстве/${theme}`, async ({ page }, info) => {
await page.addInitScript(value => localStorage.setItem("rl-theme", value), theme)
await setup(page, "/probes?server=1000&keep=1", 1000)
const rail = page.locator("aside").filter({ has: page.getByRole("listbox", { name: "Серверы", exact: true }) })
const list = rail.getByRole("listbox", { name: "Серверы", exact: true })
await expect(list.getByRole("option")).toHaveCount(50)
await expect(list.getByRole("option", { selected: true })).toContainText("Источник 1000")
await expect(rail.getByText("951–1000 из 1001", { exact: true })).toBeVisible()
expect(await rail.locator("*").count()).toBeLessThan(1200)
await info.attach("large-rail-metrics", { body: JSON.stringify({ fixtureDevices: 1001, renderedOptions: await list.getByRole("option").count(), railNodes: await rail.locator("*").count(), domNodes: await page.locator("*").count() }), contentType: "application/json" })
await list.getByRole("option", { selected: true }).focus()
await page.keyboard.press("End")
await expect(page).toHaveURL(url => url.searchParams.get("server") === "1001" && url.searchParams.get("keep") === "1")
await expect(list.getByRole("option")).toHaveCount(1)
await expect(list.getByRole("option", { selected: true })).toContainText("Цель")
await page.keyboard.press("Home")
await expect(page).toHaveURL(url => url.searchParams.get("server") === "1" && url.searchParams.get("keep") === "1")
await expect(list.getByRole("option")).toHaveCount(50)
await page.keyboard.press("ArrowDown")
await expect(page).toHaveURL(url => url.searchParams.get("server") === "2" && url.searchParams.get("keep") === "1")
await expect(list.getByRole("option", { selected: true })).toContainText("Источник 2")
await rail.getByRole("button", { name: "Следующие устройства", exact: true }).click()
await expect(list.getByRole("option").first()).toContainText("Источник 51")
await page.getByRole("button", { name: "Обновить устройства диагностики", exact: true }).click()
await expect(page.getByRole("button", { name: "Обновить устройства диагностики", exact: true })).toBeEnabled()
await expect(rail.getByText("51–100 из 1001", { exact: true })).toBeVisible()
const search = rail.getByRole("textbox", { name: "Поиск сервера", exact: true })
await search.fill("Источник 1000")
await expect(list.getByRole("option")).toHaveCount(1)
await list.getByRole("option").click()
await expect(page).toHaveURL(url => url.searchParams.get("server") === "1000" && url.searchParams.get("keep") === "1")
await search.fill("")
await expect(list.getByRole("option", { selected: true })).toContainText("Источник 1000")
await page.setViewportSize({ width: 320, height: 900 })
await page.getByRole("button", { name: "Серверы", exact: true }).click()
const sheet = page.getByRole("dialog")
await expect(sheet.getByRole("option")).toHaveCount(50)
await expect(sheet.getByRole("option", { selected: true })).toContainText("Источник 1000")
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
expect(await sheet.getByRole("button", { name: "Следующие устройства", exact: true }).evaluate(el => el.getBoundingClientRect().right <= innerWidth)).toBe(true)
await sheet.getByRole("option", { selected: true }).focus(); await expect(sheet.getByRole("option", { selected: true })).toBeFocused()
await page.addScriptTag({ content: axe.source })
const audit = await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[role="dialog"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"] } }))
await info.attach("large-rail-axe", { body: JSON.stringify(audit), contentType: "application/json" })
expect(audit.violations.map(violation => ({ id: violation.id, nodes: violation.nodes.map(node => node.target) }))).toEqual([])
await page.screenshot({ path: `test-results/large-server-rail-mobile-${theme}.png`, animations: "disabled" })
await page.keyboard.press("Escape")
await expect(page.getByRole("button", { name: "Серверы", exact: true })).toBeFocused()
})
test("Large server rail: выбранное устройство и граница площадок", async ({ page }) => {
await setup(page, "/probes?server=1000", 1000, true)
const rail = page.locator("aside").filter({ has: page.getByRole("listbox", { name: "Серверы", exact: true }) })
const list = rail.getByRole("listbox", { name: "Серверы", exact: true })
await expect(list.getByRole("option")).toHaveCount(1)
await expect(list.getByRole("option", { selected: true })).toContainText("Источник 1000")
await expect(rail.getByText("Площадка B", { exact: true })).toBeVisible()
await list.getByRole("option", { selected: true }).focus()
await page.keyboard.press("End")
await expect(list.getByRole("option", { selected: true })).toContainText("Цель")
await expect(list.getByRole("option")).toHaveCount(50)
await expect(rail.getByText("Площадка A", { exact: true })).toBeVisible()
await expect(rail.getByText("Площадка B", { exact: true })).toBeVisible()
await page.keyboard.press("Home")
await expect(list.getByRole("option", { selected: true })).toContainText("Источник 1")
await expect(list.getByRole("option")).toHaveCount(50)
})