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[] }[] = [] 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(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(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(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(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(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(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(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(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(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(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() })