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(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(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) })