import { test, expect, type Page } from "@playwright/test" import { serverReadSchema } from "../../packages/contracts/src/servers" import {dashboardGre,dashboardVxlan,dashboardWg} from "./fixtures/dashboard-overlays" import {bgpSession} from "./fixtures/bgp" async function setup(page: Page, initiallyFailed = false, serverCount = 1, initialBgpPayload?:unknown) { const control = { pathPayload:undefined as unknown,filterPayload:undefined as unknown,speedPayload:undefined as unknown, failed: initiallyFailed, partial: false, prefixes: 321 as number|null, grePayload:undefined as unknown,wgPayload:undefined as unknown,vxlanPayload:undefined as unknown,serversPayload:undefined as unknown,probesPayload:undefined as unknown, trafficPayload: undefined as unknown, bgpPayload: initialBgpPayload as unknown, hold: false, holdPath: "/api/bgp/catalog", fallback: false, failFallback: false, pending: 0, release: () => {} } let release!: () => void const gate = new Promise(resolve => { release = resolve }) control.release = release const servers = Array.from({ length: serverCount }, (_, index) => ({ id: index + 1, name: serverCount === 1 ? "Узел" : `Узел ${index + 1}`, host: `192.0.2.${index + 1}`, site: "TEST", country: "RU", asn: "64512", type: "exit-node", enabled: true, status: serverCount === 1 ? "online" : "offline", latency: 10, os: "7", model: "CHR" })).map(row => serverReadSchema.parse({ port: 443, useSsl: true, verifySsl: true, username: "test", password: "fixture-only", comment: "", lanSubnet: "", wanUplinks: [], createdAt: "2026-09-20T00:00:00Z", updatedAt: "2026-09-20T00:00:00Z", uptime: null, cpuLoad: null, freeMemory: null, totalMemory: null, identityName: null, sessions: 0, polledAt: null, ...row, })) const snapshot = { collectionComplete:true, sampledAt: "2026-09-20T00:00:00Z", servers, greTunnels: [], filtersRulesets: [], speedProbes: [], routeLookupByServerId: {}, wanRuntimeByHomeId: {} } await page.route("**/health", route => route.fulfill({ json: { ok: true } })) await page.route("**/api/**", async route => { const path = new URL(route.request().url()).pathname if (path === "/api/auth/config") return route.fulfill({ json: { required: false } }) if (path === "/api/evobgp/settings") return route.fulfill({ json: { enabled: false, baseUrl: "", secretConfigured: false } }) const fail = control.failed || (control.failFallback && path === "/api/uptime/speed-probes") || (control.partial && ["/api/bgp/catalog", "/api/filters/gre-tunnels", "/api/events", "/api/internet-path/latest", "/api/traffic/servers"].includes(path)) const prefixes = control.prefixes if (control.hold && path === control.holdPath) { control.hold = false control.pending += 1 await gate } if (fail) return route.fulfill({ status: 503, json: { error: "Источник временно недоступен" } }) const json = path === "/api/servers" ? control.serversPayload !== undefined ? control.serversPayload : servers : path === "/api/uptime/overview" ? control.probesPayload !== undefined ? control.probesPayload : { probes: [] } : path === "/api/bgp/catalog" ? control.bgpPayload !== undefined ? control.bgpPayload : {sessions:[bgpSession(1,prefixes)],failures:[]} : path === "/api/filters/rules" ? control.filterPayload!==undefined?control.filterPayload:{ rulesets: [] } : path === "/api/filters/gre-tunnels" ? control.grePayload!==undefined?control.grePayload:{ tunnels: [dashboardGre],failures:[] } : path === "/api/wireguard" ? control.wgPayload!==undefined?control.wgPayload:{ interfaces: [],failures:[] } : path === "/api/vxlan" ? control.vxlanPayload!==undefined?control.vxlanPayload:{ tunnels: [],failures:[] } : path === "/api/traffic/servers" ? control.trafficPayload !== undefined ? control.trafficPayload : { servers: [{ id: "1", rxNow: 1000, txNow: 500, rxSeries: [1000], txSeries: [500],observations:{rxNow:true,txNow:true,rxSeries:[true],txSeries:[true],sampledAt:"2026-09-20T00:00:00Z"} }] } : path === "/api/internet-path/latest" ? control.pathPayload!==undefined?control.pathPayload:{ snapshot: control.fallback ? null : snapshot } : path === "/api/uptime/speed-probes" ? control.speedPayload!==undefined?control.speedPayload:{ probes: [] } : path === "/api/events" ? { events: [{ id: "e1", title: "Событие сохранено", message: "Тестовое событие", level: "info", sourceModule: "system", eventType: "test", createdAt: "2026-09-20T00:00:00Z", entityId: null, entityType: null, payload: {} }] } : {} return route.fulfill({ json }) }) await page.goto("/dashboard") return control } const refresh = (page: Page) => page.getByRole("button", { name: "Обновить данные", exact: true }) const bgp = (page: Page) => page.getByRole("link").filter({ hasText: "Префиксы BGP" }) test("Дашборд: очередь над измерениями, полный счётчик и раскрытие остальных проблем", async ({ page }) => { await setup(page, false, 7) await expect(refresh(page)).toBeEnabled() const queue = page.getByRole("region", { name: "Требует внимания", exact: true }) await expect(queue.getByText("7", { exact: true })).toBeVisible() const serverLinks = queue.getByRole("link", { name: /^Узел \d/ }) await expect(serverLinks).toHaveCount(5) const more = queue.locator("summary").filter({ hasText: "Ещё 2" }) await more.focus() await more.press("Enter") await expect(serverLinks).toHaveCount(7) await more.press("Enter") await expect(serverLinks).toHaveCount(5) const queueBox = await queue.boundingBox() const measurementBox = await page.getByText("Задержка до серверов", { exact: true }).boundingBox() expect(queueBox!.y + queueBox!.height).toBeLessThan(measurementBox!.y) await expect(queue.getByText("Нет активных проверок", { exact: true })).toBeVisible() await page.screenshot({ path: "test-results/dashboard-priority-desktop.png", animations: "disabled" }) await page.setViewportSize({ width: 390, height: 844 }) await page.getByRole("navigation", { name: "Путь страницы" }).scrollIntoViewIfNeeded() await page.screenshot({ path: "test-results/dashboard-priority-mobile.png", animations: "disabled" }) expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) await serverLinks.first().click() await expect(page).toHaveURL(/\/servers\?server=1$/) await expect(page.getByRole("status", { name: "Сервер по ссылке" })).toContainText("Выбран сервер: Узел 1") await expect(page.locator("tr[data-row-id]")).toHaveCount(1) await page.goBack() await expect(queue).toBeVisible() }) test("Дашборд: первоначальная ошибка не подменяется нулями и исправным состоянием", async ({ page }) => { const control = await setup(page, true) await expect(refresh(page)).toBeEnabled() await expect(page.getByText("Не обновлены источники: 9", { exact: true }).first()).toBeVisible() await expect(page.getByRole("link").filter({ hasText: "Серверы онлайн" })).toContainText("—") await expect(bgp(page)).toContainText("—") await expect(page.getByText("Серверы без проблем", { exact: true })).toHaveCount(0) await expect(page.getByText("Проверки без ошибок", { exact: true })).toHaveCount(0) control.failed = false await refresh(page).click() await expect(bgp(page)).toContainText("321") await expect(page.getByText("Не обновлены источники: 9", { exact: true })).toHaveCount(0) }) test("Дашборд: частичный сбой сохраняет данные, события и время последнего успеха", async ({ page }) => { const control = await setup(page) await expect(refresh(page)).toBeEnabled() await expect(bgp(page)).toContainText("321") const source = page.locator('[data-source="bgp"]') const receivedAt = await source.locator("time").getAttribute("datetime") control.partial = true await refresh(page).click() await expect(source).toContainText("ошибка обновления") await expect(source.locator("time")).toHaveAttribute("datetime", receivedAt!) await expect(bgp(page)).toContainText("321") await expect(page.getByText("Событие сохранено", { exact: true })).toBeVisible() await expect(page.getByText("GRE сохранён", { exact: true })).toBeVisible() await expect(page.locator('[data-source="servers"]')).not.toContainText("ошибка") await page.screenshot({ path: "test-results/dashboard-data-desktop.png", fullPage: true }) await page.setViewportSize({ width: 390, height: 844 }) await expect(source).toBeVisible() await page.screenshot({ path: "test-results/dashboard-data-mobile.png", fullPage: true }) control.partial = false control.prefixes = 654 await refresh(page).click() await expect(bgp(page)).toContainText("654") await expect(page.getByText(/Не обновлены источники:/)).toHaveCount(0) }) test("Дашборд: поздний ответ не заменяет свежие данные при фоновом обновлении", async ({ page }) => { const control = await setup(page) await expect(bgp(page)).toContainText("321") await expect(refresh(page)).toBeEnabled() control.hold = true control.prefixes = 111 await refresh(page).click() await expect.poll(() => control.pending).toBe(1) await expect(bgp(page)).toContainText("321") await expect(page.getByText("Обновление данных…", { exact: true })).toBeVisible() control.prefixes = 999 await page.evaluate(() => window.dispatchEvent(new Event("mm:uptime-probes-changed"))) await expect(bgp(page)).toContainText("999") const oldResponse = page.waitForResponse(response => response.url().includes("/api/bgp/catalog")) control.release() await (await oldResponse).finished() await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))) await expect(refresh(page)).toBeEnabled() await expect(bgp(page)).toContainText("999") }) test("Internet path: ошибка дополнительных запросов сохраняет прежний снимок", async ({ page }) => { const control = await setup(page) await expect(refresh(page)).toBeEnabled() const source = page.locator('[data-source="internetPath"]') const timestamp = await source.locator("time").first().getAttribute("datetime") control.fallback = true control.failFallback = true await refresh(page).click() await expect(source).toContainText("Не удалось обновить все данные Internet path") await expect(source.locator("time").first()).toHaveAttribute("datetime", timestamp!) await expect(source.locator("time").last()).toHaveAttribute("datetime", "2026-09-20T00:00:00Z") }) test("Internet path: поздний дополнительный запрос не заменяет новый снимок", async ({ page }) => { const control = await setup(page) await expect(refresh(page)).toBeEnabled() control.fallback = true control.holdPath = "/api/uptime/speed-probes" control.hold = true await refresh(page).click() await expect.poll(() => control.pending).toBe(1) control.fallback = false await page.evaluate(() => window.dispatchEvent(new Event("mm:uptime-probes-changed"))) await expect(refresh(page)).toBeEnabled() const source = page.locator('[data-source="internetPath"]') const timestamp = await source.locator("time").first().getAttribute("datetime") const response = page.waitForResponse(r => r.url().includes("/api/uptime/speed-probes")) control.release() await (await response).finished() await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))) await expect(source.locator("time").first()).toHaveAttribute("datetime", timestamp!) await expect(source.locator("time").last()).toHaveAttribute("datetime", "2026-09-20T00:00:00Z") }) test('Сквозная приёмка: проблема → опрос сервера с отказом и повтором → возврат', async ({ page }) => { const writes: string[] = [] page.on('request', request => { if (!['GET', 'HEAD', 'OPTIONS'].includes(request.method())) writes.push(new URL(request.url()).pathname) }) await setup(page, false, 2) let fail = true await page.route('**/api/servers/2/poll', route => route.fulfill({ status: fail ? 403 : 200, json: fail ? { error: 'Опрос запрещён политикой сервера' } : {}, })) const queue = page.getByRole('region', { name: 'Требует внимания', exact: true }) await queue.getByRole('link', { name: /^Узел 2/ }).click() await expect(page).toHaveURL(/\/servers\?server=2$/) const context = page.getByRole('status', { name: 'Сервер по ссылке' }) await expect(context).toContainText('Узел 2') await expect(page.locator('tr[data-row-id]')).toHaveCount(1) expect(writes).toEqual([]) const poll = page.getByRole('button', { name: 'Проверить сервер', exact: true }) await poll.click() await expect(page.getByText('Опрос: успешно 0 из 1', { exact: true })).toBeVisible() await expect(context).toContainText('Узел 2') fail = false await poll.click() await expect(page.getByText('Опрос: успешно 1 из 1', { exact: true })).toBeVisible() expect(writes).toEqual(['/api/servers/2/poll', '/api/servers/2/poll']) await page.goBack() await expect(page).toHaveURL(/\/dashboard$/) await expect(queue.getByRole('link', { name: /^Узел 2/ })).toBeVisible() await page.goForward() await expect(context).toContainText('Узел 2') await expect(page.locator('tr[data-row-id]')).toHaveCount(1) await expect(poll).toBeEnabled() expect(writes).toEqual(['/api/servers/2/poll', '/api/servers/2/poll']) }) for(const broken of ["missing","partial","duplicate","counter","conflict"])test("Dashboard strict BGP unknown and last good/"+broken,async({page})=>{ const sessions=[bgpSession()],failure={serverId:2,serverName:"Узел без ответа",error:"timeout"};const payload=broken==="missing"?{sessions:[]}:broken==="partial"?{sessions,failures:[failure]}:broken==="duplicate"?{sessions:[...sessions,...sessions],failures:[]}:broken==="counter"?{sessions:[bgpSession(1,-1)],failures:[]}:{sessions,failures:[{...failure,serverId:1}]}; const control=await setup(page,false,1,payload);const source=page.locator('[data-source="bgp"]');await expect(refresh(page)).toBeEnabled();await expect(source).toContainText("ошибка");await expect(bgp(page)).toContainText("—"); control.bgpPayload=undefined;await refresh(page).click();await expect(bgp(page)).toContainText("321");const receivedAt=await source.locator("time").getAttribute("datetime");control.bgpPayload=payload;await refresh(page).click();await expect(source).toContainText("ошибка обновления");await expect(source.locator("time")).toHaveAttribute("datetime",receivedAt!);await expect(bgp(page)).toContainText("321");await expect(page.locator('[data-source="servers"]')).not.toContainText("ошибка"); control.bgpPayload=undefined;control.prefixes=999;await refresh(page).click();await expect(bgp(page)).toContainText("999");await expect(source).not.toContainText("ошибка"); }) for(const unknown of ["prefix","state"])test("Dashboard BGP accepts observed unknown without inventing zero/"+unknown,async({page})=>{ const control=await setup(page);await expect(bgp(page)).toContainText("321");await expect(refresh(page)).toBeEnabled();control.bgpPayload={sessions:[unknown==="prefix"?bgpSession(1,null):{...bgpSession(),state:"Unknown"}],failures:[]};await refresh(page).click();await expect(bgp(page)).toContainText("—");await expect(bgp(page)).toContainText("префиксы неизвестны");await expect(bgp(page)).toContainText(unknown==="prefix"?"Сессий установлено: 1":"Сессий установлено: неизвестно");await expect(page.locator('[data-source="bgp"]')).not.toContainText("ошибка"); control.bgpPayload={sessions:[],failures:[]};await refresh(page).click();await expect(bgp(page)).toContainText("Сессий установлено: 0");await expect(bgp(page)).not.toContainText("—"); }) for(const broken of ["missing","duplicate","counter","null","length"])test("Dashboard traffic rejects unconfirmed totals and keeps last good/"+broken,async({page})=>{ const control=await setup(page);const source=page.locator('[data-source="traffic"]');await expect(refresh(page)).toBeEnabled();await expect(source.locator("time")).toHaveCount(2);const receivedAt=await source.locator("time").first().getAttribute("datetime");const rate={id:"1",rxNow:1000,txNow:500,rxSeries:[1000],txSeries:[500],observations:{rxNow:true,txNow:true,rxSeries:[true],txSeries:[true],sampledAt:"2026-09-20T00:00:00Z"}};control.trafficPayload=broken==="missing"?{}:broken==="duplicate"?{servers:[rate,rate]}:broken==="counter"?{servers:[{...rate,rxNow:-1}]}:broken==="null"?{servers:[{...rate,rxNow:null}]}:{servers:[{...rate,txSeries:[]}]};await refresh(page).click();await expect(source).toContainText("ошибка обновления");await expect(source.locator("time").first()).toHaveAttribute("datetime",receivedAt!);await expect(page.getByText("RX 1.00 Гбит/с",{exact:true})).toBeVisible();await expect(page.locator('[data-source="servers"]')).not.toContainText("ошибка");control.trafficPayload={servers:[]};await refresh(page).click();await expect(source).not.toContainText("ошибка");await expect(page.getByText("Нет исторических измерений за выбранный период",{exact:true})).toBeVisible();await expect(page.getByText("RX 0 Кбит/с",{exact:true})).toBeVisible(); }) for(const state of ["legacy","unobserved","measured-zero","reset-rx","invalid-time","invalid-mask"])test("Dashboard traffic observation metadata/"+state,async({page})=>{ const control=await setup(page);const source=page.locator('[data-source="traffic"]');await expect(refresh(page)).toBeEnabled();await expect(page.getByText("RX 1.00 Гбит/с",{exact:true})).toBeVisible();const rate={id:"1",rxNow:0,txNow:0,rxSeries:[0],txSeries:[0]};const observations={rxNow:true,txNow:true,rxSeries:[true],txSeries:[true],sampledAt:"2026-10-03T00:00:00Z"};control.trafficPayload={servers:[state==="legacy"?rate:{...rate,observations:state==="unobserved"?{rxNow:false,txNow:false,rxSeries:[false],txSeries:[false],sampledAt:null}:state==="reset-rx"?{...observations,rxNow:false,rxSeries:[false]}:state==="invalid-time"?{...observations,sampledAt:"invalid"}:state==="invalid-mask"?{...observations,rxSeries:[]}:observations}]};await refresh(page).click();await expect(refresh(page)).toBeEnabled(); if(state.startsWith("invalid")){await expect(source).toContainText("ошибка обновления");await expect(page.getByText("RX 1.00 Гбит/с",{exact:true})).toBeVisible();return} await expect(source).not.toContainText("ошибка");if(state==="measured-zero"){await expect(page.getByText("RX 0 Кбит/с",{exact:true})).toBeVisible();await expect(page.getByRole("region",{name:"История RX/TX",exact:true})).toBeVisible();await expect(page.getByText("Нет исторических измерений за выбранный период",{exact:true})).toHaveCount(0)}else{await expect(page.getByText("RX —",{exact:true})).toBeVisible();if(state==="reset-rx"){await expect(page.getByText("TX 0 Кбит/с",{exact:true})).toBeVisible();await expect(page.getByText("Нет измерений RX за выбранный период",{exact:true})).toBeVisible()}else{await expect(page.getByText("TX —",{exact:true})).toBeVisible();await expect(page.getByText("Нет исторических измерений за выбранный период",{exact:true})).toBeVisible()}} if(state==="legacy")await expect(page.getByText("Измерения не подтверждены API",{exact:true})).toBeVisible() }) for(const kind of ["servers","probes"])for(const broken of ["missing","duplicate","numeric"])test("Dashboard strict catalog keeps source cache/"+kind+"/"+broken,async({page})=>{ const control=await setup(page);await expect(refresh(page)).toBeEnabled();const source=page.locator('[data-source="'+kind+'"]');const receivedAt=await source.locator("time").getAttribute("datetime");const server={id:1,name:"Узел",host:"192.0.2.1",site:"TEST",country:"RU",asn:"64512",type:"exit-node",enabled:true,status:"online",latency:1,os:"7",model:"CHR",sessions:0};const probe={id:"p1",srcServerId:"1",name:"Тест",target:"192.0.2.2",filter:"main",rtt:1,loss:0,status:"up",series:[1],enabled:true,showOnDashboard:true}; if(kind==="servers")control.serversPayload=broken==="missing"?{}:broken==="duplicate"?[server,server]:[{...server,latency:-1}];else control.probesPayload=broken==="missing"?{}:broken==="duplicate"?{probes:[probe,probe]}:{probes:[{...probe,loss:101}]};await refresh(page).click();await expect(source).toContainText("ошибка обновления");await expect(source.locator("time")).toHaveAttribute("datetime",receivedAt!);if(kind==="servers")await expect(page.getByRole("link").filter({hasText:"Серверы онлайн"})).toContainText("1 / 1");else await expect(page.getByText("Проверки без ошибок",{exact:true})).toHaveCount(0); control.serversPayload=undefined;control.probesPayload=undefined;await refresh(page).click();await expect(source).not.toContainText("ошибка"); }) test("Dashboard unknown server status is not reported offline or healthy",async({page})=>{ const control=await setup(page);await expect(refresh(page)).toBeEnabled();control.serversPayload=[{id:1,name:"Неопрошенный узел",host:"192.0.2.1",site:"TEST",country:"RU",asn:"64512",type:"exit-node",enabled:true,status:null,latency:null,os:null,model:null,sessions:0}];await refresh(page).click();const tile=page.getByRole("link").filter({hasText:"Серверы онлайн"});await expect(tile).toContainText("Статус неизвестен: 1");await expect(tile).not.toContainText("не в сети");await expect(page.getByRole("region",{name:"Требует внимания",exact:true}).getByRole("link",{name:/Неопрошенный узел/})).toContainText("Статус неизвестен");await expect(page.getByText("Серверы без проблем",{exact:true})).toHaveCount(0);await expect(page.locator('[data-source="servers"]')).not.toContainText("ошибка"); }) for(const state of ["legacy","unmeasured","zero","failure","gaps","invalid"])test("Dashboard ping distinguishes observed and missing measurements/"+state,async({page})=>{ const control=await setup(page);await expect(refresh(page)).toBeEnabled();const probe={id:"ping-observation",srcServerId:"1",name:"Измерение ping",target:"192.0.2.2",filter:"main",rtt:0,loss:0,status:"up",series:[0],enabled:true,showOnDashboard:true};const sample=(minute:number,rtt:number|null,status="up",loss=0)=>({sampledAt:"2026-10-03T00:"+String(minute).padStart(2,"0")+":00Z",rtt,status,loss});const samples=state==="unmeasured"?[]:state==="failure"?[sample(30,null,"down",100)]:state==="gaps"?[sample(29,5),sample(30,6),sample(32,0)]:state==="invalid"?[sample(30,0),sample(30,0)]:[sample(30,0)];control.probesPayload=state==="legacy"?{probes:[probe]}:{rangeStartAt:"2026-10-03T00:00:00Z",rangeEndAt:"2026-10-03T01:00:00Z",probes:[{...probe,observations:{samples}}]};await refresh(page).click();await expect(refresh(page)).toBeEnabled();const source=page.locator('[data-source="probes"]');if(state==="invalid"){await expect(source).toContainText("ошибка обновления");return}await expect(source).not.toContainText("ошибка");const row=page.locator('tr[data-row-id]').filter({hasText:"Измерение ping"});await expect(row).toHaveCount(1); if(state==="legacy"||state==="unmeasured"){await expect(row).toContainText("Неизвестно");await expect(page.getByRole("link").filter({hasText:"Проверки с проблемами"})).toContainText("неизвестны: 1");await expect(page.getByText("Проверки без ошибок",{exact:true})).toHaveCount(0);await expect(row).not.toContainText("100%");await expect(page.locator('[data-latency-source]')).toHaveCount(0)}else if(state==="failure"){await expect(row).toContainText("Оффлайн");await expect(row).toContainText("100%");await expect(page.getByRole("link").filter({hasText:"Проверки с проблемами"})).toContainText("1 ошибок")}else{await expect(row).toContainText("Онлайн");await expect(row).toContainText("0%");await expect(page.locator('[data-latency-isolated]')).toHaveCount(1);if(state==="gaps"){await expect(page.locator('.recharts-line-curve')).toHaveAttribute("d",/M.*M/);for(const width of [1440,320]){await page.setViewportSize({width,height:900});await page.getByRole("region",{name:"История RTT по источникам",exact:true}).scrollIntoViewIfNeeded();expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true);await page.screenshot({path:"docs/ux-ui/screens/dashboard-observed-ping-"+width+".png",animations:"disabled"})}}else await expect(page.locator('[data-latency-segment]')).toHaveCount(0);await expect(source.locator("time").last()).toHaveAttribute("datetime",new Date(samples.at(-1)!.sampledAt).toISOString())} }) for(const kind of ["gre","wg","vxlan"] as const)for(const broken of ["missing","partial","duplicate"])test("Dashboard overlay complete source and cache/"+kind+"/"+broken,async({page})=>{ const control=await setup(page);await expect(refresh(page)).toBeEnabled();const source=page.locator('[data-source="'+kind+'"]'),controlKey=kind==="gre"?"grePayload":kind==="wg"?"wgPayload":"vxlanPayload",rowsKey=kind==="wg"?"interfaces":"tunnels",row=kind==="gre"?dashboardGre:kind==="wg"?dashboardWg:dashboardVxlan;control[controlKey]={[rowsKey]:[row],failures:[]};await refresh(page).click();await expect(source).not.toContainText("ошибка");const receivedAt=await source.locator("time").getAttribute("datetime");control[controlKey]=broken==="missing"?{}:broken==="partial"?{[rowsKey]:[row],failures:[{serverId:"2",serverName:"Узел без ответа",error:"timeout"}]}:{[rowsKey]:[row,row],failures:[]};await refresh(page).click();await expect(source).toContainText("ошибка обновления");await expect(source.locator("time")).toHaveAttribute("datetime",receivedAt!);await expect(page.getByRole("link").filter({hasText:"Туннели работают"})).toContainText("Последний успешный снимок");await expect(page.locator('[data-source="servers"]')).not.toContainText("ошибка");if(kind==="gre")await expect(page.getByText("GRE сохранён",{exact:true})).toBeVisible();control[controlKey]={[rowsKey]:[],failures:[]};await refresh(page).click();await expect(source).not.toContainText("ошибка");if(kind==="gre")await expect(page.getByText("GRE сохранён",{exact:true})).toHaveCount(0) }) for(const broken of ["missing","incomplete","foreign","invalid-date","corrupt-null","legacy","filters","speed"])test("Dashboard Internet path keeps last confirmed source/"+broken,async({page})=>{ const control=await setup(page);await expect(refresh(page)).toBeEnabled();const source=page.locator('[data-source="internetPath"]');await expect(source).not.toContainText("ошибка");const times=await source.locator("time").evaluateAll(nodes=>nodes.map(n=>n.getAttribute("datetime"))); if(broken==="filters"||broken==="speed"){control.fallback=true;if(broken==="filters")control.filterPayload={rulesets:[],stale:true};else control.speedPayload={}} else control.pathPayload=broken==="missing"?{}:broken==="corrupt-null"?{snapshot:null,sampledAt:"2026-09-20T00:00:00Z"}:{snapshot:{collectionComplete:broken==="legacy"?undefined:true,sampledAt:broken==="invalid-date"?"invalid":"2026-09-20T00:00:00Z",servers:[],greTunnels:[],filtersRulesets:[],speedProbes:broken==="incomplete"?undefined:[],routeLookupByServerId:broken==="foreign"?{"99":null}:{},wanRuntimeByHomeId:{}}}; await refresh(page).click();await expect(source).toContainText("ошибка обновления");expect(await source.locator("time").evaluateAll(nodes=>nodes.map(n=>n.getAttribute("datetime")))).toEqual(times);await expect(page.locator('[data-source="servers"]')).not.toContainText("ошибка"); control.pathPayload=undefined;control.filterPayload=undefined;control.speedPayload=undefined;control.fallback=false;await refresh(page).click();await expect(source).not.toContainText("ошибка"); }) for(const state of ["unmeasured","measured","failed","unknown"])test("Dashboard Internet path hop observations/"+state,async({page})=>{ const control=await setup(page);await expect(refresh(page)).toBeEnabled();const at="2026-10-04T00:00:00Z"; const nodes=["home-router","jump-host","exit-node"].map((type,i)=>({id:i+1,name:"Path node "+(i+1),host:"192.0.2."+(i+1),site:"TEST",country:"RU",asn:"64512",type,enabled:true,status:state==="unknown"&&i===0?null:"online",latency:99,os:"7",model:"CHR",sessions:0,wanUplinks:i===0?[{id:"wan1",name:"WAN",isp:"Test ISP",iface:"ether1",ip:"192.0.2.10",maxDl:999,maxUl:888}]:[]})); const probe={id:"hop1",srcServerId:"1",dstServerId:"2",srcInterface:"ether1",dstInterface:"",enabled:true,lastPingRttMs:0,lastPingLossPct:0,lastTxAvgMbps:0,lastRxAvgMbps:5,lastPingAt:state==="unmeasured"?null:at,lastRunAt:state==="unmeasured"?null:at,lastStatus:state==="failed"?"error":"done",lastError:state==="failed"?"denied":null,lastPingError:state==="failed"?"denied":null}; control.pathPayload={snapshot:{collectionComplete:true,sampledAt:at,servers:nodes,greTunnels:[],filtersRulesets:[],speedProbes:[probe],routeLookupByServerId:{"1":{gateway:"192.0.2.254",routingMark:null}},wanRuntimeByHomeId:{"1":{defaultGateway:"192.0.2.254",defaultInterface:"ether1",uplinks:[{id:"wan1",name:"WAN",isp:"Test ISP",iface:"ether1",configuredIp:"192.0.2.10",leasedIp:"192.0.2.10",dhcpStatus:"bound",isDefault:true}]}}}}; await refresh(page).click();await expect(refresh(page)).toBeEnabled();await expect(page.locator('[data-source="internetPath"]')).not.toContainText("ошибка");const strip=page.getByText("JH/Exit — схема кандидатов. Default route подтверждает только WAN. Метрики — последние подтверждённые пробы участков.",{exact:true}).locator("..");await expect(strip).toBeVisible();await expect(strip).not.toContainText("999");await expect(strip).not.toContainText("99 мс"); if(state==="measured"||state==="unknown"){await expect(strip.getByText("0 мс",{exact:true})).toBeVisible();await expect(strip.getByText("5 ↓ Мбит/с",{exact:true})).toBeVisible()}else{await expect(strip.getByText("0 мс",{exact:true})).toHaveCount(0);await expect(strip.getByText("5 ↓ Мбит/с",{exact:true})).toHaveCount(0)} if(state==="unknown"){await expect(strip.getByText("Неизвестно",{exact:true})).toBeVisible();await expect(strip.getByText("Оффлайн",{exact:true})).toHaveCount(0)} await page.setViewportSize({width:320,height:900});await expect(strip).toBeVisible();expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true) }) test("Dashboard large attention queue mounts rows on demand",async({page},info)=>{ test.setTimeout(90000);const start=Date.now();await setup(page,false,1000);await expect(refresh(page)).toBeEnabled();const queue=page.getByRole("region",{name:"Требует внимания",exact:true});const links=queue.locator('a[href^="/servers?server="]');await expect(links).toHaveCount(5);const sample={fixtureRows:1000,readyMs:Date.now()-start,renderedAttentionRows:await links.count(),domNodes:await page.locator("*").count()};console.log(JSON.stringify(sample));await info.attach("large-attention-metrics",{body:JSON.stringify(sample),contentType:"application/json"});const summary=queue.locator("summary").filter({hasText:"Ещё 995"});await summary.focus();await summary.press("Enter");await expect(links).toHaveCount(55);await queue.getByRole("button",{name:"Показать ещё 50",exact:true}).click();await expect(links).toHaveCount(105);await summary.focus();await summary.press("Enter");await expect(links).toHaveCount(5);await summary.press("Enter");await expect(links).toHaveCount(55);await page.setViewportSize({width:320,height:900});expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true);await links.first().click();await expect(page).toHaveURL(/servers\?server=1$/);await expect(page.getByRole("status",{name:"Сервер по ссылке"})).toContainText("Узел 1") }) test("Dashboard active probes retain page after refreshed measurements",async({page})=>{ const control=await setup(page);await expect(refresh(page)).toBeEnabled();const at="2026-10-04T00:00:00Z";const probes=Array.from({length:100},(_,i)=>({id:"large-p"+(i+1),srcServerId:"1",name:"Paged probe "+String(i+1).padStart(3,"0"),target:"192.0.2.2",filter:"main",rtt:0,loss:0,status:"up",series:[],enabled:true,showOnDashboard:true,observations:{samples:[{sampledAt:at,rtt:0,loss:0,status:"up"}]}}));control.probesPayload={rangeStartAt:"2026-10-03T23:00:00Z",rangeEndAt:at,probes};await refresh(page).click();await expect(page.locator("tr[data-row-id]")).toHaveCount(50);await page.getByRole("button",{name:"Следующая страница",exact:true}).click();await expect(page.getByText("Paged probe 051",{exact:true})).toBeVisible();await refresh(page).click();await expect(refresh(page)).toBeEnabled();await expect(page.getByText("51–100 из 100",{exact:true})).toBeVisible();await expect(page.getByText("Paged probe 051",{exact:true})).toBeVisible();control.probesPayload={rangeStartAt:"2026-10-03T23:00:00Z",rangeEndAt:at,probes:probes.slice(0,1)};await refresh(page).click();await expect(page.locator("tr[data-row-id]")).toHaveCount(1);await expect(page.getByText("Paged probe 001",{exact:true})).toBeVisible() })