fix(ui): сохранять устройство диагностики в URL
This commit is contained in:
1 parent
1b91ba4e1d
commit
d0c715e93c
7 files changed
+89
-28
No files matched your search
+22
-11
@@ -1,6 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useRef, useState, useMemo, useCallback } from "react"
|
import { Suspense, useEffect, useRef, useState, useMemo, useCallback } from "react"
|
||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
|
import { readProbeUrl, patchProbeUrl } from "@/lib/probe-url"
|
||||||
import { PageHeader } from "@/components/page-header"
|
import { PageHeader } from "@/components/page-header"
|
||||||
import { FormToggle } from "@/components/form-kit"
|
import { FormToggle } from "@/components/form-kit"
|
||||||
import { Frame, FramePanel } from "@/components/reui/frame"
|
import { Frame, FramePanel } from "@/components/reui/frame"
|
||||||
@@ -567,12 +569,23 @@ function ScheduleTab({
|
|||||||
// ─── page ─────────────────────────────────────────────────────────────────────
|
// ─── page ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function ProbesPage() {
|
export default function ProbesPage() {
|
||||||
|
return <Suspense fallback={<p role="status">Загрузка диагностики…</p>}><ProbesRoute /></Suspense>
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProbesRoute() {
|
||||||
|
const params = useSearchParams()
|
||||||
|
const url = readProbeUrl(new URLSearchParams(params.toString()))
|
||||||
const { mode, backendUrl, prefsHydrated } = useDataSource()
|
const { mode, backendUrl, prefsHydrated } = useDataSource()
|
||||||
|
if (url.error) return <div role="alert" aria-label="Ссылка диагностики" className="p-4 space-y-2"><p>{url.error}</p><Button type="button" variant="outline" onClick={() => window.history.replaceState(null, "", patchProbeUrl(new URLSearchParams(params.toString()), null) + window.location.hash)}>Сбросить контекст диагностики</Button></div>
|
||||||
if (!prefsHydrated) return <p role="status">Загрузка настроек диагностики…</p>
|
if (!prefsHydrated) return <p role="status">Загрузка настроек диагностики…</p>
|
||||||
return <ProbesContent key={JSON.stringify([mode, backendUrl])} />
|
return <ProbesContent key={JSON.stringify([mode, backendUrl])} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProbesContent() {
|
function ProbesContent() {
|
||||||
|
const params = useSearchParams()
|
||||||
|
const { server } = readProbeUrl(new URLSearchParams(params.toString()))
|
||||||
|
const srcId = server ?? ""
|
||||||
|
const selectServer = (id: string) => window.history.pushState(null, "", patchProbeUrl(new URLSearchParams(params.toString()), id || null) + window.location.hash)
|
||||||
const { mode, backendUrl } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const canRead = can("mm:servers:read")
|
const canRead = can("mm:servers:read")
|
||||||
const canRun = can("mm:servers:write")
|
const canRun = can("mm:servers:write")
|
||||||
@@ -590,7 +603,6 @@ function ProbesContent() {
|
|||||||
const [greByServer, setGreByServer] = useState<Record<string, GreTunnel[]>>({})
|
const [greByServer, setGreByServer] = useState<Record<string, GreTunnel[]>>({})
|
||||||
|
|
||||||
const [tool, setTool] = useState<DiagTool>("ping")
|
const [tool, setTool] = useState<DiagTool>("ping")
|
||||||
const [srcId, setSrcId] = useState(servers[0]?.id ?? "srv1")
|
|
||||||
const [target, setTarget] = useState("8.8.8.8")
|
const [target, setTarget] = useState("8.8.8.8")
|
||||||
const [pingCount, setPingCount] = useState(5)
|
const [pingCount, setPingCount] = useState(5)
|
||||||
const [pingSize, setPingSize] = useState(64)
|
const [pingSize, setPingSize] = useState(64)
|
||||||
@@ -653,8 +665,8 @@ function ProbesContent() {
|
|||||||
), [allServers])
|
), [allServers])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLive) {
|
if (!isLive || !canRead || liveLoad !== "idle" || !allServers.some(s => s.id === srcId && s.enabled)) {
|
||||||
setRosSrcV4(undefined)
|
queueMicrotask(() => setRosSrcV4(undefined))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const n = Number.parseInt(srcId, 10)
|
const n = Number.parseInt(srcId, 10)
|
||||||
@@ -672,7 +684,7 @@ function ProbesContent() {
|
|||||||
if (!cancelled) setRosSrcV4(null)
|
if (!cancelled) setRosSrcV4(null)
|
||||||
})
|
})
|
||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [isLive, srcId, apiFetch])
|
}, [isLive, canRead, liveLoad, allServers, srcId, apiFetch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLive || !canReadGre || liveLoad !== "idle" || !allServers.some(s => s.id === srcId && s.enabled)) return
|
if (!isLive || !canReadGre || liveLoad !== "idle" || !allServers.some(s => s.id === srcId && s.enabled)) return
|
||||||
@@ -699,11 +711,6 @@ function ProbesContent() {
|
|||||||
return greTunnels.filter(t => t.serverId === srcId)
|
return greTunnels.filter(t => t.serverId === srcId)
|
||||||
}, [isLive, greByServer, srcId])
|
}, [isLive, greByServer, srcId])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const first = allServers[0]?.id
|
|
||||||
if (!first) return
|
|
||||||
if (!allServers.some(s => s.id === srcId)) setSrcId(first)
|
|
||||||
}, [allServers, srcId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const list = isLive ? (greByServer[srcId] ?? []) : greTunnels.filter(t => t.serverId === srcId)
|
const list = isLive ? (greByServer[srcId] ?? []) : greTunnels.filter(t => t.serverId === srcId)
|
||||||
@@ -939,7 +946,8 @@ function ProbesContent() {
|
|||||||
<ServerRailLayout
|
<ServerRailLayout
|
||||||
items={probeRailItems}
|
items={probeRailItems}
|
||||||
selectedId={srcId}
|
selectedId={srcId}
|
||||||
onSelect={setSrcId}
|
onSelect={selectServer}
|
||||||
|
preserveSelection
|
||||||
showAll={false}
|
showAll={false}
|
||||||
loading={isLive && liveLoad === "loading" && allServers.length === 0}
|
loading={isLive && liveLoad === "loading" && allServers.length === 0}
|
||||||
header={
|
header={
|
||||||
@@ -963,6 +971,9 @@ function ProbesContent() {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
|
||||||
|
{!server && <p role="status">Выберите устройство для диагностики. Выбор сохраняется в URL.</p>}
|
||||||
|
{server && (!isLive || catalogReadAt !== null) && !srcServer && <div role="alert" aria-label="Устройство диагностики"><p>Указанное устройство отсутствует в текущем каталоге. Другое устройство автоматически не выбирается.</p><Button type="button" variant="outline" onClick={() => selectServer("")}>Сбросить выбор устройства</Button></div>}
|
||||||
|
{srcServer && !srcServer.enabled && <p role="status">Выбранное устройство выключено. Диагностика недоступна.</p>}
|
||||||
{isLive && liveLoad === "error" && (
|
{isLive && liveLoad === "error" && (
|
||||||
<div role="alert" aria-label="Каталог диагностики" className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-2.5 text-sm text-destructive">
|
<div role="alert" aria-label="Каталог диагностики" className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-2.5 text-sm text-destructive">
|
||||||
<p>{catalogReadAt === null ? "Устройства пока неизвестны." : "Не удалось обновить устройства. Последний список сохранён; запуск приостановлен."}</p>
|
<p>{catalogReadAt === null ? "Устройства пока неизвестны." : "Не удалось обновить устройства. Последний список сохранён; запуск приостановлен."}</p>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
| 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.62; оптимизатор, карта и терминал приняты локально; диагностика /probes в работе |
|
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.63; оптимизатор, карта и терминал приняты локально; диагностика /probes в работе |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
@@ -36,6 +36,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Блок 10.63 — устройство диагностики из URL (2026-10-01)
|
||||||
|
|
||||||
|
- /probes читает server через useSearchParams/Suspense. Выбор явно сохраняется pushState; остальные query/hash, back и reload сохранены. Нет автоматического выбора первого устройства в live/демо; ServerRail preserveSelection удерживает исходный контекст.
|
||||||
|
- Повторный/пустой/некорректный server останавливает API до монтирования содержимого. Отсутствующее устройство показывает отдельную ошибку/сброс выбора, выключенное — причину недоступности. Без выбора запуск и зависимые запросы не выполняются. GET ros-src-address также ограничен подтверждённым enabled устройством.
|
||||||
|
- 29 основных + 4 source E2E, 2 URL unit, профильный lint, TypeScript и production frontend прошли. Старые тесты выбирают server=1 явно; при переходе в demo выбирают реальное demo устройство вместо прежнего fallback.
|
||||||
|
- Далее 10.64: строгий ros-src-address контракт и привязка preview к id/host с отменой/повтором; затем backend probes DI/аргументы/ошибки/метрики, доступность/адаптивная приёмка. Этап 10 остаётся в работе. Моки не доказывают реальные RouterOS/DNS/БД.
|
||||||
|
|
||||||
## Блок 10.62 — честный DNS preview и inline ошибка (2026-10-01)
|
## Блок 10.62 — честный DNS preview и inline ошибка (2026-10-01)
|
||||||
|
|
||||||
- DNS в /probes подписан как DNS приложения; preview описывает параметры резолвера хоста backend. Не обещает /resolve MikroTik или server=1.1.1.1, которых текущий dnsLookupText не использует. Демонстрационный источник отмечен отдельно.
|
- DNS в /probes подписан как DNS приложения; preview описывает параметры резолвера хоста backend. Не обещает /resolve MikroTik или server=1.1.1.1, которых текущий dnsLookupText не использует. Демонстрационный источник отмечен отдельно.
|
||||||
|
|||||||
@@ -381,3 +381,11 @@ Shared контракт и backend observed исходные значения/д
|
|||||||
- Остановка при 98% использования пятичасового окна; все блоки сохранены локальными коммитами без push. Следующий шаг 10.63: восстановление /probes server из URL без первого-device fallback, сохранение query/hash/back/reload, контракт/cleanup ros-src-address. Затем backend probes DI (невалидные id/аргументы, HTTP ошибки вместо legacy output, DNS/MTU/неизвестные метрики), доступность/адаптивные снимки и приёмка.
|
- Остановка при 98% использования пятичасового окна; все блоки сохранены локальными коммитами без push. Следующий шаг 10.63: восстановление /probes server из URL без первого-device fallback, сохранение query/hash/back/reload, контракт/cleanup ros-src-address. Затем backend probes DI (невалидные id/аргументы, HTTP ошибки вместо legacy output, DNS/MTU/неизвестные метрики), доступность/адаптивные снимки и приёмка.
|
||||||
- Этап 10 ещё в работе; /probes не принят. Остались 6 прежних set-state-in-effect ошибок и 2 предупреждения страницы, включая расписание (11Б). История локальная; HTTP моки не подтверждают реальные RouterOS/БД/DNS/портал.
|
- Этап 10 ещё в работе; /probes не принят. Остались 6 прежних set-state-in-effect ошибок и 2 предупреждения страницы, включая расписание (11Б). История локальная; HTTP моки не подтверждают реальные RouterOS/БД/DNS/портал.
|
||||||
|
|
||||||
|
|
||||||
|
## Блок 10.63 — устройство диагностики из URL (2026-10-01)
|
||||||
|
|
||||||
|
- /probes читает server через useSearchParams/Suspense. Выбор явно сохраняется pushState; остальные query/hash, back и reload сохранены. Нет автоматического выбора первого устройства в live/демо; ServerRail preserveSelection удерживает исходный контекст.
|
||||||
|
- Повторный/пустой/некорректный server останавливает API до монтирования содержимого. Отсутствующее устройство показывает отдельную ошибку/сброс выбора, выключенное — причину недоступности. Без выбора запуск и зависимые запросы не выполняются. GET ros-src-address также ограничен подтверждённым enabled устройством.
|
||||||
|
- 29 основных + 4 source E2E, 2 URL unit, профильный lint, TypeScript и production frontend прошли. Старые тесты выбирают server=1 явно; при переходе в demo выбирают реальное demo устройство вместо прежнего fallback.
|
||||||
|
- Далее 10.64: строгий ros-src-address контракт и привязка preview к id/host с отменой/повтором; затем backend probes DI/аргументы/ошибки/метрики, доступность/адаптивная приёмка. Этап 10 остаётся в работе. Моки не доказывают реальные RouterOS/DNS/БД.
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { test } from "node:test"
|
||||||
|
import assert from "node:assert/strict"
|
||||||
|
import {readProbeUrl,patchProbeUrl} from "./probe-url"
|
||||||
|
test("URL диагностики отклоняет повтор/пустой/невалидный id",()=>{for(const q of ["server=","server=bad%20id","server=1&server=2"])assert.ok(readProbeUrl(new URLSearchParams(q)).error);assert.equal(readProbeUrl(new URLSearchParams()).server,null);assert.equal(readProbeUrl(new URLSearchParams("server=srv1")).server,"srv1")})
|
||||||
|
test("Выбор изменяет только server и сохраняет остальные параметры",()=>{assert.equal(patchProbeUrl(new URLSearchParams("server=2&keep=1"),"1"),"/probes?keep=1&server=1");assert.equal(patchProbeUrl(new URLSearchParams("server=2&keep=1"),null),"/probes?keep=1")})
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export function readProbeUrl(params: URLSearchParams) {
|
||||||
|
const server=params.get("server")
|
||||||
|
const error=params.getAll("server").length>1?"В ссылке повторяется устройство диагностики."
|
||||||
|
:server!==null&&!/^[a-zA-Z0-9_.:-]{1,64}$/.test(server)?"В ссылке указан некорректный сервер диагностики.":null
|
||||||
|
return {server,error}
|
||||||
|
}
|
||||||
|
export function patchProbeUrl(params:URLSearchParams,server:string|null){
|
||||||
|
const next=new URLSearchParams(params);next.delete("server");if(server)next.set("server",server)
|
||||||
|
return "/probes"+(next.size?"?"+next.toString():"")
|
||||||
|
}
|
||||||
@@ -4,11 +4,11 @@ for(const change of ["backend","mode"])for(const fails of [false,true])test("Д
|
|||||||
let started=false,completed=false,release!:()=>void;const gate=new Promise<void>(r=>release=r)
|
let started=false,completed=false,release!:()=>void;const gate=new Promise<void>(r=>release=r)
|
||||||
await page.route("**/health",r=>r.fulfill({headers,json:{ok:true}}))
|
await page.route("**/health",r=>r.fulfill({headers,json:{ok:true}}))
|
||||||
await page.route("**/api/**",r=>{const u=new URL(r.request().url()),p=u.pathname;return r.fulfill({headers,json:p==="/api/auth/config"?{required:false}:p==="/api/servers"?[{id:1,name:u.port==="8101"?"Новый источник":"Прежний источник",host:"192.0.2.1",type:"exit-node",country:"RU",enabled:true,status:"online"}]:p.endsWith("/ros-src-address")?{ipv4:"192.0.2.1"}:p.endsWith("/probes/run")?{output:"НОВЫЙ РЕЗУЛЬТАТ"}:{tunnels:[]}})})
|
await page.route("**/api/**",r=>{const u=new URL(r.request().url()),p=u.pathname;return r.fulfill({headers,json:p==="/api/auth/config"?{required:false}:p==="/api/servers"?[{id:1,name:u.port==="8101"?"Новый источник":"Прежний источник",host:"192.0.2.1",type:"exit-node",country:"RU",enabled:true,status:"online"}]:p.endsWith("/ros-src-address")?{ipv4:"192.0.2.1"}:p.endsWith("/probes/run")?{output:"НОВЫЙ РЕЗУЛЬТАТ"}:{tunnels:[]}})})
|
||||||
await page.goto("/probes");await expect(page.getByRole("button",{name:"Запустить",exact:true})).toBeEnabled()
|
await page.goto("/probes?server=1");await expect(page.getByRole("button",{name:"Запустить",exact:true})).toBeEnabled()
|
||||||
await page.route("**/api/servers/1/probes/run",async r=>{if(new URL(r.request().url()).port!=="8000")return r.fallback();started=true;await gate;try{await r.fulfill(fails?{headers,status:503,json:{error:"СТАРАЯ ОШИБКА"}}:{headers,json:{output:"СТАРЫЙ РЕЗУЛЬТАТ"}})}catch{}finally{completed=true}})
|
await page.route("**/api/servers/1/probes/run",async r=>{if(new URL(r.request().url()).port!=="8000")return r.fallback();started=true;await gate;try{await r.fulfill(fails?{headers,status:503,json:{error:"СТАРАЯ ОШИБКА"}}:{headers,json:{output:"СТАРЫЙ РЕЗУЛЬТАТ"}})}catch{}finally{completed=true}})
|
||||||
await page.getByRole("button",{name:"Запустить",exact:true}).click();await expect.poll(()=>started).toBe(true);await page.getByRole("link",{name:/^Настройки/}).click()
|
await page.getByRole("button",{name:"Запустить",exact:true}).click();await expect.poll(()=>started).toBe(true);await page.getByRole("link",{name:/^Настройки/}).click()
|
||||||
if(change==="backend"){const input=page.getByPlaceholder("http://localhost:8000");await input.fill("http://localhost:8101");await input.press("Enter")}else await page.getByRole("button",{name:"Моковые",exact:true}).click()
|
if(change==="backend"){const input=page.getByPlaceholder("http://localhost:8000");await input.fill("http://localhost:8101");await input.press("Enter")}else await page.getByRole("button",{name:"Моковые",exact:true}).click()
|
||||||
await page.goto("/probes");await expect(page.getByRole("button",{name:"Запустить",exact:true})).toBeEnabled();release();await expect.poll(()=>completed).toBe(true)
|
await page.goto("/probes?server=1");if(change==="mode")await page.getByRole("option").first().click();await expect(page.getByRole("button",{name:"Запустить",exact:true})).toBeEnabled();release();await expect.poll(()=>completed).toBe(true)
|
||||||
await page.getByRole("button",{name:"Запустить",exact:true}).click()
|
await page.getByRole("button",{name:"Запустить",exact:true}).click()
|
||||||
if(change==="backend")await expect(page.getByText("НОВЫЙ РЕЗУЛЬТАТ",{exact:true})).toBeVisible()
|
if(change==="backend")await expect(page.getByText("НОВЫЙ РЕЗУЛЬТАТ",{exact:true})).toBeVisible()
|
||||||
await expect(page.getByText("СТАРЫЙ РЕЗУЛЬТАТ",{exact:true})).toHaveCount(0);await expect(page.getByText("СТАРАЯ ОШИБКА",{exact:true})).toHaveCount(0)
|
await expect(page.getByText("СТАРЫЙ РЕЗУЛЬТАТ",{exact:true})).toHaveCount(0);await expect(page.getByText("СТАРАЯ ОШИБКА",{exact:true})).toHaveCount(0)
|
||||||
|
|||||||
@@ -8,40 +8,40 @@ test.beforeEach(async({page})=>routes(page))
|
|||||||
const run=(page:Page)=>page.getByRole("button",{name:"Запустить",exact:true})
|
const run=(page:Page)=>page.getByRole("button",{name:"Запустить",exact:true})
|
||||||
for(const [name,data,status] of [["malformed",{},200],["legacy",{output:" error: RouterOS недоступен"},200],["API",{error:"Нет доступа к диагностике"},403]] as const)test("Диагностика: "+name+" сохраняет цель и допускает повтор",async({page})=>{
|
for(const [name,data,status] of [["malformed",{},200],["legacy",{output:" error: RouterOS недоступен"},200],["API",{error:"Нет доступа к диагностике"},403]] as const)test("Диагностика: "+name+" сохраняет цель и допускает повтор",async({page})=>{
|
||||||
let fail=true;await page.route("**/api/servers/1/probes/run",r=>r.fulfill(fail?{json:data,status}:{json:{output:"PING подтверждён"}}))
|
let fail=true;await page.route("**/api/servers/1/probes/run",r=>r.fulfill(fail?{json:data,status}:{json:{output:"PING подтверждён"}}))
|
||||||
await page.goto("/probes");await expect(run(page)).toBeEnabled();const target=page.getByRole("textbox",{name:"Цель диагностики"});await target.fill("198.51.100.9");await run(page).click()
|
await page.goto("/probes?server=1");await expect(run(page)).toBeEnabled();const target=page.getByRole("textbox",{name:"Цель диагностики"});await target.fill("198.51.100.9");await run(page).click()
|
||||||
await expect(page.getByText("ошибка",{exact:true})).toBeVisible();await expect(target).toHaveValue("198.51.100.9");await expect(run(page)).toBeEnabled()
|
await expect(page.getByText("ошибка",{exact:true})).toBeVisible();await expect(target).toHaveValue("198.51.100.9");await expect(run(page)).toBeEnabled()
|
||||||
fail=false;await run(page).click();await expect(page.getByText("PING подтверждён",{exact:true})).toBeVisible()
|
fail=false;await run(page).click();await expect(page.getByText("PING подтверждён",{exact:true})).toBeVisible()
|
||||||
})
|
})
|
||||||
test("Диагностика: пустой output явно подтверждён",async({page})=>{
|
test("Диагностика: пустой output явно подтверждён",async({page})=>{
|
||||||
await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:""}}));await page.goto("/probes");await run(page).click();await expect(page.getByText("Запрос выполнен: API вернул пустой вывод.",{exact:true})).toBeVisible()
|
await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:""}}));await page.goto("/probes?server=1");await run(page).click();await expect(page.getByText("Запрос выполнен: API вернул пустой вывод.",{exact:true})).toBeVisible()
|
||||||
})
|
})
|
||||||
test("Диагностика: повторный Enter, Стоп и поздний успех",async({page})=>{
|
test("Диагностика: повторный Enter, Стоп и поздний успех",async({page})=>{
|
||||||
let calls=0,release!:()=>void;const gate=new Promise<void>(r=>release=r)
|
let calls=0,release!:()=>void;const gate=new Promise<void>(r=>release=r)
|
||||||
await page.route("**/api/servers/1/probes/run",async r=>{calls++;await gate;await r.fulfill({json:{output:"ЗАПОЗДАЛЫЙ УСПЕХ"}}).catch(()=>{})})
|
await page.route("**/api/servers/1/probes/run",async r=>{calls++;await gate;await r.fulfill({json:{output:"ЗАПОЗДАЛЫЙ УСПЕХ"}}).catch(()=>{})})
|
||||||
await page.goto("/probes");const target=page.getByRole("textbox",{name:"Цель диагностики"});await target.press("Enter");await target.press("Enter");await expect.poll(()=>calls).toBe(1);await expect(run(page)).toBeDisabled()
|
await page.goto("/probes?server=1");const target=page.getByRole("textbox",{name:"Цель диагностики"});await target.press("Enter");await target.press("Enter");await expect.poll(()=>calls).toBe(1);await expect(run(page)).toBeDisabled()
|
||||||
await page.getByRole("button",{name:"Стоп",exact:true}).click();await expect(page.getByText("ожидание отменено",{exact:true})).toBeVisible();await expect(run(page)).toBeEnabled();release();await expect(page.getByText("ЗАПОЗДАЛЫЙ УСПЕХ",{exact:true})).toHaveCount(0)
|
await page.getByRole("button",{name:"Стоп",exact:true}).click();await expect(page.getByText("ожидание отменено",{exact:true})).toBeVisible();await expect(run(page)).toBeEnabled();release();await expect(page.getByText("ЗАПОЗДАЛЫЙ УСПЕХ",{exact:true})).toHaveCount(0)
|
||||||
await expect(page.getByText("Ожидание отменено. Остановка на устройстве не подтверждена.",{exact:true})).toBeVisible()
|
await expect(page.getByText("Ожидание отменено. Остановка на устройстве не подтверждена.",{exact:true})).toBeVisible()
|
||||||
})
|
})
|
||||||
test("Диагностика: таймаут сохраняет ввод, без автоматического повтора",async({page})=>{
|
test("Диагностика: таймаут сохраняет ввод, без автоматического повтора",async({page})=>{
|
||||||
await page.clock.install();let calls=0,release!:()=>void;const gate=new Promise<void>(r=>release=r)
|
await page.clock.install();let calls=0,release!:()=>void;const gate=new Promise<void>(r=>release=r)
|
||||||
await page.route("**/api/servers/1/probes/run",async r=>{calls++;await gate;await r.fulfill({json:{output:"ПОЗДНО"}}).catch(()=>{})})
|
await page.route("**/api/servers/1/probes/run",async r=>{calls++;await gate;await r.fulfill({json:{output:"ПОЗДНО"}}).catch(()=>{})})
|
||||||
await page.goto("/probes");await run(page).click();await expect.poll(()=>calls).toBe(1);await page.clock.fastForward(60_100)
|
await page.goto("/probes?server=1");await run(page).click();await expect.poll(()=>calls).toBe(1);await page.clock.fastForward(60_100)
|
||||||
await expect(page.getByText(/Время ожидания истекло/)).toBeVisible();await expect(run(page)).toBeEnabled();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("8.8.8.8");expect(calls).toBe(1);release()
|
await expect(page.getByText(/Время ожидания истекло/)).toBeVisible();await expect(run(page)).toBeEnabled();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("8.8.8.8");expect(calls).toBe(1);release()
|
||||||
})
|
})
|
||||||
test("Диагностика: уход со страницы не переносит поздний вывод",async({page})=>{
|
test("Диагностика: уход со страницы не переносит поздний вывод",async({page})=>{
|
||||||
let first=true,release!:()=>void;const gate=new Promise<void>(r=>release=r)
|
let first=true,release!:()=>void;const gate=new Promise<void>(r=>release=r)
|
||||||
await page.route("**/api/servers/1/probes/run",async r=>{if(first){first=false;await gate;await r.fulfill({json:{output:"СТАРЫЙ ВЫВОД"}}).catch(()=>{})}else await r.fulfill({json:{output:"НОВЫЙ ВЫВОД"}})})
|
await page.route("**/api/servers/1/probes/run",async r=>{if(first){first=false;await gate;await r.fulfill({json:{output:"СТАРЫЙ ВЫВОД"}}).catch(()=>{})}else await r.fulfill({json:{output:"НОВЫЙ ВЫВОД"}})})
|
||||||
await page.goto("/probes");await run(page).click();await expect(run(page)).toBeDisabled();await page.goto("/terminal");release();await page.goto("/probes");await run(page).click();await expect(page.getByText("НОВЫЙ ВЫВОД",{exact:true})).toBeVisible();await expect(page.getByText("СТАРЫЙ ВЫВОД",{exact:true})).toHaveCount(0)
|
await page.goto("/probes?server=1");await run(page).click();await expect(run(page)).toBeDisabled();await page.goto("/terminal");release();await page.goto("/probes?server=1");await run(page).click();await expect(page.getByText("НОВЫЙ ВЫВОД",{exact:true})).toBeVisible();await expect(page.getByText("СТАРЫЙ ВЫВОД",{exact:true})).toHaveCount(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
for(const malformed of [false,true])test("Диагностика: первоначальный отказ каталога и повтор "+malformed,async({page})=>{
|
for(const malformed of [false,true])test("Диагностика: первоначальный отказ каталога и повтор "+malformed,async({page})=>{
|
||||||
let fail=true;await page.route("**/api/servers",r=>r.fulfill(fail?(malformed?{json:[{...devices[0],enabled:"yes"}]}:{status:403,json:{error:"Каталог запрещён"}}):{json:devices}))
|
let fail=true;await page.route("**/api/servers",r=>r.fulfill(fail?(malformed?{json:[{...devices[0],enabled:"yes"}]}:{status:403,json:{error:"Каталог запрещён"}}):{json:devices}))
|
||||||
await page.goto("/probes");await expect(page.getByRole("alert",{name:"Каталог диагностики"})).toContainText("Устройства пока неизвестны");await expect(run(page)).toBeDisabled()
|
await page.goto("/probes?server=1");await expect(page.getByRole("alert",{name:"Каталог диагностики"})).toContainText("Устройства пока неизвестны");await expect(run(page)).toBeDisabled()
|
||||||
fail=false;await page.getByRole("button",{name:"Обновить устройства диагностики"}).click();await expect(run(page)).toBeEnabled()
|
fail=false;await page.getByRole("button",{name:"Обновить устройства диагностики"}).click();await expect(run(page)).toBeEnabled()
|
||||||
})
|
})
|
||||||
test("Диагностика: фоновая ошибка сохраняет список и цель до повтора",async({page})=>{
|
test("Диагностика: фоновая ошибка сохраняет список и цель до повтора",async({page})=>{
|
||||||
let fail=false;await page.route("**/api/servers",r=>r.fulfill(fail?{status:503,json:{error:"Каталог недоступен"}}:{json:devices}))
|
let fail=false;await page.route("**/api/servers",r=>r.fulfill(fail?{status:503,json:{error:"Каталог недоступен"}}:{json:devices}))
|
||||||
await page.goto("/probes");await expect(run(page)).toBeEnabled();await page.getByRole("textbox",{name:"Цель диагностики"}).fill("198.51.100.5");fail=true;await page.getByRole("button",{name:"Обновить устройства диагностики"}).click()
|
await page.goto("/probes?server=1");await expect(run(page)).toBeEnabled();await page.getByRole("textbox",{name:"Цель диагностики"}).fill("198.51.100.5");fail=true;await page.getByRole("button",{name:"Обновить устройства диагностики"}).click()
|
||||||
await expect(page.getByRole("alert",{name:"Каталог диагностики"})).toContainText("Последний список сохранён");await expect(page.getByRole("option",{name:/Первый/})).toBeVisible();await expect(run(page)).toBeDisabled();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("198.51.100.5")
|
await expect(page.getByRole("alert",{name:"Каталог диагностики"})).toContainText("Последний список сохранён");await expect(page.getByRole("option",{name:/Первый/})).toBeVisible();await expect(run(page)).toBeDisabled();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("198.51.100.5")
|
||||||
fail=false;await page.getByRole("button",{name:"Обновить устройства диагностики"}).click();await expect(run(page)).toBeEnabled();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("198.51.100.5")
|
fail=false;await page.getByRole("button",{name:"Обновить устройства диагностики"}).click();await expect(run(page)).toBeEnabled();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("198.51.100.5")
|
||||||
})
|
})
|
||||||
@@ -49,7 +49,7 @@ test("Диагностика: пустой каталог отличается
|
|||||||
await page.route("**/api/servers",r=>r.fulfill({json:[]}));await page.goto("/probes");await expect(page.getByRole("status").filter({hasText:"В каталоге нет устройств"})).toBeVisible();await expect(page.getByRole("alert",{name:"Каталог диагностики"})).toHaveCount(0);await expect(run(page)).toBeDisabled()
|
await page.route("**/api/servers",r=>r.fulfill({json:[]}));await page.goto("/probes");await expect(page.getByRole("status").filter({hasText:"В каталоге нет устройств"})).toBeVisible();await expect(page.getByRole("alert",{name:"Каталог диагностики"})).toHaveCount(0);await expect(run(page)).toBeDisabled()
|
||||||
})
|
})
|
||||||
test("Диагностика: неизвестный статус не становится offline",async({page})=>{
|
test("Диагностика: неизвестный статус не становится offline",async({page})=>{
|
||||||
await page.route("**/api/servers",r=>r.fulfill({json:[{...devices[0],status:null}]}));await page.goto("/probes");await expect(run(page)).toBeEnabled();await expect(page.getByRole("option",{name:/Первый/})).not.toHaveAccessibleName(/Не в сети|offline/i)
|
await page.route("**/api/servers",r=>r.fulfill({json:[{...devices[0],status:null}]}));await page.goto("/probes?server=1");await expect(run(page)).toBeEnabled();await expect(page.getByRole("option",{name:/Первый/})).not.toHaveAccessibleName(/Не в сети|offline/i)
|
||||||
})
|
})
|
||||||
|
|
||||||
for(const permission of ["mm:servers:read","mm:servers:write"])test("Диагностика: права запуска "+permission,async({page})=>{
|
for(const permission of ["mm:servers:read","mm:servers:write"])test("Диагностика: права запуска "+permission,async({page})=>{
|
||||||
@@ -57,7 +57,7 @@ for(const permission of ["mm:servers:read","mm:servers:write"])test("Диагн
|
|||||||
await page.route("**/api/auth/config",r=>r.fulfill({json:{required:true}}));let posts=0,greReads=0
|
await page.route("**/api/auth/config",r=>r.fulfill({json:{required:true}}));let posts=0,greReads=0
|
||||||
await page.route("**/api/filters/gre-tunnels?*",r=>{greReads++;return r.fulfill({json:{tunnels:[]}})})
|
await page.route("**/api/filters/gre-tunnels?*",r=>{greReads++;return r.fulfill({json:{tunnels:[]}})})
|
||||||
await page.route("**/api/servers/1/probes/run",r=>{posts++;return r.fulfill({json:{output:"РАЗРЕШЕНО"}})})
|
await page.route("**/api/servers/1/probes/run",r=>{posts++;return r.fulfill({json:{output:"РАЗРЕШЕНО"}})})
|
||||||
await page.goto("/probes")
|
await page.goto("/probes?server=1")
|
||||||
if(permission==="mm:servers:read"){await expect(page.getByRole("status").filter({hasText:"Для запуска диагностики требуется mm:servers:write"})).toBeVisible();await expect(run(page)).toBeDisabled();await page.getByRole("textbox",{name:"Цель диагностики"}).press("Enter");expect(posts).toBe(0)}
|
if(permission==="mm:servers:read"){await expect(page.getByRole("status").filter({hasText:"Для запуска диагностики требуется mm:servers:write"})).toBeVisible();await expect(run(page)).toBeDisabled();await page.getByRole("textbox",{name:"Цель диагностики"}).press("Enter");expect(posts).toBe(0)}
|
||||||
else{await expect(run(page)).toBeEnabled();await run(page).click();await expect(page.getByText("РАЗРЕШЕНО",{exact:true})).toBeVisible();expect(posts).toBe(1)}
|
else{await expect(run(page)).toBeEnabled();await run(page).click();await expect(page.getByText("РАЗРЕШЕНО",{exact:true})).toBeVisible();expect(posts).toBe(1)}
|
||||||
expect(greReads).toBe(0);await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"Для выбора GRE требуется mm:filters:read"})).toBeVisible();await expect(run(page)).toBeDisabled()
|
expect(greReads).toBe(0);await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"Для выбора GRE требуется mm:filters:read"})).toBeVisible();await expect(run(page)).toBeDisabled()
|
||||||
@@ -67,28 +67,48 @@ const tunnel={id:"*1",serverId:"1",name:"GRE-Тест",localAddress:"192.0.2.1",
|
|||||||
for(const kind of ["API","malformed","foreign"])test("Диагностика: GRE "+kind+" отличается от пустого списка",async({page})=>{
|
for(const kind of ["API","malformed","foreign"])test("Диагностика: GRE "+kind+" отличается от пустого списка",async({page})=>{
|
||||||
let fail=true;await page.route("**/api/filters/gre-tunnels?*",r=>r.fulfill(fail?(kind==="API"?{status:503,json:{error:"GRE отказ"}}:{json:{tunnels:kind==="foreign"?[{...tunnel,serverId:"2"}]:[{}]}}):{json:{tunnels:[tunnel]}}))
|
let fail=true;await page.route("**/api/filters/gre-tunnels?*",r=>r.fulfill(fail?(kind==="API"?{status:503,json:{error:"GRE отказ"}}:{json:{tunnels:kind==="foreign"?[{...tunnel,serverId:"2"}]:[{}]}}):{json:{tunnels:[tunnel]}}))
|
||||||
await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:"BW подтверждён"}}))
|
await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:"BW подтверждён"}}))
|
||||||
await page.goto("/probes");await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("alert",{name:"GRE диагностики"})).toBeVisible();await expect(run(page)).toBeDisabled()
|
await page.goto("/probes?server=1");await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("alert",{name:"GRE диагностики"})).toBeVisible();await expect(run(page)).toBeDisabled()
|
||||||
fail=false;await page.getByRole("button",{name:"Повторить GRE"}).click();await expect(run(page)).toBeEnabled();await run(page).click();await expect(page.getByText("BW подтверждён",{exact:true})).toBeVisible()
|
fail=false;await page.getByRole("button",{name:"Повторить GRE"}).click();await expect(run(page)).toBeEnabled();await run(page).click();await expect(page.getByText("BW подтверждён",{exact:true})).toBeVisible()
|
||||||
})
|
})
|
||||||
test("Диагностика: подтверждённый пустой GRE каталог",async({page})=>{
|
test("Диагностика: подтверждённый пустой GRE каталог",async({page})=>{
|
||||||
await page.goto("/probes");await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"У выбранного устройства нет GRE-туннелей"})).toBeVisible();await expect(page.getByRole("alert",{name:"GRE диагностики"})).toHaveCount(0);await expect(run(page)).toBeDisabled()
|
await page.goto("/probes?server=1");await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"У выбранного устройства нет GRE-туннелей"})).toBeVisible();await expect(page.getByRole("alert",{name:"GRE диагностики"})).toHaveCount(0);await expect(run(page)).toBeDisabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
for(const fails of [false,true])test("Диагностика: поздний GRE прежнего устройства "+fails,async({page})=>{
|
for(const fails of [false,true])test("Диагностика: поздний GRE прежнего устройства "+fails,async({page})=>{
|
||||||
let started=false,completed=false,release!:()=>void;const gate=new Promise<void>(r=>release=r)
|
let started=false,completed=false,release!:()=>void;const gate=new Promise<void>(r=>release=r)
|
||||||
await page.route("**/api/servers",r=>r.fulfill({json:[...devices,{...devices[0],id:2,name:"Второй",host:"192.0.2.2"}]}))
|
await page.route("**/api/servers",r=>r.fulfill({json:[...devices,{...devices[0],id:2,name:"Второй",host:"192.0.2.2"}]}))
|
||||||
await page.route("**/api/filters/gre-tunnels?*",async r=>{if(new URL(r.request().url()).searchParams.get("serverId")==="2")return r.fulfill({json:{tunnels:[]}});started=true;await gate;try{await r.fulfill(fails?{status:503,json:{error:"СТАРЫЙ GRE ОТКАЗ"}}:{json:{tunnels:[tunnel]}})}catch{}finally{completed=true}})
|
await page.route("**/api/filters/gre-tunnels?*",async r=>{if(new URL(r.request().url()).searchParams.get("serverId")==="2")return r.fulfill({json:{tunnels:[]}});started=true;await gate;try{await r.fulfill(fails?{status:503,json:{error:"СТАРЫЙ GRE ОТКАЗ"}}:{json:{tunnels:[tunnel]}})}catch{}finally{completed=true}})
|
||||||
await page.goto("/probes");await expect.poll(()=>started).toBe(true);await page.getByRole("option",{name:/Второй/}).click();await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"У выбранного устройства нет GRE-туннелей"})).toBeVisible();release();await expect.poll(()=>completed).toBe(true)
|
await page.goto("/probes?server=1");await expect.poll(()=>started).toBe(true);await page.getByRole("option",{name:/Второй/}).click();await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"У выбранного устройства нет GRE-туннелей"})).toBeVisible();release();await expect.poll(()=>completed).toBe(true)
|
||||||
await expect(page.getByRole("alert",{name:"GRE диагностики"})).toHaveCount(0);await expect(page.getByText("GRE-Тест",{exact:true})).toHaveCount(0);await expect(run(page)).toBeDisabled()
|
await expect(page.getByRole("alert",{name:"GRE диагностики"})).toHaveCount(0);await expect(page.getByText("GRE-Тест",{exact:true})).toHaveCount(0);await expect(run(page)).toBeDisabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
for(const invalid of ["disabled","no-address"])test("Диагностика: BW недоступен для GRE "+invalid,async({page})=>{
|
for(const invalid of ["disabled","no-address"])test("Диагностика: BW недоступен для GRE "+invalid,async({page})=>{
|
||||||
let posts=0;await page.route("**/api/filters/gre-tunnels?*",r=>r.fulfill({json:{tunnels:[{...tunnel,enabled:invalid!=="disabled",remoteAddress:invalid==="no-address"?"":tunnel.remoteAddress}]}}));await page.route("**/api/servers/1/probes/run",r=>{posts++;return r.fulfill({json:{output:"НЕ ДОЛЖНО ВЫПОЛНЯТЬСЯ"}})})
|
let posts=0;await page.route("**/api/filters/gre-tunnels?*",r=>r.fulfill({json:{tunnels:[{...tunnel,enabled:invalid!=="disabled",remoteAddress:invalid==="no-address"?"":tunnel.remoteAddress}]}}));await page.route("**/api/servers/1/probes/run",r=>{posts++;return r.fulfill({json:{output:"НЕ ДОЛЖНО ВЫПОЛНЯТЬСЯ"}})})
|
||||||
await page.goto("/probes");await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"Выберите включённый GRE с адресом назначения"})).toBeVisible();await expect(run(page)).toBeDisabled();expect(posts).toBe(0)
|
await page.goto("/probes?server=1");await page.getByRole("button",{name:/BW-тест/}).click();await expect(page.getByRole("status").filter({hasText:"Выберите включённый GRE с адресом назначения"})).toBeVisible();await expect(run(page)).toBeDisabled();expect(posts).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Диагностика: DNS приложения, inline ошибка и повтор",async({page})=>{
|
test("Диагностика: DNS приложения, inline ошибка и повтор",async({page})=>{
|
||||||
let fail=true;await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:fail?";; QUESTION: example.test A\n;; ERROR: ENOTFOUND":"example.test. IN A 192.0.2.1"}}))
|
let fail=true;await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:fail?";; QUESTION: example.test A\n;; ERROR: ENOTFOUND":"example.test. IN A 192.0.2.1"}}))
|
||||||
await page.goto("/probes");await page.getByRole("button",{name:"DNS",exact:true}).click();await page.getByRole("textbox",{name:"Домен"}).fill("example.test");await expect(page.getByText(/DNS приложения: example.test/)).toBeVisible();await expect(page.getByText(/server=1.1.1.1/)).toHaveCount(0)
|
await page.goto("/probes?server=1");await page.getByRole("button",{name:"DNS",exact:true}).click();await page.getByRole("textbox",{name:"Домен"}).fill("example.test");await expect(page.getByText(/DNS приложения: example.test/)).toBeVisible();await expect(page.getByText(/server=1.1.1.1/)).toHaveCount(0)
|
||||||
await run(page).click();await expect(page.getByText("ошибка",{exact:true})).toBeVisible();await expect(page.getByRole("textbox",{name:"Домен"})).toHaveValue("example.test");fail=false;await run(page).click();await expect(page.getByText("example.test. IN A 192.0.2.1",{exact:true})).toBeVisible()
|
await run(page).click();await expect(page.getByText("ошибка",{exact:true})).toBeVisible();await expect(page.getByRole("textbox",{name:"Домен"})).toHaveValue("example.test");fail=false;await run(page).click();await expect(page.getByText("example.test. IN A 192.0.2.1",{exact:true})).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Диагностика: без выбора не запрашивает устройство",async({page})=>{
|
||||||
|
let requests=0;await page.route("**/api/servers/*/**",r=>{requests++;return r.fulfill({json:{ipv4:"192.0.2.1"}})})
|
||||||
|
await page.goto("/probes");await expect(page.getByRole("status").filter({hasText:"Выберите устройство для диагностики"})).toBeVisible();await expect(run(page)).toBeDisabled();expect(requests).toBe(0)
|
||||||
|
})
|
||||||
|
for(const query of ["server=1&server=2","server=bad%20id","server="])test("Диагностика: неверная ссылка не читает API "+query,async({page})=>{
|
||||||
|
let requests=0;await page.route("**/api/servers**",r=>{requests++;return r.fulfill({json:devices})})
|
||||||
|
await page.goto("/probes?keep=1&"+query+"#result");await expect(page.getByRole("alert",{name:"Ссылка диагностики"})).toBeVisible();expect(requests).toBe(0)
|
||||||
|
await page.getByRole("button",{name:"Сбросить контекст диагностики"}).click();await expect(page).toHaveURL(/\/probes\?keep=1#result$/)
|
||||||
|
})
|
||||||
|
test("Диагностика: неизвестный сервер не заменяется первым",async({page})=>{
|
||||||
|
let deviceRequests=0;await page.route("**/api/servers/*/**",r=>{deviceRequests++;return r.fulfill({json:{ipv4:null}})})
|
||||||
|
await page.goto("/probes?server=999&keep=1#result");await expect(page.getByRole("alert",{name:"Устройство диагностики"})).toBeVisible();await expect(run(page)).toBeDisabled();expect(deviceRequests).toBe(0)
|
||||||
|
await page.getByRole("button",{name:"Сбросить выбор устройства"}).click();await expect(page).toHaveURL(/\/probes\?keep=1#result$/)
|
||||||
|
})
|
||||||
|
test("Диагностика: URL выбор, назад и reload сохраняют query/hash",async({page})=>{
|
||||||
|
await page.route("**/api/servers",r=>r.fulfill({json:[...devices,{...devices[0],id:2,name:"Второй",host:"192.0.2.2"}]}))
|
||||||
|
await page.goto("/probes?server=2&keep=1#result");await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true")
|
||||||
|
await page.getByRole("option",{name:/Первый/}).click();await expect(page).toHaveURL(/keep=1&server=1#result$/);await page.goBack();await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true");await page.reload();await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true")
|
||||||
|
})
|
||||||
Reference in new issue
Block a user