Files
MikrotikManager/tests/e2e/uptime-links.spec.ts
T

926 lines
61 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, type Page } from "@playwright/test"
async function setup(page: Page, url: string, failed = 0, longNames = false, permission?: string, isAdmin = false, thirdServer = false) {
const control = { writes: [] as { method: string; path: string; body: unknown }[], failed, posts: [] as { path: string; body: unknown }[] }
const servers = [{ id: 1, name: "Источник", host: "192.0.2.1", type: "exit-node", enabled: true, country: "RU", site: "TEST", asn: "64512", status: "online", latency: 1, os: "7", model: "CHR", sessions:0, wanUplinks: [] }]
const probes = ["p1", "p/2"].map(id => ({ id, srcServerId: "1", name: "Проверка сети", target: "192.0.2.10", filter: "main", rtt: null, loss: 100, status: "down", series: [0], enabled: true, showOnDashboard: false }))
if (permission) {
await page.addInitScript(token => localStorage.setItem("mmapp_token", token), uptimeToken(permission, isAdmin))
servers.push({ ...servers[0], id: 2, name: "Назначение", host: "192.0.2.2" })
}
if (thirdServer) servers.push({ ...servers[0], id: 3, name: "Новое назначение", host: "192.0.2.3" })
if (longNames) {
servers[0].name = "Источник-с-длинным-названием-площадки-для-проверки-переноса"
probes.forEach(p => { p.name = "Проверка-доступности-длинного-имени-сетевого-узла"; p.target = "router-with-a-long-hostname.example.test"; p.filter = "Фильтр-с-длинным-названием" })
}
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
await page.route("**/api/**", route => {
const request = route.request()
const path = new URL(request.url()).pathname
if (!["GET", "HEAD"].includes(request.method())) control.writes.push({ method: request.method(), path, body: request.postDataJSON() })
if (request.method() === "POST") control.posts.push({ path, body: request.postDataJSON() })
if (path === "/api/uptime/overview" && control.failed) return route.fulfill({ status: control.failed, json: { error: "Мониторинг недоступен" } })
const json = path === "/api/auth/config" ? { required: Boolean(permission) }
: path === "/api/evobgp/settings" ? { enabled: false, baseUrl: "", secretConfigured: false }
: path === "/api/servers" ? servers
: path === "/api/uptime/overview" ? { probes:probes.map(p=>({...p,observations:{samples:[{sampledAt:"2026-09-20T00:00:00Z",rtt:null,loss:100,status:"down"}]}})),rangeStartAt:"2026-09-19T23:00:00Z",rangeEndAt:"2026-09-20T00:00:00Z",resources: [] }
: path === "/api/uptime/speed-probes" ? { probes: permission ? [{ id: "s1", srcServerId: "1", dstServerId: "2", srcInterface: "", dstInterface: "", protocol: "tcp", direction: "both", durationSec: "10", enabled: true,lastRunAt:null,lastTxAvgMbps:null,lastRxAvgMbps:null,lastStatus:null,lastError:null,lastPingRttMs:null,lastPingLossPct:null,lastPingAt:null,lastPingError:null }] : [] }
: path === "/api/uptime/speed-test/runs" ? { runs: [] }
: path === "/api/bgp/catalog" ? {sessions:[],failures:[]}
: path === "/api/filters/gre-tunnels" || path === "/api/vxlan" ? { tunnels: [],failures:[] }
: path === "/api/wireguard" ? { interfaces: [], peers: [],failures:[] }
: path === "/api/traffic/servers" ? { servers: [] }
: path === "/api/events" ? { events: [] }
: path === "/api/internet-path/latest" ? { snapshot: { collectionComplete:true,sampledAt: "2026-09-20T00:00:00Z", servers, greTunnels: [], filtersRulesets: [], speedProbes: [], routeLookupByServerId: {}, wanRuntimeByHomeId: {} } }
: {}
return route.fulfill({ json })
})
await page.goto(url)
return control
}
test("очередь проверок: точный ID, чтение без ping, ручной ping только выбранной проверки", async ({ page }) => {
const control = await setup(page, "/dashboard")
const queue = page.getByRole("region", { name: "Требует внимания", exact: true })
const link = queue.locator('a[href="/uptime?probe=p%2F2"]')
await expect(link).toBeVisible()
await link.click()
await expect(page).toHaveURL(/\/uptime\?probe=p%2F2$/)
const context = page.getByRole("status", { name: "Проверка по ссылке" })
await expect(context).toContainText("Выбрана проверка: Проверка сети")
await expect(page.locator("[data-probe-id]")).toHaveCount(1)
await expect(page.locator('[data-probe-id="p/2"]')).toBeVisible()
expect(control.posts).toEqual([])
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await expect(context).toContainText("Список ограничен")
expect(control.posts).toEqual([])
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect.poll(() => control.posts).toEqual([{ path: "/api/uptime/probes/collect-group", body: { probeIds: ["p/2"] } }])
await context.getByRole("link", { name: "К дашборду", exact: true }).click()
await expect(queue).toBeVisible()
})
test("проверка по прямой ссылке: сброс, возврат, перезагрузка и частичный сбой", async ({ page }) => {
const control = await setup(page, "/uptime?probe=p%2F2&keep=1")
const context = page.getByRole("status", { name: "Проверка по ссылке" })
await expect(context).toContainText("Выбрана проверка")
await context.getByRole("link", { name: "Все проверки", exact: true }).click()
await expect(page).toHaveURL(/\/uptime\?keep=1$/)
await expect(page.locator("[data-probe-id]")).toHaveCount(2)
await page.goBack()
await expect(page.locator("[data-probe-id]")).toHaveCount(1)
await page.reload()
await expect(context).toContainText("Выбрана проверка")
control.failed = 503
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await expect(context).toContainText("сохранённые данные могут быть устаревшими")
await expect(page.locator('[data-probe-id="p/2"]')).toBeVisible()
control.failed = 0
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await expect(context).toContainText("Список ограничен")
await page.setViewportSize({ width: 390, height: 844 })
await context.scrollIntoViewIfNeeded()
const box = await context.boundingBox()
expect(box!.x + box!.width).toBeLessThanOrEqual(390)
await page.screenshot({ path: "test-results/uptime-link-mobile.png", animations: "disabled" })
})
for (const failed of [0, 403, 500]) {
test(`недоступная проверка: ${failed || "не найден ID"}`, async ({ page }) => {
const control = await setup(page, "/uptime?probe=missing", failed)
await expect(page.getByRole("status", { name: "Проверка по ссылке" })).toContainText(failed ? "Не удалось обновить данные проверки" : "Проверка не найдена или недоступна")
await expect(page.locator("[data-probe-id]")).toHaveCount(0)
expect(control.posts).toEqual([])
})
}
for (const width of [320, 390, 1440]) {
test(`Ping-пробы ${width}px: длинные подписи, доступные действия и раскрытие`, async ({ page }) => {
await page.setViewportSize({ width, height: 1000 })
const control = await setup(page, "/uptime?probe=p%2F2", 0, true)
const row = page.locator('[data-probe-id="p/2"]')
await expect(row).toBeVisible()
await expect(row.getByText("График RTT", { exact: true })).toBeVisible({ visible: width < 1440 })
if (width < 1440) expect(await row.getByText("Фильтр-с-длинным-названием", { exact: true }).evaluate(el => el.getBoundingClientRect().right)).toBeLessThanOrEqual(width)
for (const name of ["Редактировать пробу", "Удалить пробу", "Показать на дашборде"]) {
const button = row.getByRole("button", { name, exact: true })
await button.scrollIntoViewIfNeeded()
const bounds = await button.boundingBox()
expect(bounds!.x).toBeGreaterThanOrEqual(0)
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width)
}
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width)
const toggle = row.getByRole("switch", { name: /Включить пробу/ })
await expect(toggle).toBeChecked()
const chart = row.getByRole("button", { name: /Подробный график RTT/ })
await chart.focus()
await page.keyboard.press("Enter")
await expect(chart).toHaveAttribute("aria-expanded", "true")
await page.keyboard.press("Enter")
await expect(chart).toHaveAttribute("aria-expanded", "false")
await row.scrollIntoViewIfNeeded()
await page.screenshot({ path: `test-results/ping-row-${width}.png`, animations: "disabled" })
expect(control.writes).toEqual([])
await row.getByRole("button", { name: "Редактировать пробу", exact: true }).click()
await expect(page.getByRole("dialog")).toBeVisible()
await page.keyboard.press("Escape")
await expect(page.getByRole("dialog")).toBeHidden()
await row.getByRole("button", { name: "Показать на дашборде", exact: true }).click()
await expect.poll(() => control.writes.some(w => w.method === "PATCH" && w.path === "/api/uptime/probes/p%2F2" && (w.body as { showOnDashboard: boolean }).showOnDashboard === true)).toBe(true)
await toggle.click()
await expect.poll(() => control.writes.some(w => w.method === "PUT" && w.path === "/api/uptime/probes")).toBe(true)
})
}
// UI claim fixtures; JWT verification remains a separate integration check.
function uptimeToken(permission: string, isAdmin = false) {
return "fixture." + Buffer.from(JSON.stringify({ apps: ["mm"], permissions: ["mm:servers:read", permission], is_admin: isAdmin, exp: Math.floor(Date.now() / 1000) + 3600 })).toString("base64url") + ".fixture"
}
for (const [permission, isAdmin] of [["mm:uptime:read", false], ["mm:uptime:read", true], ["mm:servers:write", false]] as const) {
test(`мониторинг только чтение: ${permission}, is_admin=${isAdmin}`, async ({ page }) => {
const control = await setup(page, "/uptime", 0, false, permission, isAdmin)
await expect(page.getByText("Только просмотр", { exact: true })).toBeVisible()
await expect(page.getByRole("button", { name: "Новая проба", exact: true })).toBeDisabled()
const row = page.locator('[data-probe-id="p1"]')
for (const name of ["Показать на дашборде", "Редактировать пробу", "Удалить пробу"]) {
await expect(row.getByRole("button", { name, exact: true })).toBeDisabled()
}
await expect(row.getByRole("switch")).toBeDisabled()
await expect(page.getByRole("button", { name: /Добавить интерфейс для/ })).toBeDisabled()
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeDisabled()
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await page.getByRole("button", { name: /Ресурсы/ }).click()
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
await expect(page.getByRole("button", { name: "Новая speed-проба", exact: true })).toBeDisabled()
for (const name of ["Запустить", "Редактировать speed-пробу", "Удалить speed-пробу"]) {
await expect(page.getByRole("button", { name, exact: true })).toBeDisabled()
}
await expect(page.getByRole("switch")).toBeDisabled()
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
expect(control.writes).toEqual([])
})
}
for (const permission of ["mm:uptime:write", "mm:uptime:admin"]) {
test(`мониторинг: ${permission} разрешает ручной ping`, async ({ page }) => {
const control = await setup(page, "/uptime?probe=p1", 0, false, permission)
await expect(page.getByRole("button", { name: "Новая проба", exact: true })).toBeEnabled()
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect.poll(() => control.posts.map(p => p.path)).toEqual(["/api/uptime/probes/collect-group"])
await page.getByRole("button", { name: /Скорость/ }).click()
await expect(page.getByRole("button", { name: "Запустить", exact: true })).toBeEnabled()
})
}
for (const speed of [false, true]) {
test(`мониторинг: отзыв права перед сохранением ${speed ? "Speed" : "Ping"}`, async ({ page }) => {
const control = await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
await expect(page.getByRole("dialog")).toBeVisible()
await page.evaluate(token => localStorage.setItem("mmapp_token", token), uptimeToken("mm:uptime:read"))
await page.getByRole("dialog").getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(page.getByRole("dialog")).toBeVisible()
await expect(page.getByRole("dialog").getByRole("alert")).toContainText("Недостаточно прав")
expect(control.writes).toEqual([])
})
}
for (const speed of [false, true]) {
for (const status of [400, 403, 500]) {
test(`сохранение ${speed ? "Speed" : "Ping"}: ${status} сохраняет черновик, повтор подтверждён`, async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
let fail = true
const bodies: { probes: Record<string, unknown>[] }[] = []
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", route => {
if (route.request().method() !== "PUT") return route.fallback()
bodies.push(route.request().postDataJSON())
return route.fulfill({ status: fail ? status : 200, json: fail ? { error: "Сохранение отклонено" } : { ok: true } })
})
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
const dialog = page.getByRole("dialog")
const input = speed ? dialog.locator('input').filter({ visible: true }).last() : dialog.getByPlaceholder("youtube.com")
await input.fill(speed ? "17" : "Черновик проверки")
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog.getByRole("alert")).toContainText(status === 403 ? "Недостаточно прав" : "Сохранение отклонено")
await expect(input).toHaveValue(speed ? "17" : "Черновик проверки")
fail = false
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog).toBeHidden()
expect(bodies).toHaveLength(2)
expect(bodies[1]).toEqual(bodies[0])
expect(bodies[1].probes[0][speed ? "durationSec" : "name"]).toBe(speed ? 17 : "Черновик проверки")
})
}
test(`сохранение ${speed ? "Speed" : "Ping"}: один PUT, Escape и отмена заблокированы до ответа`, async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
let calls = 0
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
if (route.request().method() !== "PUT") return route.fallback()
calls++
await pending
await route.fulfill({ json: { ok: true } })
})
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
const dialog = page.getByRole("dialog")
await dialog.getByRole("button", { name: "Сохранить", exact: true }).evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
await expect(dialog.getByRole("button", { name: "Сохранение…", exact: true })).toBeDisabled()
await expect(dialog.getByRole("button", { name: "Отмена", exact: true })).toBeDisabled()
await expect(dialog.locator("fieldset input").last()).toBeDisabled()
await page.keyboard.press("Escape")
await expect(dialog).toBeVisible()
expect(calls).toBe(1)
release()
await expect(dialog).toBeHidden()
})
}
for (const speed of [false, true]) {
test(`сохранение ${speed ? "Speed" : "Ping"}: таймаут сохраняет форму с неизвестным результатом`, async ({ page }) => {
await page.addInitScript(() => {
const timeout = AbortSignal.timeout.bind(AbortSignal)
AbortSignal.timeout = ms => timeout(ms === 30_000 ? 100 : ms)
})
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
if (route.request().method() !== "PUT") return route.fallback()
await pending
await route.fulfill({ json: { ok: true } }).catch(() => {})
})
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
const dialog = page.getByRole("dialog")
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog.getByRole("alert")).toContainText("Результат операции неизвестен")
await expect(dialog.getByRole("button", { name: "Сохранить", exact: true })).toBeEnabled()
release()
await expect(dialog).toBeVisible()
})
}
for (const speed of [false, true]) {
test(`переключение ${speed ? "Speed" : "Ping"}: ожидание API, отказ, повтор без потери состояния`, async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
let calls = 0
let fail = true
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
if (route.request().method() !== "PUT") return route.fallback()
calls++
expect(route.request().postDataJSON().probes[0].enabled).toBe(false)
if (fail) await pending
await route.fulfill({ status: fail ? 403 : 200, json: fail ? { error: "Forbidden" } : { ok: true } })
})
const toggle = page.getByRole("switch")
await expect(toggle).toBeChecked()
await toggle.click()
await expect(toggle).toBeDisabled()
await expect(toggle).toBeChecked()
await expect.poll(() => calls).toBe(1)
release()
await expect(page.getByRole("alert").filter({ hasText: "Недостаточно прав" })).toBeVisible()
await expect(toggle).toBeChecked()
await expect(toggle).toBeEnabled()
fail = false
await toggle.click()
await expect(toggle).not.toBeChecked()
await expect(toggle).toBeEnabled()
await expect(page.getByRole("alert").filter({ hasText: "Недостаточно прав" })).toHaveCount(0)
expect(calls).toBe(2)
})
}
for (const speed of [false, true]) {
test(`удаление ${speed ? "Speed" : "Ping"}: подтверждение, отказ и повтор`, async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
let calls = 0
let fail = true
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
if (route.request().method() !== "PUT") return route.fallback()
calls++
const rows = route.request().postDataJSON().probes
expect(rows.some((p: { id: string }) => p.id === (speed ? "s1" : "p1"))).toBe(false)
if (fail) await pending
await route.fulfill({ status: fail ? 403 : 200, json: fail ? { error: "Forbidden" } : { ok: true } })
})
const remove = page.getByRole("button", { name: speed ? "Удалить speed-пробу" : "Удалить пробу", exact: true })
await remove.click()
const dialog = page.getByRole("alertdialog")
await expect(dialog).toContainText(speed ? "Источник → Назначение" : "Проверка сети → 192.0.2.10")
await dialog.getByRole("button", { name: "Отмена", exact: true }).click()
expect(calls).toBe(0)
await remove.click()
await dialog.getByRole("button", { name: "Удалить", exact: true }).evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
await expect(dialog.getByRole("button", { name: "Удаление…", exact: true })).toBeDisabled()
await page.keyboard.press("Escape")
await expect(dialog).toBeVisible()
await expect.poll(() => calls).toBe(1)
release()
await expect(dialog.getByRole("alert")).toContainText("Недостаточно прав")
fail = false
await dialog.getByRole("button", { name: "Удалить", exact: true }).click()
await expect(dialog).toBeHidden()
await expect(remove).toHaveCount(0)
expect(calls).toBe(2)
})
}
test("звезда дашборда: состояние меняется после PATCH, ошибка сохраняет прежнее", async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
let calls = 0
let fail = true
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/uptime/probes/p1", async route => {
if (route.request().method() !== "PATCH") return route.fallback()
calls++
expect(route.request().postDataJSON()).toEqual({ showOnDashboard: true })
if (fail) await pending
await route.fulfill({ status: fail ? 500 : 200, json: fail ? { error: "Звезда не сохранена" } : { ok: true } })
})
const star = page.getByRole("button", { name: "Показать на дашборде", exact: true })
await star.click()
await expect(star).toBeDisabled()
await expect(star).toHaveAttribute("aria-pressed", "false")
await expect(page.getByRole("switch")).toBeDisabled()
await expect.poll(() => calls).toBe(1)
release()
await expect(page.getByRole("alert").filter({ hasText: "Звезда не сохранена" })).toBeVisible()
await expect(star).toBeEnabled()
fail = false
await star.click()
await expect(page.getByRole("button", { name: "Убрать с дашборда", exact: true })).toHaveAttribute("aria-pressed", "true")
expect(calls).toBe(2)
})
test("Speed-test: один запуск, запись проб заблокирована до ответа и освобождается при ошибке", async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
let calls = 0
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/uptime/speed-test", async route => {
calls++
await pending
await route.fulfill({ status: 500, json: { error: "Измерение отклонено" } })
})
const run = page.getByRole("button", { name: "Запустить", exact: true })
await run.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
await expect(run).toBeDisabled()
await expect(page.getByRole("button", { name: "Удалить speed-пробу", exact: true })).toBeDisabled()
await expect(page.getByRole("button", { name: "Редактировать speed-пробу", exact: true })).toBeDisabled()
await expect(page.getByRole("switch")).toBeDisabled()
await expect.poll(() => calls).toBe(1)
await page.getByRole("button", { name: /Пинг-пробы/ }).click()
await expect(page.getByRole("button", { name: "Показать на дашборде", exact: true })).toBeDisabled()
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeDisabled()
release()
await expect(page.getByRole("button", { name: "Показать на дашборде", exact: true })).toBeEnabled()
await page.getByRole("button", { name: /Скорость/ }).click()
await expect(run).toBeEnabled()
await expect(page.getByText("Измерение отклонено", { exact: true }).first()).toBeVisible()
})
for (const group of [false, true]) {
for (const status of [200, 403]) {
test(`ручной Ping ${group ? "группы" : "всех"}: один POST, блокировка записи, ответ ${status}`, async ({ page }) => {
await setup(page, group ? "/uptime?probe=p1" : "/uptime", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
let calls = 0
let reads = 0
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/uptime/overview?*", route => { reads++; return route.fallback() })
await page.route(group ? "**/api/uptime/probes/collect-group" : "**/api/uptime/collect-now", async route => {
calls++
await pending
await route.fulfill({ status, json: status === 200 ? { ok: true, polled: 1 } : { error: "Forbidden" } })
})
const refresh = page.getByRole("button", { name: "Обновить", exact: true }).first()
await refresh.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
await expect(refresh).toBeDisabled()
await expect(page.getByRole("button", { name: "Новая проба", exact: true })).toBeDisabled()
await expect(page.getByRole("switch").first()).toBeDisabled()
await expect.poll(() => calls).toBe(1)
await page.getByRole("button", { name: /Скорость/ }).click()
await expect(page.getByRole("button", { name: "Запустить", exact: true })).toBeDisabled()
release()
await expect(page.getByRole("button", { name: "Запустить", exact: true })).toBeEnabled()
await page.getByRole("button", { name: /Пинг-пробы/ }).click()
await expect(refresh).toBeEnabled()
if (status === 403) await expect(page.getByRole("alert").filter({ hasText: "Недостаточно прав" })).toBeVisible()
if (status === 200 || !group) expect(reads).toBeGreaterThan(0)
expect(calls).toBe(1)
})
}
}
test("Speed-test: таймаут освобождает действия и сообщает неизвестный результат", async ({ page }) => {
await page.addInitScript(() => {
const timeout = AbortSignal.timeout.bind(AbortSignal)
AbortSignal.timeout = ms => timeout(ms === 120_000 ? 100 : ms)
})
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/uptime/speed-test", async route => {
await pending
await route.fulfill({ json: { result: { txAvgMbps: 10, rxAvgMbps: 20 } } }).catch(() => {})
})
await page.getByRole("button", { name: "Запустить", exact: true }).click()
await expect(page.getByRole("alert").filter({ hasText: "Результат операции неизвестен" })).toBeVisible()
await expect(page.getByRole("button", { name: "Запустить", exact: true })).toBeEnabled()
await expect(page.getByRole("button", { name: "Редактировать speed-пробу", exact: true })).toBeEnabled()
release()
})
test("ручной Ping: поздняя ошибка выбранной пробы не попадает в новый список", async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/uptime/probes/collect-group", async route => {
await pending
await route.fulfill({ status: 500, json: { error: "Ошибка прежнего контекста" } })
})
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await page.getByRole("link", { name: "Все проверки", exact: true }).click()
await expect(page.locator("[data-probe-id]")).toHaveCount(2)
const response = page.waitForResponse(r => r.url().endsWith("/collect-group"))
release()
await response
await expect(page.getByText("Ошибка прежнего контекста", { exact: true })).toHaveCount(0)
await expect(page.getByRole("button", { name: "Новая проба", exact: true })).toBeEnabled()
})
test("URL мониторинга: вкладка, поиск, reload и история без записи", async ({ page }) => {
const control = await setup(page, "/uptime?probe=p1&keep=1", 0, false, "mm:uptime:read")
const search = page.getByPlaceholder("Имя, IP-адрес, фильтр…")
await search.fill("нет совпадений")
await expect(page.locator("[data-probe-id]")).toHaveCount(0)
await page.getByRole("button", { name: /Скорость/ }).click()
await expect(page).toHaveURL(url => url.searchParams.get("tab") === "speed" && url.searchParams.get("q") === "нет совпадений" && url.searchParams.get("probe") === "p1")
await page.reload()
await expect(page.getByRole("button", { name: /Скорость/ })).toHaveAttribute("aria-pressed", "true")
await page.goBack()
await expect(search).toHaveValue("нет совпадений")
await expect(page.locator("[data-probe-id]")).toHaveCount(0)
await page.goForward()
await expect(page.getByRole("button", { name: /Скорость/ })).toHaveAttribute("aria-pressed", "true")
await page.getByRole("button", { name: /Пинг-пробы/ }).click()
await page.getByRole("button", { name: "Сбросить", exact: true }).click()
await expect(page).toHaveURL(/\/uptime\?probe=p1&keep=1$/)
await expect(page.locator("[data-probe-id]")).toHaveCount(1)
expect(control.writes).toEqual([])
})
test("URL мониторинга: статус и неизвестный сервер не расширяют выдачу", async ({ page }) => {
const control = await setup(page, '/uptime?probe=p1&keep=1&status=down&servers=%5B%22999%22%5D', 0, false, "mm:uptime:read")
await expect(page.locator("[data-probe-id]")).toHaveCount(0)
await page.getByRole("button", { name: "Сбросить", exact: true }).click()
await expect(page.locator("[data-probe-id]")).toHaveCount(1)
await page.getByRole("button", { name: /^OK/ }).click()
await expect(page).toHaveURL(url => url.searchParams.get("status") === "up")
await expect(page.locator("[data-probe-id]")).toHaveCount(0)
await page.goBack()
await expect(page.locator("[data-probe-id]")).toHaveCount(1)
await page.locator('button[aria-pressed]').filter({ hasText: "Источник" }).click()
await expect(page).toHaveURL(url => url.searchParams.get("servers") === '["1"]')
await page.reload()
await expect(page.locator('button[aria-pressed]').filter({ hasText: "Источник" })).toHaveAttribute("aria-pressed", "true")
expect(control.writes).toEqual([])
})
for (const invalid of ["tab=unknown", "status=bad", "servers=%7B%7D", "q=a&q=b"]) {
test(`URL мониторинга: неверный параметр ${invalid}`, async ({ page }) => {
const control = await setup(page, "/uptime?probe=p1&keep=1&" + invalid)
await expect(page.getByRole("alert").filter({ hasText: "Параметры ссылки требуют внимания" })).toBeVisible()
await expect(page.locator("[data-probe-id]")).toHaveCount(0)
await page.getByRole("button", { name: "Сбросить параметры мониторинга", exact: true }).click()
await expect(page).toHaveURL(/\/uptime\?probe=p1&keep=1$/)
await expect(page.locator("[data-probe-id]")).toHaveCount(1)
expect(control.writes).toEqual([])
})
}
async function resourceSetup(page: Page, extra = "") {
const control = await setup(page, '/uptime?tab=resources&probe=p1&keep=1&q=ping' + extra, 0, false, "mm:uptime:read")
await page.route("**/api/uptime/overview?*", route => route.fulfill({ json: { probes: [], resources: [{ serverId: "1", hasData: true, cpu: 10, cpuHistory: [10], ramUsed: 100, ramTotal: 1000, hddUsed: 10, hddTotal: 100, uptimeSeconds: 60, boardName: "CHR" }] } }))
await page.reload()
return control
}
test("Resources URL: фильтры, смена вкладки, история и сброс сохраняют Ping-контекст", async ({ page }) => {
const control = await resourceSetup(page)
const rows = page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(1)
const search = page.getByPlaceholder("Поиск по имени, площадке…")
await search.fill("Источник")
await page.getByRole("button", { name: "JumpHost", exact: true }).click()
await expect(rows).toHaveCount(0)
await expect(page).toHaveURL(url => url.searchParams.get("rtype") === "jump-host" && url.searchParams.get("rq") === "Источник")
await page.goBack()
await expect(rows).toHaveCount(1)
await page.goForward()
await expect(rows).toHaveCount(0)
await page.reload()
await expect(search).toHaveValue("Источник")
await expect(rows).toHaveCount(0)
await page.getByRole("button", { name: /Пинг-пробы/ }).click()
await expect(page.getByPlaceholder("Имя, IP-адрес, фильтр…")).toHaveValue("ping")
await page.getByRole("button", { name: /Ресурсы/ }).click()
await expect(search).toHaveValue("Источник")
await expect(rows).toHaveCount(0)
await page.getByRole("button", { name: "Сбросить фильтры ресурсов", exact: true }).click()
await expect(rows).toHaveCount(1)
await expect(page).toHaveURL(url => !url.searchParams.has("rq") && !url.searchParams.has("rtype") && url.searchParams.get("q") === "ping" && url.searchParams.get("probe") === "p1" && url.searchParams.get("keep") === "1")
expect(control.writes).toEqual([])
})
for (const extra of ["&rtype=unknown", "&rq=a&rq=b"]) {
test(`Resources URL: неверные фильтры ${extra}`, async ({ page }) => {
await resourceSetup(page, extra)
await expect(page.getByRole("alert").filter({ hasText: "фильтры ресурсов" })).toBeVisible()
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
await page.getByRole("button", { name: "Сбросить фильтры ресурсов", exact: true }).click()
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
})
}
for (const width of [320, 390, 1440]) {
test(`приёмка ${width}px: Resources, Speed и формы проб`, async ({ page }) => {
await page.setViewportSize({ width, height: 844 })
await resourceSetup(page)
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
await page.screenshot({ path: `test-results/resources-${width}.png`, animations: "disabled" })
await page.unrouteAll({ behavior: "wait" })
await setup(page, "/uptime?probe=p1", 0, true, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
for (const speed of [false, true]) {
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
await expect(dialog.getByRole("button", { name: "Сохранить", exact: true })).toBeInViewport()
await expect(dialog.getByRole("button", { name: "Отмена", exact: true })).toBeInViewport()
await expect.poll(() => dialog.locator("button, input").evaluateAll(elements => elements.filter(el => {
const rect = el.getBoundingClientRect()
return rect.width > 0 && (rect.left < -1 || rect.right > innerWidth + 1)
}).map(el => el.textContent || el.tagName))).toEqual([])
await page.screenshot({ path: `test-results/${speed ? "speed" : "ping"}-form-${width}.png`, animations: "disabled" })
await dialog.locator("fieldset").evaluate(el => { el.scrollTop = el.scrollHeight })
if (speed) {
const direction = await dialog.getByRole("button", { name: "rx", exact: true }).boundingBox()
const duration = await dialog.locator("input").last().boundingBox()
expect(direction).not.toBeNull()
expect(duration).not.toBeNull()
expect(direction!.y + direction!.height).toBeLessThanOrEqual(duration!.y)
}
await page.screenshot({ path: `test-results/${speed ? "speed" : "ping"}-form-bottom-${width}.png`, animations: "disabled" })
await page.keyboard.press("Escape")
await expect(dialog).toBeHidden()
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
}
for (const name of [/Пинг-пробы/, /Ресурсы/, /Скорость/]) {
const bounds = await page.getByRole("button", { name }).boundingBox()
expect(bounds).not.toBeNull()
expect(bounds!.x).toBeGreaterThanOrEqual(0)
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width)
}
await page.screenshot({ path: `test-results/speed-${width}.png`, animations: "disabled" })
})
}
for (const width of [320, 390]) {
test(`Resources ${width}px: таблица после прокрутки и поиск`, async ({ page }) => {
await page.setViewportSize({ width, height: 844 })
await resourceSetup(page)
const search = page.getByPlaceholder('Поиск по имени, площадке…')
await search.scrollIntoViewIfNeeded()
await search.fill('Источник')
await expect(page.locator('tr[data-row-id]')).toHaveCount(1)
const cpu = page.getByRole('button', { name: 'CPU', exact: true })
await cpu.click()
await expect(cpu).toBeInViewport()
const viewport = page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]')
await viewport.evaluate(el => { el.scrollLeft = el.scrollWidth })
await expect(page.getByRole("columnheader").last()).toBeInViewport()
const lastValue = page.locator("tr[data-row-id] td").last().locator("span").last()
await lastValue.scrollIntoViewIfNeeded()
await expect(lastValue).toBeInViewport()
const home = page.getByRole("button", { name: "Home Router", exact: true })
const bounds = await home.boundingBox()
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width)
await page.screenshot({ path: `test-results/resources-table-${width}.png`, animations: 'disabled' })
await search.fill('нет-совпадений')
await expect(page.getByText('Ничего не найдено', { exact: true })).toBeVisible()
await search.clear()
await expect(page.locator('tr[data-row-id]')).toHaveCount(1)
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
})
}
test('Смена режима через настройки: поздняя ошибка Ping не появляется после возврата', async ({ page }) => {
await setup(page, '/uptime?probe=p1', 0, false, 'mm:uptime:write')
let release!: () => void
const gate = new Promise<void>(resolve => { release = resolve })
await page.route('**/api/uptime/probes/collect-group', async route => {
await gate
await route.fulfill({ status: 500, json: { error: 'Ошибка старого live-подключения' } })
})
const started = page.waitForRequest(r => r.url().endsWith('/collect-group'))
await page.getByRole('button', { name: 'Обновить', exact: true }).click()
await started
await page.locator('a[href="/settings"]').first().click()
await page.getByRole('button', { name: 'Моковые', exact: true }).click()
await page.goBack()
await expect(page.getByText(/Демо-режим · пробы/)).toBeVisible()
const response = page.waitForResponse(r => r.url().endsWith('/collect-group'))
release()
await response
await expect(page.getByText('Ошибка старого live-подключения', { exact: true })).toHaveCount(0)
await expect(page.getByRole('button', { name: 'Новая проба', exact: true })).toBeEnabled()
})
for (const active of [false, true]) test(`Speed interfaces: загрузка и повторное открытие, active=${active}`, async ({ page }) => {
await setup(page, "/uptime?tab=speed", 0, false, "mm:uptime:write")
const probe = { id: "s1", srcServerId: "1", dstServerId: "2", srcInterface: "ether1", dstInterface: "ether1", protocol: "tcp", direction: "both", durationSec: "10", enabled: true, lastRunAt: null, lastTxAvgMbps: null, lastRxAvgMbps: null, lastStatus: null, lastError: null, lastPingRttMs: null, lastPingLossPct: null, lastPingAt: null, lastPingError: null }
const bodies: { probes: Record<string, unknown>[] }[] = []
let interfaceCalls = 0
await page.route("**/api/uptime/speed-probes", route => {
if (route.request().method() === "PUT") { bodies.push(route.request().postDataJSON()); return route.fulfill({ json: { ok: true } }) }
return route.fulfill({ json: { probes: [probe] } })
})
await page.route("**/api/uptime/speed-test/endpoints/*/interfaces", route => { interfaceCalls++; return route.fulfill({ json: { interfaces: [{ name: "ether1", running: active, disabled: !active, addresses: ["192.0.2.1/24"] }] } }) })
await page.reload()
await page.getByRole("button", { name: "Пауза", exact: true }).click()
for (let attempt = 0; attempt < 2; attempt++) {
await page.getByRole("button", { name: "Редактировать speed-пробу", exact: true }).click()
await expect.poll(() => interfaceCalls).toBe(2)
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog).toBeHidden()
expect(bodies[attempt].probes[0].srcInterface).toBe(active ? "ether1" : "")
expect(bodies[attempt].probes[0].dstInterface).toBe(active ? "ether1" : "")
}
expect(interfaceCalls).toBe(2)
})
for (const invalid of ["2", "10junk", "3.5"]) test("Speed draft rejects invalid duration without PUT/" + invalid, async ({ page }) => {
const control = await setup(page, "/uptime", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
await page.getByRole("button", { name: "Редактировать speed-пробу", exact: true }).click()
const dialog = page.getByRole("dialog"), input = dialog.locator("input").filter({ visible: true }).last()
await input.fill(invalid)
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog.getByRole("alert")).toContainText("Проверьте speed-пробы")
await expect(dialog).toBeVisible(); expect(control.writes).toEqual([])
})
for (const malformed of [false, true]) test("Speed save requires confirmation and shows scheduler warning/" + malformed, async ({ page }) => {
await setup(page, "/uptime", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
let calls = 0
await page.route("**/api/uptime/speed-probes", route => {
if (route.request().method() !== "PUT") return route.fallback()
calls++; const body = route.request().postDataJSON()
expect(body.probes[0]).not.toHaveProperty("lastRunAt")
return route.fulfill({ json: malformed ? {} : { ok: true, warning: "Пробы сохранены, но расписание не обновлено. Проверьте состояние сборщика данных." } })
})
await page.getByRole("button", { name: "Редактировать speed-пробу", exact: true }).click()
const dialog = page.getByRole("dialog"), input = dialog.locator("input").filter({ visible: true }).last()
await input.fill("17")
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
if (malformed) {
await expect(dialog.getByRole("alert")).toContainText("Ответ о сохранении speed-проб не подтверждён")
await expect(input).toHaveValue("17")
} else {
await expect(dialog).toBeHidden()
await expect(page.getByRole("status").filter({ hasText: "Пробы сохранены, но расписание" })).toBeVisible()
}
expect(calls).toBe(1)
})
for (const malformed of [false, true]) test("Ping save requires confirmation and separates scheduler failure/" + malformed, async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
let calls = 0
await page.route("**/api/uptime/probes", route => { if (route.request().method() !== "PUT") return route.fallback(); calls++; return route.fulfill({ json: malformed ? {} : { ok: true, warning: "Ping-пробы сохранены, но расписание не обновлено. Проверьте состояние сборщика данных." } }) })
await page.getByRole("button", { name: "Редактировать пробу", exact: true }).click()
const dialog = page.getByRole("dialog"), input = dialog.getByPlaceholder("youtube.com")
await input.fill("Черновик")
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
if (malformed) { await expect(dialog.getByRole("alert")).toContainText("Ответ о сохранении Ping-проб не подтверждён"); await expect(input).toHaveValue("Черновик") }
else { await expect(dialog).toBeHidden(); await expect(page.getByRole("status").filter({ hasText: "Ping-пробы сохранены, но расписание" })).toBeVisible() }
expect(calls).toBe(1)
})
test("Ping catalog: malformed initial/refresh disables replacement, recovery and history deletion copy", async ({ page }, info) => {
const control = await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
let malformed = true
await page.route("**/api/uptime/overview*", route => malformed ? route.fulfill({ json: { probes: [{ id: "broken" }], resources: [] } }) : route.fallback())
await page.reload(); await page.getByRole("button", { name: "Пауза", exact: true }).click()
const warning = page.getByRole("alert", { name: "Чтение Ping-проб", exact: true })
await expect(warning).toContainText("Данные пока неизвестны")
await expect(page.getByRole("button", { name: "Новая проба", exact: true }).first()).toBeDisabled()
await expect(page.getByText("Пробы не найдены", { exact: true })).toHaveCount(0)
malformed = false; await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await expect(warning).toHaveCount(0); await expect(page.getByRole("button", { name: "Редактировать пробу", exact: true })).toHaveCount(1)
malformed = true; await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await expect(warning).toContainText("Показан последний успешный список")
await expect(page.getByRole("button", { name: "Редактировать пробу", exact: true }).first()).toBeDisabled()
await page.setViewportSize({ width: 320, height: 1000 })
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" })
await page.screenshot({ path: info.outputPath("ping-catalog-error-320.png"), animations: "disabled" })
malformed = false; await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await page.getByRole("button", { name: "Удалить пробу", exact: true }).first().click()
await expect(page.getByRole("alertdialog")).toContainText("Вместе с ней будет удалена история её измерений")
await page.getByRole("alertdialog").getByRole("button", { name: "Отмена", exact: true }).click()
expect(control.writes).toEqual([])
})
for (const invalid of [false, true]) {
test(`Speed-test: nullable POST, ноль и ${invalid ? "чужой runId" : "невалидная скорость"}`, async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
let calls = 0
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/uptime/speed-test", async route => {
calls++
const { runId } = route.request().postDataJSON()
if (calls === 1) await pending
await route.fulfill({ json: { ok: true, result: { runId: calls === 2 && invalid ? "foreign-run" : runId, txAvgMbps: calls === 2 && !invalid ? "999" : null, rxAvgMbps: calls === 2 ? 999 : 0 } } })
})
const run = page.getByRole("button", { name: "Запустить", exact: true })
const summary = page.locator('[aria-label="Сводка speed-проб"]')
await run.click()
await expect(page.getByText("Тест выполняется…", { exact: true })).toBeVisible()
await expect(summary).not.toContainText("0 Мбит/с")
release()
await expect(run).toBeEnabled()
await expect(run.locator("xpath=../../..").getByText("Неизвестно", { exact: true })).toBeVisible()
await expect(run.locator("xpath=../../..").getByText("0 Мбит/с", { exact: true })).toBeVisible()
await expect(summary).toContainText("1 run")
await run.click()
await expect(page.getByRole("alert").filter({ hasText: "Некорректное подтверждение speed-test" })).toBeVisible()
await expect(run).toBeEnabled()
await expect(summary).toContainText("1 run")
await expect(summary).not.toContainText("999 Мбит/с")
expect(calls).toBe(2)
})
}
test("Speed-test: конфликт пары устройств сохраняет каталог и освобождает действия", async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
let calls = 0
const message = "Пара устройств speed-пробы изменилась. Обновите список проб перед запуском"
await page.route("**/api/uptime/speed-test", async route => {
calls++
await route.fulfill({ status: 409, json: { error: message } })
})
const run = page.getByRole("button", { name: "Запустить", exact: true })
await run.click()
await expect(page.getByRole("alert").filter({ hasText: message })).toBeVisible()
await expect(run).toBeEnabled()
await expect(page.getByRole("button", { name: "Редактировать speed-пробу", exact: true })).toBeEnabled()
await expect(page.getByRole("button", { name: "Удалить speed-пробу", exact: true })).toBeEnabled()
await expect(page.locator('[aria-label="Сводка speed-проб"]')).toContainText("0 run")
await expect(run.locator("xpath=../../..")).toContainText("Источник")
await expect(run.locator("xpath=../../..")).toContainText("Назначение")
expect(calls).toBe(1)
})
test("Speed snapshot: смена пары сбрасывает снимок после подтверждения, история сохраняется", async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write", false, true)
await page.getByRole("button", { name: "Пауза", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
await page.route("**/api/uptime/speed-test", route => route.fulfill({ json: { ok: true, result: { runId: route.request().postDataJSON().runId, txAvgMbps: 30, rxAvgMbps: 80 } } }))
let fail = true, writes = 0
await page.route("**/api/uptime/speed-probes", route => {
if (route.request().method() !== "PUT") return route.fallback()
writes++
return route.fulfill(fail ? { status: 500, json: { error: "Пара не сохранена" } } : { json: { ok: true } })
})
const run = page.getByRole("button", { name: "Запустить", exact: true })
const row = () => page.getByRole("button", { name: "Запустить", exact: true, includeHidden: true }).locator("xpath=../../..")
await run.click()
await expect(row().getByText("80 Мбит/с", { exact: true })).toBeVisible()
const history = page.getByRole("table")
await expect(history).toContainText("80")
const summary = page.locator('[aria-label="Сводка speed-проб"]')
await expect(summary).toContainText("1 run")
await page.getByRole("button", { name: "Редактировать speed-пробу", exact: true }).click()
const dialog = page.getByRole("dialog")
const destination = () => dialog.locator('[data-slot="field"]').filter({ has: page.locator('[data-slot="field-label"]').filter({ hasText: /^Назначение$/ }) })
await destination().getByRole("button", { name: /Новое назначение/ }).click()
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog.getByRole("alert")).toContainText("Пара не сохранена")
await expect(row()).toContainText("80 Мбит/с")
fail = false
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog).toBeHidden()
await expect(row()).toContainText("Новое назначение")
await expect(row()).not.toContainText("80 Мбит/с")
await expect(summary).toContainText("1 run")
await expect(history).toContainText("80")
await page.getByRole("button", { name: "Редактировать speed-пробу", exact: true }).click()
await destination().getByRole("button", { name: /Назначение / }).click()
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog).toBeHidden()
await expect(row()).not.toContainText("80 Мбит/с")
await expect(history).toContainText("80")
expect(writes).toBe(3)
})
test("Speed snapshot: смена длительности сбрасывает снимок, прежняя история не накладывается", async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write", false, true)
await page.getByRole("button", { name: "Пауза", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
await page.route("**/api/uptime/speed-test", route => route.fulfill({ json: { ok: true, result: { runId: route.request().postDataJSON().runId, txAvgMbps: 30, rxAvgMbps: 80 } } }))
let fail = true, writes = 0
await page.route("**/api/uptime/speed-probes", route => {
if (route.request().method() !== "PUT") return route.fallback()
writes++
return route.fulfill(fail ? { status: 500, json: { error: "Пара не сохранена" } } : { json: { ok: true } })
})
const run = page.getByRole("button", { name: "Запустить", exact: true })
const row = () => page.getByRole("button", { name: "Запустить", exact: true, includeHidden: true }).locator("xpath=../../..")
await run.click()
await expect(row().getByText("80 Мбит/с", { exact: true })).toBeVisible()
const history = page.getByRole("table")
await expect(history).toContainText("80")
const summary = page.locator('[aria-label="Сводка speed-проб"]')
await expect(summary).toContainText("1 run")
await page.getByRole("button", { name: "Редактировать speed-пробу", exact: true }).click()
const dialog = page.getByRole("dialog")
const duration = () => dialog.locator('[data-slot="field"]').filter({ has: page.locator('[data-slot="field-label"]').filter({ hasText: /^Сек$/ }) }).getByRole("textbox")
await duration().fill("20")
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog.getByRole("alert")).toContainText("Пара не сохранена")
await expect(row()).toContainText("80 Мбит/с")
fail = false
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog).toBeHidden()
await expect(row()).toContainText("20")
await expect(row()).not.toContainText("80 Мбит/с")
await expect(summary).toContainText("1 run")
await expect(history).toContainText("80")
await page.getByRole("button", { name: "Редактировать speed-пробу", exact: true }).click()
await duration().fill("10")
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog).toBeHidden()
await expect(row()).not.toContainText("80 Мбит/с")
await expect(history).toContainText("80")
expect(writes).toBe(3)
})
for (const ping of [false, true]) test("Speed receipt: серверное время и отсутствие старого Ping/" + ping, async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
await page.route("**/api/uptime/speed-probes", route => route.fulfill({ json: { probes: [{ id: "s1", srcServerId: "1", dstServerId: "2", srcInterface: "", dstInterface: "", protocol: "tcp", direction: "both", durationSec: "10", enabled: true, lastRunAt: "2026-10-05T00:00:00Z", lastTxAvgMbps: 12, lastRxAvgMbps: 20, lastStatus: "done", lastError: null, lastPingRttMs: 99, lastPingLossPct: 0, lastPingAt: "2026-10-05T00:00:00Z", lastPingError: null }] } }))
await page.getByRole("button", { name: /Скорость/ }).click()
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
const launch = page.getByRole("button", { name: "Запустить", exact: true })
const probe = launch.locator("xpath=../../..")
await expect(probe).toContainText("99 мс")
const createdAt = "2026-10-06T00:00:00Z"
await page.route("**/api/uptime/speed-test", route => route.fulfill({ json: { ok: true, result: { runId: route.request().postDataJSON().runId, createdAt, txAvgMbps: 30, rxAvgMbps: 80, ...(ping ? { afterBtPing: { rttMs: 0, lossPct: 0, error: null } } : {}) } } }))
await launch.click()
await expect(probe).toContainText("80 Мбит/с")
await expect(probe).not.toContainText("99 мс")
if (ping) await expect(probe).toContainText("0 мс")
else await expect(probe.getByText("Ping", { exact: true })).toHaveCount(0)
const expectedTime = await page.evaluate(value => new Date(value).toLocaleString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit", day: "2-digit", month: "2-digit" }), createdAt)
await expect(page.getByRole("table")).toContainText(expectedTime)
})