Files
MikrotikManager/tests/e2e/dashboard-data.spec.ts
T

136 lines
8.3 KiB
TypeScript

import { test, expect, type Page } from "@playwright/test"
async function setup(page: Page, initiallyFailed = false) {
const control = { failed: initiallyFailed, partial: false, prefixes: 321, hold: false, holdPath: "/api/bgp/sessions", fallback: false, failFallback: false, pending: 0, release: () => {} }
let release!: () => void
const gate = new Promise<void>(resolve => { release = resolve })
control.release = release
const servers = [{ id: 1, name: "Узел", host: "192.0.2.1", site: "TEST", country: "RU", asn: "64512", type: "exit-node", enabled: true, status: "online", latency: 10, os: "7", model: "CHR" }]
const snapshot = { sampledAt: "2026-09-20T00:00:00Z", servers, greTunnels: [], filtersRulesets: [], speedProbes: [], routeLookupByServerId: {}, wanRuntimeByHomeId: {} }
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
await page.route("**/api/**", async route => {
const path = new URL(route.request().url()).pathname
if (path === "/api/auth/config") return route.fulfill({ json: { required: false } })
if (path === "/api/evobgp/settings") return route.fulfill({ json: { enabled: false, baseUrl: "", secretConfigured: false } })
const fail = control.failed || (control.failFallback && path === "/api/uptime/speed-probes") || (control.partial && ["/api/bgp/sessions", "/api/filters/gre-tunnels", "/api/events", "/api/internet-path/latest", "/api/traffic/servers"].includes(path))
const prefixes = control.prefixes
if (control.hold && path === control.holdPath) {
control.hold = false
control.pending += 1
await gate
}
if (fail) return route.fulfill({ status: 503, json: { error: "Источник временно недоступен" } })
const json = path === "/api/servers" ? servers
: path === "/api/uptime/overview" ? { probes: [] }
: path === "/api/bgp/sessions" ? [{ state: "Established", prefixesRx: prefixes }]
: path === "/api/filters/rules" ? { rulesets: [] }
: path === "/api/filters/gre-tunnels" ? { tunnels: [{ id: "g1", name: "GRE сохранён", status: "down", serverId: "1" }] }
: path === "/api/wireguard" ? { interfaces: [], peers: [] }
: path === "/api/vxlan" ? { tunnels: [] }
: path === "/api/traffic/servers" ? { servers: [{ id: "1", rxNow: 1000, txNow: 500, rxSeries: [1000], txSeries: [500] }] }
: path === "/api/internet-path/latest" ? { snapshot: control.fallback ? null : snapshot }
: path === "/api/uptime/speed-probes" ? { probes: [] }
: path === "/api/events" ? { events: [{ id: "e1", title: "Событие сохранено", message: "Тестовое событие", level: "info", sourceModule: "system", eventType: "test", createdAt: "2026-09-20T00:00:00Z", entityId: null, entityType: null, payload: {} }] } : {}
return route.fulfill({ json })
})
await page.goto("/dashboard")
return control
}
const refresh = (page: Page) => page.getByRole("button", { name: "Обновить данные", exact: true })
const bgp = (page: Page) => page.getByRole("link").filter({ hasText: "BGP Rx" })
test("Дашборд: первоначальная ошибка не подменяется нулями и исправным состоянием", async ({ page }) => {
const control = await setup(page, true)
await expect(refresh(page)).toBeEnabled()
await expect(page.getByText("Не обновлены источники: 9", { exact: true }).first()).toBeVisible()
await expect(page.getByRole("link").filter({ hasText: "Флот" })).toContainText("—")
await expect(bgp(page)).toContainText("—")
await expect(page.getByText("Все online", { exact: true })).toHaveCount(0)
await expect(page.getByText("Пробы up", { exact: true })).toHaveCount(0)
control.failed = false
await refresh(page).click()
await expect(bgp(page)).toContainText("321")
await expect(page.getByText("Не обновлены источники: 9", { exact: true })).toHaveCount(0)
})
test("Дашборд: частичный сбой сохраняет данные, события и время последнего успеха", async ({ page }) => {
const control = await setup(page)
await expect(refresh(page)).toBeEnabled()
await expect(bgp(page)).toContainText("321")
const source = page.locator('[data-source="bgp"]')
const receivedAt = await source.locator("time").getAttribute("datetime")
control.partial = true
await refresh(page).click()
await expect(source).toContainText("ошибка обновления")
await expect(source.locator("time")).toHaveAttribute("datetime", receivedAt!)
await expect(bgp(page)).toContainText("321")
await expect(page.getByText("Событие сохранено", { exact: true })).toBeVisible()
await expect(page.getByText("GRE сохранён", { exact: true })).toBeVisible()
await expect(page.locator('[data-source="servers"]')).not.toContainText("ошибка")
await page.screenshot({ path: "test-results/dashboard-data-desktop.png", fullPage: true })
await page.setViewportSize({ width: 390, height: 844 })
await expect(source).toBeVisible()
await page.screenshot({ path: "test-results/dashboard-data-mobile.png", fullPage: true })
control.partial = false
control.prefixes = 654
await refresh(page).click()
await expect(bgp(page)).toContainText("654")
await expect(page.getByText(/Не обновлены источники:/)).toHaveCount(0)
})
test("Дашборд: поздний ответ не заменяет свежие данные при фоновом обновлении", async ({ page }) => {
const control = await setup(page)
await expect(bgp(page)).toContainText("321")
await expect(refresh(page)).toBeEnabled()
control.hold = true
control.prefixes = 111
await refresh(page).click()
await expect.poll(() => control.pending).toBe(1)
await expect(bgp(page)).toContainText("321")
await expect(page.getByText("Обновление данных…", { exact: true })).toBeVisible()
control.prefixes = 999
await page.evaluate(() => window.dispatchEvent(new Event("mm:uptime-probes-changed")))
await expect(bgp(page)).toContainText("999")
const oldResponse = page.waitForResponse(response => response.url().includes("/api/bgp/sessions"))
control.release()
await (await oldResponse).finished()
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))))
await expect(refresh(page)).toBeEnabled()
await expect(bgp(page)).toContainText("999")
})
test("Internet path: ошибка дополнительных запросов сохраняет прежний снимок", async ({ page }) => {
const control = await setup(page)
await expect(refresh(page)).toBeEnabled()
const source = page.locator('[data-source="internetPath"]')
const timestamp = await source.locator("time").first().getAttribute("datetime")
control.fallback = true
control.failFallback = true
await refresh(page).click()
await expect(source).toContainText("Не удалось обновить все данные Internet path")
await expect(source.locator("time").first()).toHaveAttribute("datetime", timestamp!)
await expect(source.locator("time").last()).toHaveAttribute("datetime", "2026-09-20T00:00:00Z")
})
test("Internet path: поздний дополнительный запрос не заменяет новый снимок", async ({ page }) => {
const control = await setup(page)
await expect(refresh(page)).toBeEnabled()
control.fallback = true
control.holdPath = "/api/uptime/speed-probes"
control.hold = true
await refresh(page).click()
await expect.poll(() => control.pending).toBe(1)
control.fallback = false
await page.evaluate(() => window.dispatchEvent(new Event("mm:uptime-probes-changed")))
await expect(refresh(page)).toBeEnabled()
const source = page.locator('[data-source="internetPath"]')
const timestamp = await source.locator("time").first().getAttribute("datetime")
const response = page.waitForResponse(r => r.url().includes("/api/uptime/speed-probes"))
control.release()
await (await response).finished()
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))))
await expect(source.locator("time").first()).toHaveAttribute("datetime", timestamp!)
await expect(source.locator("time").last()).toHaveAttribute("datetime", "2026-09-20T00:00:00Z")
})