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