Files
MikrotikManager/tests/e2e/uptime-speed-data.spec.ts
T

85 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from "@playwright/test"
import axe from "axe-core"
import { server } from "./fixtures/users"
const run = { id:"history-1", srcServerId:"1", dstServerId:"2", srcInterface:"ether1", dstInterface:"ether1", protocol:"tcp", direction:"both", durationSec:10, txAvgMbps:42, rxAvgMbps:0, status:"done", error:null, createdAt:"2026-10-04T00:00:00Z" }
for (const resource of ["probes", "history"] as const) test("Скорость: initial unknown и независимое recovery/" + resource, async ({ page }) => {
let fail = true
await page.route("**/health",r=>r.fulfill({json:{ok:true}}))
await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname; const target=resource === "probes" ? "/api/uptime/speed-probes" : "/api/uptime/speed-test/runs";return r.fulfill(p === target && fail ? {status:503,json:{error:"Источник недоступен"}} : {json:p === "/api/auth/config" ? {required:false} : p === "/api/servers" ? [server] : p === "/api/uptime/overview" ? {probes:[],resources:[]} : p === "/api/uptime/speed-probes" ? {probes:[]} : p === "/api/uptime/speed-test/runs" ? {runs:[run]} : {}})})
await page.goto("/uptime?tab=speed"); await page.getByRole("button",{name:"Пауза",exact:true}).click()
const alert=page.getByRole("alert",{name:"Чтение скорости: "+resource,exact:true}); await expect(alert).toContainText("Данные пока неизвестны")
if(resource === "probes") { await expect(page.locator('tr[data-row-id="history-1"]')).toBeVisible(); await expect(page.getByText("Нет speed-проб",{exact:true})).toHaveCount(0) }
else { await expect(page.getByText("Нет speed-проб",{exact:true})).toBeVisible(); await expect(page.getByText("История тестов: данные пока неизвестны",{exact:true})).toBeVisible() }
fail=false; await page.getByRole("button",{name:"Обновить",exact:true}).click(); await expect(alert).toHaveCount(0); await expect(page.locator('tr[data-row-id="history-1"]')).toBeVisible()
})
test("Скорость: background503 и malformed сохраняют последнюю историю и свежесть", async ({page}) => {
let failure="",value=42
await page.route("**/health",r=>r.fulfill({json:{ok:true}})); await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;return r.fulfill(p === "/api/uptime/speed-test/runs" && failure === "503" ? {status:503,json:{error:"История недоступна"}} : {json:p === "/api/auth/config" ? {required:false} : p === "/api/servers" ? [server] : p === "/api/uptime/overview" ? {probes:[],resources:[]} : p === "/api/uptime/speed-probes" ? {probes:[]} : p === "/api/uptime/speed-test/runs" ? failure === "malformed" ? {} : {runs:[{...run,txAvgMbps:value}]} : {}})})
await page.goto("/uptime?tab=speed"); const row=page.locator('tr[data-row-id="history-1"]'); await expect(row).toContainText("42 Мбит/с"); await page.getByRole("button",{name:"Пауза",exact:true}).click()
const freshness=page.getByRole("status").filter({hasText:/^История тестов: получено/}); const original=await freshness.textContent()
for(const kind of ["503","malformed"]) {failure=kind; await page.getByRole("button",{name:"Обновить",exact:true}).click(); await expect(page.getByRole("alert",{name:"Чтение скорости: history",exact:true})).toContainText("Последние данные сохранены"); await expect(row).toContainText("42 Мбит/с"); await expect(freshness).toHaveText(original!); await expect(page.getByText("Нет speed-проб",{exact:true})).toBeVisible()}
failure="";value=84;await page.getByRole("button",{name:"Обновить",exact:true}).click();await expect(row).toContainText("84 Мбит/с");await expect(page.getByRole("alert",{name:"Чтение скорости: history",exact:true})).toHaveCount(0)
})
test("Скорость: поздняя история не задерживает пробы и не заменяет новый ответ", async ({page}) => {
let release!:()=>void;const gate=new Promise<void>(r=>release=r);let historyCalls=0,newer=false
await page.route("**/health",r=>r.fulfill({json:{ok:true}}));await page.route("**/api/**",async r=>{const p=new URL(r.request().url()).pathname;if(p === "/api/uptime/speed-test/runs") {historyCalls++;const old=!newer;if(old) await gate;return r.fulfill({json:{runs:[{...run,id:old ? "stale" : "current"}]}}).catch(()=>{})}return r.fulfill({json:p === "/api/auth/config" ? {required:false} : p === "/api/servers" ? [server] : p === "/api/uptime/overview" ? {probes:[],resources:[]} : p === "/api/uptime/speed-probes" ? {probes:[]} : {}})})
await page.goto("/uptime?tab=speed");await page.getByRole("button",{name:"Пауза",exact:true}).click();await expect(page.getByText("Нет speed-проб",{exact:true})).toBeVisible();await expect(page.getByText("История тестов: данные пока неизвестны",{exact:true})).toBeVisible()
newer=true;await page.getByRole("button",{name:"Обновить",exact:true}).click();await expect(page.locator('tr[data-row-id="current"]')).toBeVisible();const late=page.waitForResponse(async response => response.url().endsWith("/api/uptime/speed-test/runs") && (await response.json()).runs[0].id === "stale");release();await (await late).finished();await page.evaluate(()=>new Promise<void>(resolve=>requestAnimationFrame(()=>requestAnimationFrame(()=>resolve()))));await expect.poll(()=>historyCalls).toBeGreaterThan(1);await expect(page.getByRole("button",{name:"Обновить",exact:true})).toBeEnabled();await expect(page.locator('tr[data-row-id="stale"]')).toHaveCount(0)
})
test("Скорость: null не становится нулём, malformed DTO сохраняет последний результат", async ({page}) => {
let entries:unknown[]=[{...run,txAvgMbps:null,rxAvgMbps:0}]
await page.route("**/health",r=>r.fulfill({json:{ok:true}}));await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;return r.fulfill({json:p === "/api/auth/config" ? {required:false} : p === "/api/servers" ? [server] : p === "/api/uptime/overview" ? {probes:[],resources:[]} : p === "/api/uptime/speed-probes" ? {probes:[{id:"probe-1",srcServerId:"1",dstServerId:"2",srcInterface:"ether1",dstInterface:"ether1",protocol:"tcp",direction:"both",durationSec:"10",enabled:true,lastStatus:"done",lastRunAt:"2026-10-03T00:00:00Z",lastTxAvgMbps:999,lastRxAvgMbps:0,lastError:null}]} : p === "/api/uptime/speed-test/runs" ? {runs:entries} : {}})})
await page.goto("/uptime?tab=speed");await page.getByRole("button",{name:"Пауза",exact:true}).click();const row=page.locator('tr[data-row-id="history-1"]');await expect(row).toContainText("Неизвестно");await expect(row).toContainText("0 Мбит/с");await expect(page.getByText("999 Мбит/с",{exact:true})).toHaveCount(0)
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:"docs/ux-ui/screens/speed-history-null-320.png",fullPage:true,animations:"disabled"})
const summary=page.locator('[aria-label="Сводка speed-проб"]');await expect(summary).toContainText("Макс TX");await expect(summary).toContainText("—");await expect(summary).toContainText("0 Мбит/с")
for(const bad of [{...run,status:"unknown"},{...run,txAvgMbps:-1},{...run,createdAt:"bad"},run]) {
entries=bad === run ? [run,run] : [bad];await page.getByRole("button",{name:"Обновить",exact:true}).click();await expect(page.getByRole("alert",{name:"Чтение скорости: history",exact:true})).toContainText("Некорректная история тестов скорости");await expect(row).toContainText("Неизвестно")
}
entries=[{...run,txAvgMbps:0}];await page.getByRole("button",{name:"Обновить",exact:true}).click();await expect(page.getByRole("alert",{name:"Чтение скорости: history",exact:true})).toHaveCount(0);await expect(row).not.toContainText("Неизвестно");await expect(row).toContainText("0 Мбит/с")
})
test("Speed-пробы: malformed каталог, cache/freshness и блокировка замены до recovery", async ({page}) => {
const probe={id:"probe-1",srcServerId:"1",dstServerId:"2",srcInterface:"ether1",dstInterface:"ether1",protocol:"tcp",direction:"both",durationSec:"10",enabled:true,lastRunAt:null,lastTxAvgMbps:null,lastRxAvgMbps:0,lastStatus:null,lastError:null}
let raw:unknown={probes:[{...probe,enabled:"yes"}]};const writes:string[]=[]
await page.route("**/health",r=>r.fulfill({json:{ok:true}}));await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;if(r.request().method() !== "GET")writes.push(p);return r.fulfill({json:p === "/api/auth/config" ? {required:false} : p === "/api/servers" ? [server] : p === "/api/uptime/overview" ? {probes:[],resources:[]} : p === "/api/uptime/speed-probes" ? raw : p === "/api/uptime/speed-test/runs" ? {runs:[]} : {}})})
await page.goto("/uptime?tab=speed");await page.getByRole("button",{name:"Пауза",exact:true}).click();const alert=page.getByRole("alert",{name:"Чтение скорости: probes",exact:true});await expect(alert).toContainText("Данные пока неизвестны");await expect(page.getByText("Нет speed-проб",{exact:true})).toHaveCount(0);await expect(page.getByRole("button",{name:"Новая speed-проба",exact:true})).toBeDisabled()
const refresh=page.getByRole("button",{name:"Обновить",exact:true});raw={probes:[probe]};await refresh.click();await expect(alert).toHaveCount(0);const toggle=page.getByRole("switch");await expect(toggle).toBeChecked();await expect(toggle).toBeEnabled();const freshness=page.getByRole("status").filter({hasText:/^Speed-пробы: получено/});const before=await freshness.textContent()
for(const invalid of [{probes:[probe,probe]},{probes:[{...probe,lastRxAvgMbps:-1}]},{probes:[{...probe,lastStatus:"other"}]},{probes:[{...probe,lastRunAt:"bad"}]}]) {raw=invalid;await refresh.click();await expect(alert).toContainText("Последние данные сохранены");await expect(freshness).toHaveText(before!);await expect(toggle).toBeChecked();await expect(toggle).toBeDisabled();await expect(page.getByRole("button",{name:"Новая speed-проба",exact:true})).toBeDisabled()}
await page.setViewportSize({width:320,height:1000});expect(await page.evaluate(()=>document.documentElement.scrollWidth <= innerWidth)).toBe(true);await expect(toggle).toHaveAccessibleName(/Включить speed-пробу/)
await page.addScriptTag({content:axe.source});const audit=await page.evaluate(async()=> (window as unknown as {axe:typeof axe}).axe.run(document.querySelector('[aria-label="Чтение скорости: probes"]')!,{runOnly:{type:"tag",values:["wcag2a","wcag2aa","wcag21a","wcag21aa"]}}));expect(audit.violations).toEqual([])
await page.addStyleTag({content:"nextjs-portal{display:none!important}"});await page.screenshot({path:"docs/ux-ui/screens/speed-probes-error-320.png",fullPage:true,animations:"disabled"})
raw={probes:[probe]};await refresh.click();await expect(alert).toHaveCount(0);await expect(toggle).toBeEnabled();expect(writes).toEqual([])
})
const pingCases = [
{ name: "missing", value: undefined, label: "Нет данных" },
{ name: "unknown", value: { rttMs: null, lossPct: null, error: null }, label: "RTT неизвестен" },
{ name: "loss100", value: { rttMs: null, lossPct: 100, error: null }, label: "Нет ответов" },
{ name: "zero", value: { rttMs: 0, lossPct: 0, error: null }, label: "0 мс" },
{ name: "error", value: { rttMs: null, lossPct: null, error: "Ошибка Ping" }, label: "ошибка" },
]
for (const sample of pingCases) test("Ping после BT: данные, неизвестный RTT, отсутствие ответов и ошибка/" + sample.name, async ({ page }) => {
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
await page.route("**/api/**", route => {
const path = new URL(route.request().url()).pathname
return route.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [server] : path === "/api/uptime/overview" ? { probes: [], resources: [] } : path === "/api/uptime/speed-probes" ? { probes: [{ id: "probe-1", srcServerId: "1", dstServerId: "2", srcInterface: "ether1", dstInterface: "ether1", protocol: "tcp", direction: "both", durationSec: "10", enabled: true, lastStatus: "done", lastRunAt: "2026-10-03T00:00:00Z", lastTxAvgMbps: 999, lastRxAvgMbps: 0, lastError: null, lastPingRttMs: 99, lastPingLossPct: 0, lastPingAt: "2026-10-03T00:00:00Z", lastPingError: null }] } : path === "/api/uptime/speed-test/runs" ? { runs: [{ ...run, afterBtPing: sample.value }] } : {} })
})
await page.goto("/uptime?tab=speed")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
const history = page.locator('tr[data-row-id="history-1"]')
await expect(history).toContainText(sample.label)
const probe = page.getByRole("button", { name: "Запустить", exact: true }).locator("xpath=../../..")
await expect(probe).not.toContainText("99 мс")
if (sample.name === "missing") await expect(probe.getByText("Ping", { exact: true })).toHaveCount(0)
else await expect(probe).toContainText(sample.name === "error" ? "Ошибка Ping" : sample.label)
await expect(history).not.toContainText("timeout")
await expect(probe).not.toContainText("timeout")
if (sample.name === "loss100") await expect(history).toContainText("100%")
await page.setViewportSize({ width: 320, height: 1000 })
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
})