fix(ui): сохранять устройство оптимизатора в адресе
This commit is contained in:
1 parent
2cdfa2cb13
commit
c82434c435
5 files changed
+70
-19
No files matched your search
@@ -1,7 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useState, useMemo, useRef } from "react"
|
import { useCallback, useEffect, useState, useMemo, useRef, Suspense } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import {useSearchParams} from "next/navigation"
|
||||||
import {can} from "@/lib/auth"
|
import {can} from "@/lib/auth"
|
||||||
import {ospfCatalogSchema,ospfOptimizeSchema} from "@/lib/ospf-response"
|
import {ospfCatalogSchema,ospfOptimizeSchema} from "@/lib/ospf-response"
|
||||||
import {optimizerServersSchema,optimizerRulesSchema,optimizerProbesSchema,optimizerOspfPreviewSchema,type OptimizerOspfPreview} from "@/lib/route-optimizer-response"
|
import {optimizerServersSchema,optimizerRulesSchema,optimizerProbesSchema,optimizerOspfPreviewSchema,type OptimizerOspfPreview} from "@/lib/route-optimizer-response"
|
||||||
@@ -396,11 +397,21 @@ function TopologyBar() {
|
|||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export default function RouteOptimizerPage() {
|
export default function RouteOptimizerPage() {
|
||||||
const { mode, backendUrl } = useDataSource()
|
return <Suspense fallback={<p role="status">Загрузка контекста оптимизатора…</p>}><RouteOptimizerRoute /></Suspense>
|
||||||
return <RouteOptimizerContent key={JSON.stringify([mode, backendUrl])} />
|
}
|
||||||
|
function RouteOptimizerRoute() {
|
||||||
|
const {mode,backendUrl}=useDataSource()
|
||||||
|
const params=useSearchParams()
|
||||||
|
const requestedServer=params.get("server")
|
||||||
|
function selectServer(server:string) {
|
||||||
|
const url=new URL(window.location.href)
|
||||||
|
url.searchParams.set("server",server)
|
||||||
|
window.history.pushState(null,"",url.pathname+url.search+url.hash)
|
||||||
|
}
|
||||||
|
return <RouteOptimizerContent key={JSON.stringify([mode,backendUrl])} requestedServer={requestedServer} onSelectServer={selectServer} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function RouteOptimizerContent() {
|
function RouteOptimizerContent({requestedServer,onSelectServer}:{requestedServer:string|null;onSelectServer:(server:string)=>void}) {
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl, backendStatus } = useDataSource()
|
||||||
/** Данные из API/БД при режиме «Живые»; не ждём /health — иначе до ответа показывались моки. */
|
/** Данные из API/БД при режиме «Живые»; не ждём /health — иначе до ответа показывались моки. */
|
||||||
const useLiveData = mode === "live"
|
const useLiveData = mode === "live"
|
||||||
@@ -449,7 +460,9 @@ function RouteOptimizerContent() {
|
|||||||
const [ecmpAlgo, setEcmpAlgo] = useState<"per-dst" | "per-conn" | "per-packet">("per-dst")
|
const [ecmpAlgo, setEcmpAlgo] = useState<"per-dst" | "per-conn" | "per-packet">("per-dst")
|
||||||
const [rpfMode, setRpfMode] = useState<"disabled" | "loose" | "strict">("disabled")
|
const [rpfMode, setRpfMode] = useState<"disabled" | "loose" | "strict">("disabled")
|
||||||
const [selectedVrf, setSelectedVrf] = useState("main")
|
const [selectedVrf, setSelectedVrf] = useState("main")
|
||||||
const [ospfServerId, setOspfServerId] = useState("")
|
const ospfServerId=requestedServer ?? String(liveServers[0]?.id ?? "")
|
||||||
|
const ospfEligible=/^[1-9]\d*$/.test(ospfServerId) && liveServers.some(server=>String(server.id)===ospfServerId)
|
||||||
|
const contextError=requestedServer!==null && !loading && !ospfEligible ? "Устройство из ссылки неизвестно или отключено. Выберите доступное устройство." : ""
|
||||||
const [ospfApplying, setOspfApplying] = useState(false)
|
const [ospfApplying, setOspfApplying] = useState(false)
|
||||||
const [applyResult, setApplyResult] = useState<{owner:string;serverName:string;optimizedCount:number;receivedAt:string}|null>(null)
|
const [applyResult, setApplyResult] = useState<{owner:string;serverName:string;optimizedCount:number;receivedAt:string}|null>(null)
|
||||||
const [applyError,setApplyError]=useState({owner:"",message:""})
|
const [applyError,setApplyError]=useState({owner:"",message:""})
|
||||||
@@ -462,7 +475,7 @@ function RouteOptimizerContent() {
|
|||||||
const ospfMetaError=metaState.owner===ospfServerId?metaState.error:""
|
const ospfMetaError=metaState.owner===ospfServerId?metaState.error:""
|
||||||
const ospfPreview=previewState.owner===previewKey?previewState.data:null
|
const ospfPreview=previewState.owner===previewKey?previewState.data:null
|
||||||
const ospfPreviewError=previewState.owner===previewKey?previewState.error:""
|
const ospfPreviewError=previewState.owner===previewKey?previewState.error:""
|
||||||
const ospfPreviewLoading=useLiveData && Boolean(ospfServerId) && (previewState.owner!==previewKey || previewState.pending)
|
const ospfPreviewLoading=useLiveData && ospfEligible && (previewState.owner!==previewKey || previewState.pending)
|
||||||
useEffect(()=>{activeContext.current=previewKey},[previewKey])
|
useEffect(()=>{activeContext.current=previewKey},[previewKey])
|
||||||
const ospfApplyResult=applyResult?.owner===previewKey?applyResult:null
|
const ospfApplyResult=applyResult?.owner===previewKey?applyResult:null
|
||||||
const ospfApplyError=applyError.owner===previewKey?applyError.message:""
|
const ospfApplyError=applyError.owner===previewKey?applyError.message:""
|
||||||
@@ -502,7 +515,6 @@ function RouteOptimizerContent() {
|
|||||||
const {jumpHosts,exitNodes} = mapApiServersToTopology(rows)
|
const {jumpHosts,exitNodes} = mapApiServersToTopology(rows)
|
||||||
const enabledRows=rows.filter(row=>row.enabled)
|
const enabledRows=rows.filter(row=>row.enabled)
|
||||||
setLiveServers(enabledRows);setLiveJumpHosts(jumpHosts);setLiveExitNodes(exitNodes)
|
setLiveServers(enabledRows);setLiveJumpHosts(jumpHosts);setLiveExitNodes(exitNodes)
|
||||||
setOspfServerId(previous=>previous && enabledRows.some(row=>String(row.id)===previous)?previous:String(enabledRows[0]?.id??""))
|
|
||||||
setData(next);setError("");setReadAt(new Date().toISOString())
|
setData(next);setError("");setReadAt(new Date().toISOString())
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (current()) setError(error instanceof Error ? error.message : "Ошибка загрузки данных")
|
if (current()) setError(error instanceof Error ? error.message : "Ошибка загрузки данных")
|
||||||
@@ -639,7 +651,7 @@ function RouteOptimizerContent() {
|
|||||||
setSettings(prev => ({ ...prev, [k]: v }))
|
setSettings(prev => ({ ...prev, [k]: v }))
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(!useLiveData||!ospfServerId)return
|
if(!useLiveData||!ospfEligible)return
|
||||||
let cancelled=false
|
let cancelled=false
|
||||||
void apiFetch<unknown>("/api/ospf/catalog").then(raw=>{
|
void apiFetch<unknown>("/api/ospf/catalog").then(raw=>{
|
||||||
if(cancelled)return
|
if(cancelled)return
|
||||||
@@ -655,10 +667,10 @@ function RouteOptimizerContent() {
|
|||||||
if(!cancelled)setMetaState(previous=>({owner:ospfServerId,data:previous.owner===ospfServerId?previous.data:null,error:error instanceof Error?error.message:"Ошибка каталога OSPF"}))
|
if(!cancelled)setMetaState(previous=>({owner:ospfServerId,data:previous.owner===ospfServerId?previous.data:null,error:error instanceof Error?error.message:"Ошибка каталога OSPF"}))
|
||||||
})
|
})
|
||||||
return()=>{cancelled=true}
|
return()=>{cancelled=true}
|
||||||
},[apiFetch,liveServers,ospfServerId,useLiveData,previewTick])
|
},[apiFetch,liveServers,ospfServerId,ospfEligible,useLiveData,previewTick])
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(!useLiveData||!ospfServerId)return
|
if(!useLiveData||!ospfEligible)return
|
||||||
let cancelled=false
|
let cancelled=false
|
||||||
queueMicrotask(()=>{
|
queueMicrotask(()=>{
|
||||||
if(cancelled)return
|
if(cancelled)return
|
||||||
@@ -673,7 +685,7 @@ function RouteOptimizerContent() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
return()=>{cancelled=true}
|
return()=>{cancelled=true}
|
||||||
},[apiFetch,ospfServerId,settings.pingWeight,useLiveData,previewKey,readAt,previewTick])
|
},[apiFetch,ospfServerId,ospfEligible,settings.pingWeight,useLiveData,previewKey,readAt,previewTick])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full min-h-0 min-w-0">
|
<div className="flex flex-col h-full min-h-0 min-w-0">
|
||||||
@@ -834,6 +846,7 @@ function RouteOptimizerContent() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{useLiveData && contextError && <p role="alert" className="text-xs text-destructive">{contextError}</p>}
|
||||||
{useLiveData && (
|
{useLiveData && (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg border bg-muted/15 overflow-hidden">
|
<div className="rounded-lg border bg-muted/15 overflow-hidden">
|
||||||
@@ -841,9 +854,10 @@ function RouteOptimizerContent() {
|
|||||||
<select aria-label="Устройство OSPF"
|
<select aria-label="Устройство OSPF"
|
||||||
className="w-full min-w-0 sm:w-auto sm:min-w-64 text-xs bg-background text-foreground border border-input rounded-md px-2 py-1 h-7 focus:outline-none focus:ring-1 focus:ring-ring"
|
className="w-full min-w-0 sm:w-auto sm:min-w-64 text-xs bg-background text-foreground border border-input rounded-md px-2 py-1 h-7 focus:outline-none focus:ring-1 focus:ring-ring"
|
||||||
value={ospfServerId}
|
value={ospfServerId}
|
||||||
onChange={(e) => setOspfServerId(e.target.value)}
|
onChange={(e) => onSelectServer(e.target.value)}
|
||||||
>
|
>
|
||||||
{liveServers.length === 0 && <option value="">Нет доступных серверов</option>}
|
{requestedServer !== null && !ospfEligible && <option value={requestedServer}>Недоступное устройство</option>}
|
||||||
|
{liveServers.length === 0 && requestedServer === null && <option value="">Нет доступных серверов</option>}
|
||||||
{liveServers.map((s) => (
|
{liveServers.map((s) => (
|
||||||
<option key={s.id} value={String(s.id)}>
|
<option key={s.id} value={String(s.id)}>
|
||||||
{s.name || s.host} ({s.host})
|
{s.name || s.host} ({s.host})
|
||||||
@@ -851,13 +865,13 @@ function RouteOptimizerContent() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<span className="text-[11px] text-muted-foreground">
|
<span className="text-[11px] text-muted-foreground">
|
||||||
{ospfMetaError ? "Каталог OSPF недостоверен" : ospfMeta ? `${ospfMeta.interfaces} iface · ${ospfMeta.areas} area` : "сбор OSPF-метрик…"}
|
{contextError ? "Устройство недоступно" : ospfMetaError ? "Каталог OSPF недостоверен" : ospfMeta ? `${ospfMeta.interfaces} iface · ${ospfMeta.areas} area` : "сбор OSPF-метрик…"}
|
||||||
</span>
|
</span>
|
||||||
<Button type="button"
|
<Button type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="ml-auto h-7 text-xs"
|
className="ml-auto h-7 text-xs"
|
||||||
onClick={() => void applyOspfOptimization()}
|
onClick={() => void applyOspfOptimization()}
|
||||||
disabled={!can("mm:network:write") || loading || Boolean(error) || !data || !ospfServerId || ospfApplying || !previewReady || Boolean(ospfMetaError) || !ospfPreview?.changedCount}
|
disabled={!can("mm:network:write") || loading || Boolean(error) || !data || !ospfEligible || ospfApplying || !previewReady || Boolean(ospfMetaError) || !ospfPreview?.changedCount}
|
||||||
>
|
>
|
||||||
<ZapIcon aria-hidden="true" className={cn("size-3.5", ospfApplying && "animate-pulse")} />
|
<ZapIcon aria-hidden="true" className={cn("size-3.5", ospfApplying && "animate-pulse")} />
|
||||||
{ospfApplying ? "Оптимизация…" : "Оптимизировать OSPF"}
|
{ospfApplying ? "Оптимизация…" : "Оптимизировать OSPF"}
|
||||||
@@ -876,7 +890,7 @@ function RouteOptimizerContent() {
|
|||||||
<div className="rounded-lg border bg-background/70 overflow-hidden">
|
<div className="rounded-lg border bg-background/70 overflow-hidden">
|
||||||
<div className="px-3 py-2 border-b text-[11px] text-muted-foreground flex flex-wrap gap-2 items-center justify-between">
|
<div className="px-3 py-2 border-b text-[11px] text-muted-foreground flex flex-wrap gap-2 items-center justify-between">
|
||||||
<span>Preview изменений OSPF cost (до применения)</span>
|
<span>Preview изменений OSPF cost (до применения)</span>
|
||||||
<Button type="button" variant="outline" size="sm" disabled={ospfApplying || ospfPreviewLoading} onClick={()=>setPreviewTick(value=>value+1)}>Обновить preview OSPF</Button>
|
<Button type="button" variant="outline" size="sm" disabled={!ospfEligible || ospfApplying || ospfPreviewLoading} onClick={()=>setPreviewTick(value=>value+1)}>Обновить preview OSPF</Button>
|
||||||
<span>
|
<span>
|
||||||
{ospfPreviewLoading
|
{ospfPreviewLoading
|
||||||
? "расчёт…"
|
? "расчёт…"
|
||||||
|
|||||||
@@ -26,11 +26,18 @@
|
|||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.35; отсутствующий индекс не подменяется нулём, далее URL/source-контекст |
|
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.36; URL устройства OSPF сохраняется, далее source/provenance |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
## Блок 10.36 — URL устройства оптимизатора (2026-09-30)
|
||||||
|
|
||||||
|
- Параметр server восстанавливает OSPF устройство, select обновляет историю, другие параметры/fragment сохраняются. Suspense обеспечивает production render; URL меняет контекст без сброса локальной формы, источник по-прежнему изолирован key(mode, backendUrl).
|
||||||
|
- Unknown/disabled/malformed/empty explicit server не заменяется первым доступным: доступное имя отсутствует, alert предлагает выбор; preview/metadata запросы и применение заблокированы до явного выбора. Late preview проверен прежним race-тестом.
|
||||||
|
- 22 E2E preview/apply (6 новых URL сценариев), 3 unit, TS/lint и production frontend прошли. Новый unit fixture из 10.35 уточнён site/model/type после TypeScript проверки. Прежний teardown catalog ECONNREFUSED остаётся известным ограничением теста readonly.
|
||||||
|
- Далее: source isolation отдельным config, model provenance/fallback отдельно; затем карта/терминал. Вкладки Home/настройки пока не входят в URL. Реальные RouterOS/БД/JWT операции не проверены.
|
||||||
|
|
||||||
## Блок 10.35 — неизвестные индексы Community (2026-09-30)
|
## Блок 10.35 — неизвестные индексы Community (2026-09-30)
|
||||||
|
|
||||||
- Если полной цепочки нет, оба индекса показывают «—» с доступным названием; отсутствующая база не участвует в вычислении отображаемого выигрыша. Переключение не рекомендуется даже при нулевом пороге. Формула ранжирования полных цепочек не менялась.
|
- Если полной цепочки нет, оба индекса показывают «—» с доступным названием; отсутствующая база не участвует в вычислении отображаемого выигрыша. Переключение не рекомендуется даже при нулевом пороге. Формула ранжирования полных цепочек не менялась.
|
||||||
|
|||||||
@@ -192,3 +192,7 @@ Observed selector вместо default cost/disabled; неоднозначные
|
|||||||
## 10.35. Отсутствующие индексы Community
|
## 10.35. Отсутствующие индексы Community
|
||||||
|
|
||||||
Без полной цепочки индексы неизвестны, рекомендации нет даже при пороге 0. 7 E2E, 3 unit, TS/lint прошли; расчёт существующих цепочек прежний. Далее URL/source-контекст.
|
Без полной цепочки индексы неизвестны, рекомендации нет даже при пороге 0. 7 E2E, 3 unit, TS/lint прошли; расчёт существующих цепочек прежний. Далее URL/source-контекст.
|
||||||
|
|
||||||
|
## 10.36. URL устройства оптимизатора
|
||||||
|
|
||||||
|
server сохраняет выбор/reload/history без потери других параметров. Неизвестный/disabled/malformed explicit контекст не подменяется и не запускает preview/metadata; явный select восстанавливает работу. 22 E2E, 3 unit, TS/lint и production frontend прошли. Далее source/provenance; Home tabs URL пока вне блока.
|
||||||
@@ -19,7 +19,7 @@ test("optimizer refuses unknown types, duplicate inventory/WAN and invalid metri
|
|||||||
|
|
||||||
import {buildLiveOptimizerData,DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS} from "./route-optimizer-data"
|
import {buildLiveOptimizerData,DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS} from "./route-optimizer-data"
|
||||||
test("optimizer does not recommend an absent chain at a zero threshold",()=>{
|
test("optimizer does not recommend an absent chain at a zero threshold",()=>{
|
||||||
const rows=[{...server,wanUplinks:[{id:"wan",name:"WAN",isp:"ISP",iface:"ether1",ip:"198.51.100.1",maxDl:100,maxUl:100}]}]
|
const rows=[{...server,type:"home-router" as const,site:"TEST",model:"CHR",wanUplinks:[{id:"wan",name:"WAN",isp:"ISP",iface:"ether1",ip:"198.51.100.1",maxDl:100,maxUl:100}]}]
|
||||||
const rules=optimizerRulesSchema.parse({rulesets:[{serverId:"1",rules:[{id:"r",community:"65001:1",communityName:"Service",gateway:"192.0.2.2",gatewayTunnelId:"",description:""}]}]}).rulesets
|
const rules=optimizerRulesSchema.parse({rulesets:[{serverId:"1",rules:[{id:"r",community:"65001:1",communityName:"Service",gateway:"192.0.2.2",gatewayTunnelId:"",description:""}]}]}).rulesets
|
||||||
const result=buildLiveOptimizerData(rows,rules,{...DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS,switchThreshold:0})
|
const result=buildLiveOptimizerData(rows,rules,{...DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS,switchThreshold:0})
|
||||||
const recommendation=result.homes[0].commRecs[0]
|
const recommendation=result.homes[0].commRecs[0]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const iface=(name="ether1")=>({id:"i",interface:name,currentCost:10,optimalCost:
|
|||||||
const preview=(id=1,weight=60,name="ether1")=>({serverId:id,serverName:"Роутер "+id,pingWeight:weight,interfacesTotal:1,changedCount:1,unchangedCount:0,interfaces:[iface(name)],changes:[iface(name)]})
|
const preview=(id=1,weight=60,name="ether1")=>({serverId:id,serverName:"Роутер "+id,pingWeight:weight,interfacesTotal:1,changedCount:1,unchangedCount:0,interfaces:[iface(name)],changes:[iface(name)]})
|
||||||
async function base(page:Page){
|
async function base(page:Page){
|
||||||
await page.route("**/health",r=>r.fulfill({json:{ok:true}}))
|
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/servers"?[1,2].map(id=>({id,name:"Роутер "+id,host:"192.0.2."+id,type:"home-router",country:"RU",enabled:true,status:null})):p==="/api/auth/config"?{required:false}:p==="/api/filters/rules"?{rulesets:[]}:p==="/api/uptime/speed-probes"?{probes:[]}:p==="/api/ospf/catalog"?{interfaces:[],instances:[],neighbors:[],bfdSessions:[],routes:[],failures:[]}:preview(Number(p.split("/")[3]),r.request().postDataJSON()?.pingWeight??60)})})
|
await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;return r.fulfill({json:p==="/api/servers"?[1,2,3].map(id=>({id,name:"Роутер "+id,host:"192.0.2."+id,type:"home-router",country:"RU",enabled:id!==3,status:null})):p==="/api/auth/config"?{required:false}:p==="/api/filters/rules"?{rulesets:[]}:p==="/api/uptime/speed-probes"?{probes:[]}:p==="/api/ospf/catalog"?{interfaces:[],instances:[],neighbors:[],bfdSessions:[],routes:[],failures:[]}:preview(Number(p.split("/")[3]),r.request().postDataJSON()?.pingWeight??60)})})
|
||||||
}
|
}
|
||||||
for(const failure of ["http","foreign","weight","count","missing","duplicate"] as const)test("Оптимизатор: OSPF preview блокирует "+failure,async({page})=>{
|
for(const failure of ["http","foreign","weight","count","missing","duplicate"] as const)test("Оптимизатор: OSPF preview блокирует "+failure,async({page})=>{
|
||||||
await base(page);let failing=true
|
await base(page);let failing=true
|
||||||
@@ -37,3 +37,29 @@ test("Оптимизатор: поздний preview прежнего устро
|
|||||||
await page.evaluate(()=>new Promise<void>(resolve=>requestAnimationFrame(()=>requestAnimationFrame(()=>resolve()))))
|
await page.evaluate(()=>new Promise<void>(resolve=>requestAnimationFrame(()=>requestAnimationFrame(()=>resolve()))))
|
||||||
await expect(grid).toContainText("ether2");await expect(grid).not.toContainText("ether1")
|
await expect(grid).toContainText("ether2");await expect(grid).not.toContainText("ether1")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Оптимизатор: URL устройства, reload и история",async({page})=>{
|
||||||
|
await base(page)
|
||||||
|
await page.goto("/route-optimizer?server=2&keep=value")
|
||||||
|
const select=page.getByRole("combobox",{name:"Устройство OSPF",exact:true})
|
||||||
|
await expect(select).toHaveValue("2")
|
||||||
|
await expect(page.getByText("0 iface · 0 area",{exact:true})).toBeVisible()
|
||||||
|
await select.selectOption("1");await expect(page).toHaveURL(/server=1&keep=value/)
|
||||||
|
await expect(select).toHaveValue("1");await page.reload();await expect(select).toHaveValue("1")
|
||||||
|
await expect(page.getByText("0 iface · 0 area",{exact:true})).toBeVisible()
|
||||||
|
await page.goBack();await expect(select).toHaveValue("2")
|
||||||
|
await expect(page.getByText("0 iface · 0 area",{exact:true})).toBeVisible()
|
||||||
|
})
|
||||||
|
for(const server of ["999","3","abc","01",""])test("Оптимизатор: недоступный URL не выбирает другой сервер "+server,async({page})=>{
|
||||||
|
await base(page);let reads=0
|
||||||
|
await page.route("**/ospf/optimize/preview",r=>{reads++;return r.fulfill({json:preview(Number(new URL(r.request().url()).pathname.split("/")[3]))})})
|
||||||
|
await page.goto("/route-optimizer?server="+server)
|
||||||
|
await expect(page.getByRole("alert").filter({hasText:"Устройство из ссылки"})).toBeVisible()
|
||||||
|
await expect(page.getByRole("combobox",{name:"Устройство OSPF",exact:true})).toHaveValue(server)
|
||||||
|
await expect(page.getByRole("button",{name:"Оптимизировать OSPF",exact:true})).toBeDisabled()
|
||||||
|
await expect(page.getByRole("button",{name:"Обновить preview OSPF",exact:true})).toBeDisabled()
|
||||||
|
expect(reads).toBe(0)
|
||||||
|
await page.getByRole("combobox",{name:"Устройство OSPF",exact:true}).selectOption("2")
|
||||||
|
await expect(page.getByText("0 iface · 0 area",{exact:true})).toBeVisible()
|
||||||
|
await expect(page.getByRole("button",{name:"Оптимизировать OSPF",exact:true})).toBeEnabled()
|
||||||
|
})
|
||||||
Reference in new issue
Block a user