fix(ui): открывать GRE-туннель с контекстом сервера
This commit is contained in:
1 parent
6c8e66693b
commit
67a152b7c8
7 files changed
+188
-14
No files matched your search
+53
-7
@@ -1,6 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/components/reui/alert"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { DataPageCard } from "@/components/data-page-card"
|
||||
import { DataPageToolbar, DataPageToolbarFrame } from "@/components/data-page-toolbar"
|
||||
@@ -248,20 +251,25 @@ type TabFilter = "all" | "up" | "ipsec" | "plain"
|
||||
type PageTab = "tunnels" | "pools"
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
export default function GrePage() {
|
||||
function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, onSelectServer }: {
|
||||
selectedServerId: string
|
||||
selectedTunnelId: string | null
|
||||
allTunnelsHref: string
|
||||
onSelectServer: (id: string) => void
|
||||
}) {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
|
||||
const [liveServers, setLiveServers] = useState<Server[]>([])
|
||||
const [liveTunnels, setLiveTunnels] = useState<GreTunnel[]>([])
|
||||
const [dataLoading, setDataLoading] = useState(false)
|
||||
const [dataLoading, setDataLoading] = useState(true)
|
||||
const [dataError, setDataError] = useState<string | null>(null)
|
||||
const [failedServerIds, setFailedServerIds] = useState<string[]>([])
|
||||
|
||||
const [pageTab, setPageTab] = useState<PageTab>("tunnels")
|
||||
const [tabFilter, setTabFilter] = useState<TabFilter>("all")
|
||||
const [search, setSearch] = useState("")
|
||||
const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID)
|
||||
|
||||
const [tunnelOpen, setTunnelOpen] = useState(false)
|
||||
const [tunnelMode, setTunnelMode] = useState<"create" | "edit">("create")
|
||||
@@ -293,6 +301,7 @@ export default function GrePage() {
|
||||
])
|
||||
setLiveServers(backendServers.map(mapBackendToServer))
|
||||
setLiveTunnels(greRes.tunnels)
|
||||
setFailedServerIds((greRes.failures ?? []).map(f => String(f.serverId)))
|
||||
setLiveStale(false)
|
||||
if (greRes.failures?.length) {
|
||||
toast.warning(
|
||||
@@ -344,9 +353,13 @@ export default function GrePage() {
|
||||
), [displayServers, displayTunnels])
|
||||
|
||||
const scopedTunnels = useMemo(() => {
|
||||
if (selectedTunnelId !== null) return displayTunnels.filter(t => t.serverId === selectedServerId && t.id === selectedTunnelId)
|
||||
if (selectedServerId === ALL_SERVERS_ID) return displayTunnels
|
||||
return displayTunnels.filter((t) => t.serverId === selectedServerId)
|
||||
}, [displayTunnels, selectedServerId])
|
||||
}, [displayTunnels, selectedServerId, selectedTunnelId])
|
||||
|
||||
const selectedTunnel = selectedTunnelId === null ? undefined : scopedTunnels[0]
|
||||
const selectedTunnelError = isLive && (dataError !== null || failedServerIds.includes(selectedServerId))
|
||||
|
||||
const serverById = useMemo(
|
||||
() => Object.fromEntries(displayServers.map((s) => [s.id, s])),
|
||||
@@ -358,7 +371,7 @@ export default function GrePage() {
|
||||
)
|
||||
|
||||
const historyServerId = selectedServerId === ALL_SERVERS_ID ? null : selectedServerId
|
||||
const mutationsLocked = isLive && (mutateBusy || liveStale || historyRestoring)
|
||||
const mutationsLocked = isLive && (mutateBusy || liveStale || selectedTunnelError || historyRestoring)
|
||||
|
||||
const loadRevisions = useCallback(async () => {
|
||||
if (!isLive || !historyServerId) return
|
||||
@@ -580,7 +593,8 @@ export default function GrePage() {
|
||||
<ServerRailLayout
|
||||
items={greRailItems}
|
||||
selectedId={selectedServerId}
|
||||
onSelect={setSelectedServerId}
|
||||
preserveSelection
|
||||
onSelect={onSelectServer}
|
||||
showAll
|
||||
allCount={displayServers.length}
|
||||
loading={isLive && dataLoading && displayServers.length === 0}
|
||||
@@ -630,6 +644,18 @@ export default function GrePage() {
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
{selectedTunnelId !== null && (
|
||||
<Alert variant={selectedTunnelError || ((!isLive || !dataLoading) && !selectedTunnel) ? "warning" : "info"} role="status" aria-label="GRE-туннель по ссылке">
|
||||
<AlertTitle className="line-clamp-none wrap-anywhere">{selectedTunnel ? "Выбран GRE-туннель: " + selectedTunnel.name : "GRE-туннель по ссылке"}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p className="wrap-anywhere">{isLive && dataLoading ? "Загрузка туннеля…" : selectedTunnelError ? "Не удалось получить данные выбранного сервера. Повторите обновление; сохранённые данные могут быть устаревшими." : selectedTunnel ? (serverById[selectedServerId]?.name ?? selectedServerId) + " · " + selectedTunnel.remoteAddress + ". Список ограничен выбранным туннелем; сводка относится ко всем серверам." : "Туннель не найден или недоступен на указанном сервере. Проверьте ссылку и доступ к устройству."}</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link href={allTunnelsHref} className="underline underline-offset-2">Все туннели сервера</Link>
|
||||
<Link href="/dashboard" className="underline underline-offset-2">К дашборду</Link>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Legacy banner */}
|
||||
<div className="flex items-start gap-3 rounded-lg bg-amber-500/5 border border-amber-500/20 px-4 py-3 text-sm">
|
||||
@@ -1079,3 +1105,23 @@ export default function GrePage() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function GreRoute() {
|
||||
const params = useSearchParams()
|
||||
const router = useRouter()
|
||||
const serverId = params.get("server") ?? ALL_SERVERS_ID
|
||||
const tunnelId = params.get("tunnel")
|
||||
const rest = new URLSearchParams(params.toString())
|
||||
rest.delete("tunnel")
|
||||
const allTunnelsHref = "/gre" + (rest.size ? "?" + rest.toString() : "")
|
||||
return <GrePageContent key={JSON.stringify([serverId, tunnelId])} selectedServerId={serverId} selectedTunnelId={tunnelId} allTunnelsHref={allTunnelsHref} onSelectServer={id => {
|
||||
const next = new URLSearchParams(rest)
|
||||
if (id === ALL_SERVERS_ID) next.delete("server")
|
||||
else next.set("server", id)
|
||||
router.push("/gre" + (next.size ? "?" + next.toString() : ""))
|
||||
}} />
|
||||
}
|
||||
|
||||
export default function GrePage() {
|
||||
return <Suspense fallback={<div role="status">Загрузка GRE…</div>}><GreRoute /></Suspense>
|
||||
}
|
||||
@@ -95,6 +95,7 @@ function ServerRailLayout({
|
||||
showType = true,
|
||||
headerRight,
|
||||
loading = false,
|
||||
preserveSelection = false,
|
||||
extra,
|
||||
header,
|
||||
banner,
|
||||
@@ -110,6 +111,7 @@ function ServerRailLayout({
|
||||
showType?: boolean
|
||||
headerRight?: ReactNode
|
||||
loading?: boolean
|
||||
preserveSelection?: boolean
|
||||
extra?: ReactNode
|
||||
header?: ReactNode
|
||||
banner?: ReactNode
|
||||
@@ -185,6 +187,7 @@ function ServerRailLayout({
|
||||
showType={showType}
|
||||
headerRight={railHeaderRight}
|
||||
loading={loading}
|
||||
preserveSelection={preserveSelection}
|
||||
className="min-h-0 flex-1"
|
||||
/>
|
||||
)
|
||||
@@ -222,6 +225,7 @@ function ServerRailLayout({
|
||||
showHeader={false}
|
||||
headerRight={headerRight}
|
||||
loading={loading}
|
||||
preserveSelection={preserveSelection}
|
||||
className="min-h-0 flex-1"
|
||||
/>
|
||||
{extra}
|
||||
|
||||
@@ -96,6 +96,7 @@ function ServerTileRail({
|
||||
showType = true,
|
||||
headerRight,
|
||||
loading = false,
|
||||
preserveSelection = false,
|
||||
className,
|
||||
}: {
|
||||
items: ServerTileItem[]
|
||||
@@ -108,6 +109,7 @@ function ServerTileRail({
|
||||
showType?: boolean
|
||||
headerRight?: ReactNode
|
||||
loading?: boolean
|
||||
preserveSelection?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const [query, setQuery] = useState("")
|
||||
@@ -147,9 +149,10 @@ function ServerTileRail({
|
||||
const resolvedId = useMemo(() => {
|
||||
if (showAll && selectedId === ALL_SERVERS_ID) return ALL_SERVERS_ID
|
||||
if (items.some((item) => item.id === selectedId)) return selectedId
|
||||
if (preserveSelection) return ""
|
||||
if (showAll) return ALL_SERVERS_ID
|
||||
return items.find(isSelectable)?.id ?? items[0]?.id ?? ""
|
||||
}, [items, selectedId, showAll])
|
||||
}, [items, selectedId, showAll, preserveSelection])
|
||||
|
||||
useEffect(() => {
|
||||
if (resolvedId && resolvedId !== selectedId) onSelect(resolvedId)
|
||||
|
||||
+13
-1
@@ -20,7 +20,7 @@
|
||||
| 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже |
|
||||
| 4 | Остальные таблицы, удаление старых API, общая регрессия | Завершён 2026-09-20; границы приёмки ниже |
|
||||
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
||||
| 6 | Навигация и дашборд | В работе: готовы переходы к серверам и проверкам; остаются туннели |
|
||||
| 6 | Навигация и дашборд | В работе: готовы переходы к серверам, проверкам и GRE; остаются WireGuard/VXLAN |
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат |
|
||||
| 8 | Трафик и статистика | Не начат |
|
||||
| 9 | VPN, Firewall и Filters | Не начат |
|
||||
@@ -435,6 +435,18 @@ Production build успешен: контракты, TypeScript, 34 страни
|
||||
|
||||
Квота перед итоговой сборкой: использовано 55% пятичасовой и 40% недельной. Сохранить отдельный локальный коммит без push. **Следующий шаг этапа 6 — туннели GRE/WireGuard/VXLAN:** добавить сервер и ID объекта в href overlay, обработать оба значения на принимающих страницах, исключить подстановку другого устройства при отсутствующем ID. Проверить прямую ссылку, возврат, одинаковые имена/RouterOS ID на разных устройствах и отсутствие мутаций при открытии. Этап 6 остаётся открытым до этих переходов и приёмки.
|
||||
|
||||
## Этап 6 — блок 6: переход к GRE-туннелю (2026-09-20)
|
||||
|
||||
Очередь проблем открывает `/gre?server=<serverId>&tunnel=<id>`; оба значения кодируются. Ключ GRE в очереди включает сервер и ID, поэтому одинаковые RouterOS ID на разных устройствах не конфликтуют. Принимающая страница выбирает точную пару, показывает контекст через установленный ReUI Alert и сохраняет остальные query-параметры при снятии выбора. Ссылка «Все туннели сервера» оставляет сервер; ручная смена устройства снимает выбор туннеля и отражается в URL. Back/reload восстанавливают контекст. Открытие ссылки не отправляет POST.
|
||||
|
||||
Отдельно обработаны отсутствующая пара, ошибки 403/500, неуспешное обновление с сохранением последних данных и частичный сбой выбранного сервера. Такой сбой не выдаётся за отсутствие туннеля; действия таблицы блокируются. Полная достоверность сводки GRE при частичном опросе и сетевые формы остаются этапом 9.
|
||||
|
||||
Тесты обнаружили автоматический сброс URL-выбора серверной панелью до завершения загрузки. Добавлен opt-in `preserveSelection` в ServerRailLayout/ServerTileRail, включён для GRE; существующие потребители сохраняют прежнее поведение. При отсутствующем сервере панель не выбирает другой автоматически.
|
||||
|
||||
Проверки: 20 связанных E2E (GRE, IPsec, дашборд, навигация) passed, затем отдельный дополнительный сценарий ручной смены устройства passed — всего 21. Production build и TypeScript успешны, 34 статические страницы. Скриншот контекста ошибки просмотрен на 390 px. ESLint новых замечаний не добавил: у GRE прежние 14 unused warnings, у ServerRailLayout прежняя ошибка set-state-in-effect (оба baseline сверены с HEAD); остальные изменённые TS-файлы чистые. Проверки выполнены с HTTP-моками, реальный RouterOS не проверен.
|
||||
|
||||
Квота: на старте использовано 69% пятичасовой, перед заключительными проверками 86% (недельной 45%). Следующий блок в этом окне не начинать. Сохранить локальным коммитом без push. **Следующий шаг: WireGuard, затем VXLAN** — URL с сервером и точным ID, запрет fallback на другой сервер, переиспользовать preserveSelection; проверить одинаковые имена/ID, прямой URL, Back, ошибки и отсутствие автоматических мутаций. Этап 6 остаётся открытым до завершения этих переходов и приёмки.
|
||||
|
||||
## Карта миграции (исходная инвентаризация этапа 0)
|
||||
|
||||
Исходная связка на этапе 0: @tanstack/react-table ^8.21.3, @tanstack/react-virtual ^3.14.4, base-nova. По итогам этапа 4 используется @tanstack/react-table 9.2.4 с useTable/dataGridFeatures и дерево FilterQuery вместо старого filters/onChange. Список ниже сохранён как историческая инвентаризация.
|
||||
|
||||
@@ -49,4 +49,4 @@ HTTP-моки проверяют интерфейс и запросы. Реал
|
||||
|
||||
### Переходы к объектам
|
||||
|
||||
Готовы и проверены: /servers?server=<id> и /uptime?probe=<id>. Идентификаторы кодируются encodeURIComponent, целевые страницы выбирают точный ID и сообщают об отсутствии/ошибке загрузки. Снятие выбора сохраняет остальные query-параметры. Ещё не реализованы ссылки на конкретные GRE/WireGuard/VXLAN; для них нужен контекст сервера вместе с ID объекта.
|
||||
Готовы и проверены: /servers?server=<id>, /uptime?probe=<id> и /gre?server=<serverId>&tunnel=<id>. Идентификаторы кодируются encodeURIComponent, целевые страницы выбирают точный ID (для GRE — пару сервер/туннель) и сообщают об отсутствии/ошибке загрузки. Снятие выбора сохраняет остальные query-параметры. GRE использует preserveSelection серверной панели, чтобы не сбрасывать URL при загрузке/отсутствии устройства. Ещё не реализованы ссылки на конкретные WireGuard/VXLAN; для них нужен контекст сервера вместе с ID объекта.
|
||||
@@ -208,10 +208,10 @@ function sumSeries(rows: TrafficServerRow[], key: "rxSeries" | "txSeries"): numb
|
||||
|
||||
function mockOverlayItems(): OverlayItem[] {
|
||||
const gre: OverlayItem[] = mockGreTunnels.map((t) => ({
|
||||
id: `gre-${t.id}`,
|
||||
id: JSON.stringify(["gre", t.serverId, t.id]),
|
||||
name: t.name,
|
||||
kind: "gre",
|
||||
href: "/gre",
|
||||
href: "/gre?server=" + encodeURIComponent(t.serverId) + "&tunnel=" + encodeURIComponent(t.id),
|
||||
status: t.status,
|
||||
}))
|
||||
const wg: OverlayItem[] = mockServers.flatMap((s) =>
|
||||
@@ -320,10 +320,10 @@ export function useDashboardLive() {
|
||||
const greItems: OverlayItem[] =
|
||||
greRes.status === "fulfilled"
|
||||
? (greRes.value.tunnels ?? []).map((t) => ({
|
||||
id: `gre-${t.id}`,
|
||||
id: JSON.stringify(["gre", t.serverId, t.id]),
|
||||
name: t.name,
|
||||
kind: "gre" as const,
|
||||
href: "/gre",
|
||||
href: "/gre?server=" + encodeURIComponent(t.serverId) + "&tunnel=" + encodeURIComponent(t.id),
|
||||
status: t.status,
|
||||
}))
|
||||
: []
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { test, expect, type Page } from "@playwright/test"
|
||||
|
||||
async function setup(page: Page, url: string, failed = 0) {
|
||||
const control = { partial: false, failed, posts: [] as { path: string; body: unknown }[] }
|
||||
const servers = [{ id: 1, name: "Источник", host: "192.0.2.1", type: "exit-node", enabled: true, country: "RU", site: "TEST", asn: "64512", status: "online", latency: 1, os: "7", model: "CHR", wanUplinks: [] }]
|
||||
const tunnels = ["1", "2"].flatMap(serverId => ["*/2", "other"].map(id => ({ id, serverId, name: "gre-same", localAddress: "192.0.2.1", remoteAddress: "192.0.2." + (serverId === "1" ? "11" : "22"), localInnerIp: "10.0.0.1/30", remoteInnerIp: "10.0.0.2/30", poolId: "live", ipsec: null, mtu: 1400, keepaliveInterval: 10, keepaliveRetries: 3, dscp: "inherit", clampTcpMss: true, allowFastPath: true, comment: "", enabled: true, status: "down" })));
|
||||
servers.push({ ...servers[0], id: 2, name: "Второй сервер", host: "192.0.2.2" });
|
||||
const probes = ["p1", "p/2"].map(id => ({ id, srcServerId: "1", name: "Проверка сети", target: "192.0.2.10", filter: "main", rtt: null, loss: 100, status: "down", series: [0], enabled: true, showOnDashboard: false }))
|
||||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||
await page.route("**/api/**", route => {
|
||||
const request = route.request()
|
||||
const path = new URL(request.url()).pathname
|
||||
if (request.method() === "POST") control.posts.push({ path, body: request.postDataJSON() })
|
||||
if (path === "/api/gre/tunnels" && control.failed) return route.fulfill({ status: control.failed, json: { error: "Мониторинг недоступен" } })
|
||||
const json = path === "/api/auth/config" ? { required: false }
|
||||
: path === "/api/evobgp/settings" ? { enabled: false, baseUrl: "", secretConfigured: false }
|
||||
: path === "/api/servers" ? servers
|
||||
: path === "/api/uptime/overview" ? { probes, resources: [] }
|
||||
: path === "/api/uptime/speed-probes" ? { probes: [] }
|
||||
: path === "/api/uptime/speed-test/runs" ? { runs: [] }
|
||||
: path === "/api/bgp/sessions" ? []
|
||||
: path === "/api/gre/tunnels" || path === "/api/filters/gre-tunnels" ? { tunnels: control.partial ? tunnels.filter(t => t.serverId !== "2") : tunnels, failures: control.partial ? [{ serverId: "2", error: "Unavailable" }] : [] }
|
||||
: path === "/api/vxlan" ? { tunnels: [] }
|
||||
: path === "/api/wireguard" ? { interfaces: [], peers: [] }
|
||||
: path === "/api/traffic/servers" ? { servers: [] }
|
||||
: path === "/api/events" ? { events: [] }
|
||||
: path === "/api/internet-path/latest" ? { snapshot: { sampledAt: "2026-09-20T00:00:00Z", servers, greTunnels: [], filtersRulesets: [], speedProbes: [], routeLookupByServerId: {}, wanRuntimeByHomeId: {} } }
|
||||
: {}
|
||||
return route.fulfill({ json })
|
||||
})
|
||||
await page.goto(url)
|
||||
return control
|
||||
}
|
||||
|
||||
|
||||
test("GRE: переход из очереди выбирает сервер и точный ID без записи", async ({ page }) => {
|
||||
const control = await setup(page, "/dashboard")
|
||||
const link = page.getByRole("region", { name: "Требует внимания", exact: true }).locator('a[href="/gre?server=2&tunnel=*%2F2"]')
|
||||
await expect(link).toBeVisible()
|
||||
await link.click()
|
||||
const context = page.getByRole("status", { name: "GRE-туннель по ссылке" })
|
||||
await expect(context).toContainText("Второй сервер")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
|
||||
await expect(page.locator("tr[data-row-id]")).toContainText("192.0.2.22")
|
||||
expect(control.posts).toEqual([])
|
||||
await context.getByRole("link", { name: "К дашборду", exact: true }).click()
|
||||
await expect(link).toBeVisible()
|
||||
})
|
||||
|
||||
test("GRE: прямая ссылка, сброс, назад, reload и ошибка обновления", async ({ page }) => {
|
||||
const control = await setup(page, "/gre?server=2&tunnel=*%2F2&keep=1")
|
||||
const context = page.getByRole("status", { name: "GRE-туннель по ссылке" })
|
||||
const rows = page.locator("tr[data-row-id]")
|
||||
await expect(rows).toHaveCount(1)
|
||||
await context.getByRole("link", { name: "Все туннели сервера", exact: true }).click()
|
||||
await expect(page).toHaveURL(/server=2&keep=1$/)
|
||||
await expect(rows).toHaveCount(2)
|
||||
await page.goBack()
|
||||
await expect(rows).toHaveCount(1)
|
||||
await page.reload()
|
||||
await expect(context).toContainText("Второй сервер")
|
||||
control.failed = 503
|
||||
await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||
await expect(context).toContainText("сохранённые данные могут быть устаревшими")
|
||||
await expect(rows).toHaveCount(1)
|
||||
control.failed = 0
|
||||
await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||
await expect(context).toContainText("Список ограничен")
|
||||
control.partial = true
|
||||
await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||
await expect(context).toContainText("Не удалось получить данные выбранного сервера")
|
||||
await expect(rows).toHaveCount(0)
|
||||
expect(control.posts).toEqual([])
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await context.scrollIntoViewIfNeeded()
|
||||
const box = await context.boundingBox()
|
||||
expect(box!.x + box!.width).toBeLessThanOrEqual(390)
|
||||
await page.screenshot({ path: "test-results/gre-link-mobile.png", animations: "disabled" })
|
||||
})
|
||||
|
||||
test("GRE: ручной выбор сервера снимает выбор туннеля и сохраняет query", async ({ page }) => {
|
||||
const control = await setup(page, "/gre?server=2&tunnel=*%2F2&keep=1")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
|
||||
await page.getByRole("option").filter({ hasText: "Источник" }).click()
|
||||
await expect(page).toHaveURL(/server=1&keep=1$/)
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(2)
|
||||
await expect(page.getByRole("status", { name: "GRE-туннель по ссылке" })).toHaveCount(0)
|
||||
await page.goBack()
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
|
||||
await expect(page.locator("tr[data-row-id]")).toContainText("192.0.2.22")
|
||||
expect(control.posts).toEqual([])
|
||||
})
|
||||
|
||||
for (const query of ["server=missing&tunnel=*%2F2", "server=2&tunnel=missing", "tunnel=*%2F2"]) {
|
||||
test("GRE: нет подмены объекта " + query, async ({ page }) => {
|
||||
const control = await setup(page, "/gre?" + query)
|
||||
await expect(page.getByRole("status", { name: "GRE-туннель по ссылке" })).toContainText("Туннель не найден или недоступен")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
|
||||
expect(control.posts).toEqual([])
|
||||
})
|
||||
}
|
||||
for (const status of [403, 500]) {
|
||||
test("GRE: ошибка API " + status, async ({ page }) => {
|
||||
const control = await setup(page, "/gre?server=2&tunnel=*%2F2", status)
|
||||
await expect(page.getByRole("status", { name: "GRE-туннель по ссылке" })).toContainText("Не удалось получить данные выбранного сервера")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
|
||||
expect(control.posts).toEqual([])
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user