fix(ui): различать результат и отмену диагностики
This commit is contained in:
1 parent
8bda85129f
commit
6fa8e5bfd1
9 files changed
+132
-20
No files matched your search
+42
-18
@@ -20,6 +20,7 @@ import { Input } from "@/components/ui/input"
|
||||
import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout"
|
||||
import { type ServerTileItem } from "@/components/server-tile-rail"
|
||||
import { servers, greTunnels, type GreTunnel, type Server } from "@/lib/data"
|
||||
import { readProbeOutput } from "@/lib/probe-response"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import {
|
||||
PlayIcon, SquareIcon, CopyIcon, Trash2Icon, PlusIcon,
|
||||
@@ -33,7 +34,7 @@ import { requestJson } from "@/shared/api/http-client"
|
||||
// ─── types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type DiagTool = "ping" | "traceroute" | "bandwidth" | "dns" | "route" | "mtu"
|
||||
type RunStatus = "running" | "done" | "error"
|
||||
type RunStatus = "running" | "done" | "error" | "cancelled"
|
||||
type TraceProto = "icmp" | "udp" | "tcp"
|
||||
type DnsType = "A" | "AAAA" | "MX" | "NS" | "TXT" | "CNAME" | "PTR"
|
||||
|
||||
@@ -576,6 +577,12 @@ function ScheduleTab({
|
||||
// ─── page ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function ProbesPage() {
|
||||
const { mode, backendUrl, prefsHydrated } = useDataSource()
|
||||
if (!prefsHydrated) return <p role="status">Загрузка настроек диагностики…</p>
|
||||
return <ProbesContent key={JSON.stringify([mode, backendUrl])} />
|
||||
}
|
||||
|
||||
function ProbesContent() {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
@@ -716,6 +723,13 @@ export default function ProbesPage() {
|
||||
/** Живой POST /probes/run — отмена через fetch AbortSignal */
|
||||
const liveProbeRunRef = useRef<{ testId: string; ctrl: AbortController } | null>(null)
|
||||
|
||||
const runLock = useRef(false)
|
||||
const alive = useRef(true)
|
||||
useEffect(() => {
|
||||
alive.current = true
|
||||
return () => { alive.current = false; liveProbeRunRef.current?.ctrl.abort() }
|
||||
}, [])
|
||||
|
||||
const bwTunnels = tunnelsForSrc
|
||||
const srcServer = useMemo(() => allServers.find(s => s.id === srcId), [allServers, srcId])
|
||||
const bwTun = useMemo(() => bwTunnels.find(t => t.id === bwTunId), [bwTunnels, bwTunId])
|
||||
@@ -748,15 +762,18 @@ export default function ProbesPage() {
|
||||
// ── run test ──
|
||||
const runTest = useCallback(async () => {
|
||||
const srv = allServers.find(s => s.id === srcId)
|
||||
if (!srv) return
|
||||
if (!srv || !srv.enabled || runLock.current || (isLive && liveLoad !== "idle")) return
|
||||
const id = String(nextId.current++)
|
||||
const numericServerId = Number.parseInt(srcId, 10)
|
||||
|
||||
if (isLive) {
|
||||
if (!Number.isFinite(numericServerId)) return
|
||||
runLock.current = true
|
||||
setRunBusy(true)
|
||||
const abortCtrl = new AbortController()
|
||||
liveProbeRunRef.current = { testId: id, ctrl: abortCtrl }
|
||||
let timedOut = false
|
||||
const timeout = setTimeout(() => { timedOut = true; abortCtrl.abort() }, 60_000)
|
||||
setTests(p => [{
|
||||
id,
|
||||
tool,
|
||||
@@ -793,32 +810,34 @@ export default function ProbesPage() {
|
||||
bwProto,
|
||||
bwDuration,
|
||||
}
|
||||
const res = await apiFetch<{ output: string }>(`/api/servers/${numericServerId}/probes/run`, {
|
||||
const res = await apiFetch<unknown>(`/api/servers/${numericServerId}/probes/run`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
signal: abortCtrl.signal,
|
||||
})
|
||||
const lines = parseProbeOutput(res.output ?? "")
|
||||
const looksErr = (res.output ?? "").trim().toLowerCase().startsWith("error:")
|
||||
if (!alive.current || abortCtrl.signal.aborted) return
|
||||
const output = readProbeOutput(res)
|
||||
const lines = parseProbeOutput(output || "Запрос выполнен: API вернул пустой вывод.")
|
||||
setTests(p => p.map(t => (t.id === id
|
||||
? {
|
||||
...t,
|
||||
lines,
|
||||
totalLines: lines.length,
|
||||
status: looksErr ? "error" : "done",
|
||||
status: "done",
|
||||
}
|
||||
: t)))
|
||||
} catch (e) {
|
||||
const aborted =
|
||||
if (!alive.current) return
|
||||
const aborted = abortCtrl.signal.aborted ||
|
||||
(typeof DOMException !== "undefined" && e instanceof DOMException && e.name === "AbortError")
|
||||
|| (e instanceof Error && e.name === "AbortError")
|
||||
if (aborted) {
|
||||
setTests(p => p.map(t => (t.id === id
|
||||
? {
|
||||
...t,
|
||||
lines: [{ text: "Запрос отменён (стоп или закрытие запроса).", kind: "dim" }],
|
||||
lines: [{ text: timedOut ? "Время ожидания истекло. Результат на устройстве не подтверждён; повтор выполняется вручную." : "Ожидание отменено. Остановка на устройстве не подтверждена.", kind: timedOut ? "err" : "dim" }],
|
||||
totalLines: 1,
|
||||
status: "done",
|
||||
status: timedOut ? "error" : "cancelled",
|
||||
}
|
||||
: t)))
|
||||
} else {
|
||||
@@ -833,8 +852,10 @@ export default function ProbesPage() {
|
||||
: t)))
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
if (liveProbeRunRef.current?.testId === id) liveProbeRunRef.current = null
|
||||
setRunBusy(false)
|
||||
runLock.current = false
|
||||
if (alive.current) setRunBusy(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -867,6 +888,7 @@ export default function ProbesPage() {
|
||||
setTab("history")
|
||||
}, [
|
||||
isLive,
|
||||
liveLoad,
|
||||
allServers,
|
||||
srcId,
|
||||
tool,
|
||||
@@ -887,12 +909,15 @@ export default function ProbesPage() {
|
||||
])
|
||||
|
||||
const stopTest = (id: string) => {
|
||||
if (liveProbeRunRef.current?.testId === id) liveProbeRunRef.current.ctrl.abort()
|
||||
if (liveProbeRunRef.current?.testId === id) { liveProbeRunRef.current.ctrl.abort(); return }
|
||||
setTests(p => p.map(t => (t.id === id && t.status === "running"
|
||||
? { ...t, status: "done" as const, totalLines: t.lines.length }
|
||||
? { ...t, status: "cancelled" as const, totalLines: t.lines.length }
|
||||
: t)))
|
||||
}
|
||||
const clearTest = (id: string) => setTests(p => p.filter(t => t.id !== id))
|
||||
const clearTest = (id: string) => {
|
||||
if (liveProbeRunRef.current?.testId === id) liveProbeRunRef.current.ctrl.abort()
|
||||
setTests(p => p.filter(t => t.id !== id))
|
||||
}
|
||||
|
||||
const running = tests.filter(t => t.status === "running")
|
||||
|
||||
@@ -923,10 +948,7 @@ export default function ProbesPage() {
|
||||
<ServerRailMobileButton />
|
||||
{running.length > 0
|
||||
? <Button variant="outline" size="sm" onClick={() => {
|
||||
liveProbeRunRef.current?.ctrl.abort()
|
||||
setTests(p => p.map(t => (t.status === "running"
|
||||
? { ...t, status: "done" as const, totalLines: t.lines.length }
|
||||
: t)))
|
||||
running.forEach(test => stopTest(test.id))
|
||||
}}>
|
||||
<SquareIcon className="size-4" />Остановить все
|
||||
</Button>
|
||||
@@ -984,6 +1006,7 @@ export default function ProbesPage() {
|
||||
</OptionLabel>
|
||||
<Input value={target} onChange={e => setTarget(e.target.value)}
|
||||
placeholder={tool === "dns" ? "google.com" : tool === "route" ? "8.8.8.8" : "8.8.8.8"}
|
||||
aria-label={tool === "dns" ? "Домен" : "Цель диагностики"}
|
||||
className="font-mono text-sm"
|
||||
onKeyDown={e => e.key === "Enter" && runTest()}
|
||||
/>
|
||||
@@ -1066,7 +1089,7 @@ export default function ProbesPage() {
|
||||
|
||||
<Button
|
||||
onClick={() => void runTest()}
|
||||
disabled={runBusy || !srcServer || (tool === "bandwidth" && !bwTunnels.length)}
|
||||
disabled={runBusy || !srcServer?.enabled || (isLive && liveLoad !== "idle") || (tool === "bandwidth" && !bwTunnels.length)}
|
||||
className="h-8 shrink-0 gap-1.5 self-end"
|
||||
>
|
||||
{runBusy ? <LoaderCircleIcon className="size-3.5 animate-spin" /> : <PlayIcon className="size-3.5" />}
|
||||
@@ -1155,6 +1178,7 @@ export default function ProbesPage() {
|
||||
{test.status === "done" && test.source === "live" && (
|
||||
<span className="text-[10px] font-mono text-emerald-600 dark:text-emerald-400">live</span>
|
||||
)}
|
||||
{test.status === "cancelled" && <span role="status" className="text-[11px] text-muted-foreground">ожидание отменено</span>}
|
||||
{test.status === "error" && (
|
||||
<span className="text-[11px] text-destructive">ошибка</span>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.57; оптимизатор, карта и терминал приняты локально; диагностика /probes в работе |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.58; оптимизатор, карта и терминал приняты локально; диагностика /probes в работе |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
@@ -36,6 +36,14 @@
|
||||
|
||||
|
||||
|
||||
## Блок 10.58 — достоверный результат диагностики GRE (2026-10-01)
|
||||
|
||||
- /probes проверяет строковый output и legacy error; неверный контракт не превращается в пустой успех. Пустой подтверждённый вывод подписан, цель/параметры сохраняются после отказа.
|
||||
- Немедленный single-flight lock закрывает повторный Enter; disabled устройство и неподтверждённый каталог не запускают запрос. Стоп/Остановить все отменяют ожидание со статусом cancelled, без заявления об остановке RouterOS. Таймаут 60 с показывает неизвестность результата и требует ручного повтора.
|
||||
- Cleanup отменяет запрос; ключ режима/backend исключает перенос прежней истории/вывода. Удаление выполняющегося теста отменяет ожидание. Существующие Frame/состав инструментов/сетевые правила сохранены.
|
||||
- 7 основных и 4 source E2E, 2 unit, TypeScript и production frontend прошли. Новые helper/test файлы проходят lint. Страница сохраняет исходные 8 set-state-in-effect ошибок и 2 предупреждения: сравнение ESLint HEAD/current подтвердило одинаковые counts; это не чистый lint страницы.
|
||||
- Далее 10.59: каталог серверов/ошибка/повтор и неизвестный status; затем URL-контекст, права, GRE каталог, backend probes и адаптивная приёмка. Расписание отдельно в 11Б. Реальный RouterOS не проверен.
|
||||
|
||||
## Блок 10.57 — локальная приёмка терминала (2026-10-01)
|
||||
|
||||
- Терминал принят локально: 20 основных E2E (17 данных и 3 адаптивных), 8 source E2E, TypeScript, lint и production frontend build прошли. Проверены timeout/поздний ответ, пустой/disabled каталог, неверный URL, сохранение команды, смена режима/backend и 1200 строк вывода.
|
||||
|
||||
@@ -338,3 +338,12 @@ Shared контракт и backend observed исходные значения/д
|
||||
- Границы: HTTP fixtures/DI доказывают обработку UI/API, не реальный RouterOS/БД/портал. REST терминал не является SSH-сессией; clear локальный, отмена/timeout не подтверждают остановку на устройстве.
|
||||
- Этап 10 остаётся в работе: обнаружен отдельный /probes (Диагностика GRE), который ещё требует приёмки. Далее 10.58: строгий результат, single-flight и отмена запросов диагностики; каталоги/URL/права и backend — отдельными блоками. Расписание относится к 11Б.
|
||||
|
||||
|
||||
## Блок 10.58 — достоверный результат диагностики GRE (2026-10-01)
|
||||
|
||||
- /probes проверяет строковый output и legacy error; неверный контракт не превращается в пустой успех. Пустой подтверждённый вывод подписан, цель/параметры сохраняются после отказа.
|
||||
- Немедленный single-flight lock закрывает повторный Enter; disabled устройство и неподтверждённый каталог не запускают запрос. Стоп/Остановить все отменяют ожидание со статусом cancelled, без заявления об остановке RouterOS. Таймаут 60 с показывает неизвестность результата и требует ручного повтора.
|
||||
- Cleanup отменяет запрос; ключ режима/backend исключает перенос прежней истории/вывода. Удаление выполняющегося теста отменяет ожидание. Существующие Frame/состав инструментов/сетевые правила сохранены.
|
||||
- 7 основных и 4 source E2E, 2 unit, TypeScript и production frontend прошли. Новые helper/test файлы проходят lint. Страница сохраняет исходные 8 set-state-in-effect ошибок и 2 предупреждения: сравнение ESLint HEAD/current подтвердило одинаковые counts; это не чистый lint страницы.
|
||||
- Далее 10.59: каталог серверов/ошибка/повтор и неизвестный status; затем URL-контекст, права, GRE каталог, backend probes и адаптивная приёмка. Расписание отдельно в 11Б. Реальный RouterOS не проверен.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { test } from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { readProbeOutput } from "./probe-response"
|
||||
test("Диагностика требует подтверждённый string output",()=>{for(const raw of [null,{},[],{output:null},{output:1},{output:"ok",error:"fail"}])assert.throws(()=>readProbeOutput(raw));assert.equal(readProbeOutput({output:""}),"");assert.equal(readProbeOutput({output:"PING"}),"PING")})
|
||||
test("Диагностика отклоняет legacy error и oversized output",()=>{assert.throws(()=>readProbeOutput({output:" error: отказ"}),/отказ/);assert.throws(()=>readProbeOutput({output:"x".repeat(1024*1024+1)}))})
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod"
|
||||
const response = z.object({ output: z.string().max(1024 * 1024) }).strict()
|
||||
export function readProbeOutput(raw: unknown): string {
|
||||
const result = response.safeParse(raw)
|
||||
if (!result.success) throw new Error("Некорректный ответ API: результат диагностики не подтверждён.")
|
||||
if (/^error:/i.test(result.data.output.trimStart())) throw new Error(result.data.output.trim())
|
||||
return result.data.output
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from "@playwright/test"
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
testIgnore: ["**/terminal-context.spec.ts", "**/network-map-context.spec.ts", "**/route-optimizer-context.spec.ts", "**/recursive-routes-context.spec.ts", "**/ospf-context.spec.ts", "**/bgp-context.spec.ts", "**/firewall-context.spec.ts", "**/filters-context.spec.ts", "**/ipsec-context.spec.ts", "**/gre-context.spec.ts", "**/vxlan-context.spec.ts"],
|
||||
testIgnore: ["**/probes-context.spec.ts", "**/terminal-context.spec.ts", "**/network-map-context.spec.ts", "**/route-optimizer-context.spec.ts", "**/recursive-routes-context.spec.ts", "**/ospf-context.spec.ts", "**/bgp-context.spec.ts", "**/firewall-context.spec.ts", "**/filters-context.spec.ts", "**/ipsec-context.spec.ts", "**/gre-context.spec.ts", "**/vxlan-context.spec.ts"],
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
retries: 0,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "@playwright/test"
|
||||
import base from "./playwright.config"
|
||||
const server=base.webServer
|
||||
if(!server||Array.isArray(server))throw new Error("Ожидается один локальный сервер")
|
||||
export default defineConfig({
|
||||
...base, use:{...base.use,baseURL:"http://127.0.0.1:3101"},testIgnore:[],testMatch:"probes-context.spec.ts",
|
||||
webServer:{...server,command:"node node_modules/next/dist/bin/next dev --hostname 127.0.0.1 --port 3101",url:"http://127.0.0.1:3101",env:{...server.env,NEXT_PUBLIC_BACKEND_URL:"local"}},
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
const headers={"access-control-allow-origin":"*","access-control-allow-methods":"GET,POST,OPTIONS","access-control-allow-headers":"Content-Type,Authorization"}
|
||||
for(const change of ["backend","mode"])for(const fails of [false,true])test("Диагностика: поздний запрос/"+change+"/"+fails,async({page})=>{
|
||||
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("**/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",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.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()
|
||||
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.getByRole("button",{name:"Запустить",exact:true}).click()
|
||||
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)
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { test, expect, type Page } from "@playwright/test"
|
||||
const devices=[{id:1,name:"Первый",host:"192.0.2.1",site:"Тест",country:"RU",asn:"AS64500",type:"vps",enabled:true,status:"online",latency:null}]
|
||||
async function routes(page:Page){
|
||||
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"?devices:p==="/api/auth/config"?{required:false}:p.endsWith("/ros-src-address")?{ipv4:"192.0.2.1"}:{tunnels:[]}})})
|
||||
}
|
||||
test.beforeEach(async({page})=>routes(page))
|
||||
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})=>{
|
||||
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 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()
|
||||
})
|
||||
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()
|
||||
})
|
||||
test("Диагностика: повторный Enter, Стоп и поздний успех",async({page})=>{
|
||||
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.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.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()
|
||||
})
|
||||
test("Диагностика: таймаут сохраняет ввод, без автоматического повтора",async({page})=>{
|
||||
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.goto("/probes");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()
|
||||
})
|
||||
test("Диагностика: уход со страницы не переносит поздний вывод",async({page})=>{
|
||||
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.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)
|
||||
})
|
||||
Reference in new issue
Block a user