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

644 lines
40 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) {
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", 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 (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, resources: [] }
: path === "/api/uptime/speed-probes" ? { probes: permission ? [{ id: "s1", srcServerId: "1", dstServerId: "2", srcInterface: "", dstInterface: "", protocol: "tcp", direction: "both", durationSec: "10", enabled: true }] : [] }
: path === "/api/uptime/speed-test/runs" ? { runs: [] }
: path === "/api/bgp/sessions" ? []
: path === "/api/filters/gre-tunnels" || path === "/api/vxlan" ? { tunnels: [] }
: path === "/api/wireguard" ? { interfaces: [], peers: [] }
: path === "/api/traffic/servers" ? { servers: [] }
: path === "/api/events" ? { events: [] }
: path === "/api/internet-path/latest" ? { snapshot: { 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)
})
}