666 lines
41 KiB
TypeScript
666 lines
41 KiB
TypeScript
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)
|
||
})
|
||
}
|
||
|
||
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()
|
||
})
|