fix(ui): завершить переходы к объектам дашборда через VXLAN
This commit is contained in:
1 parent
af84f0135d
commit
d25afa4b9e
6 files changed
+190
-23
No files matched your search
+53
-13
@@ -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 as ContextAlert, AlertTitle as ContextTitle, AlertDescription as ContextDescription } from "@/components/reui/alert"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { vxlanTunnels as mockVxlanTunnels, servers as mockServers } from "@/lib/data"
|
||||
import type { Server, VxlanTunnel } from "@/lib/data"
|
||||
@@ -121,17 +124,21 @@ function ExportSheet({
|
||||
)
|
||||
}
|
||||
|
||||
export default function VxlanPage() {
|
||||
function VxlanPageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, onSelectServer }: {
|
||||
selectedServerId: string
|
||||
selectedTunnelId: string | null
|
||||
allTunnelsHref: string
|
||||
onSelectServer: (id: string) => void
|
||||
}) {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
|
||||
const [search, setSearch] = useState("")
|
||||
const [exportTunnel, setExportTunnel] = useState<VxlanTunnel | null>(null)
|
||||
const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID)
|
||||
|
||||
const [liveTunnels, setLiveTunnels] = useState<VxlanTunnel[]>([])
|
||||
const [liveServers, setLiveServers] = useState<Server[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [liveError, setLiveError] = useState<string | null>(null)
|
||||
|
||||
const loadLive = useCallback(async () => {
|
||||
@@ -147,7 +154,6 @@ export default function VxlanPage() {
|
||||
setLiveServers(serversRes.filter((s) => s.enabled).map(mapBackendServer))
|
||||
} catch (e) {
|
||||
setLiveError(e instanceof Error ? e.message : "Ошибка загрузки")
|
||||
setLiveTunnels([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -170,15 +176,15 @@ export default function VxlanPage() {
|
||||
const displayTunnels = isLive ? liveTunnels : mockVxlanTunnels
|
||||
const displayServers = isLive ? liveServers : mockServers.filter((s) => s.enabled)
|
||||
|
||||
const effectiveServerId =
|
||||
selectedServerId === ALL_SERVERS_ID || displayServers.some((s) => s.id === selectedServerId)
|
||||
? selectedServerId
|
||||
: ALL_SERVERS_ID
|
||||
const effectiveServerId = selectedServerId
|
||||
|
||||
const scopedTunnels = useMemo(() => {
|
||||
if (selectedTunnelId !== null) return displayTunnels.filter(t => t.serverId === effectiveServerId && t.id === selectedTunnelId)
|
||||
if (effectiveServerId === ALL_SERVERS_ID) return displayTunnels
|
||||
return displayTunnels.filter((t) => t.serverId === effectiveServerId)
|
||||
}, [displayTunnels, effectiveServerId])
|
||||
}, [displayTunnels, effectiveServerId, selectedTunnelId])
|
||||
|
||||
const selectedTunnel = selectedTunnelId === null ? undefined : scopedTunnels[0]
|
||||
|
||||
const serverById = useMemo(
|
||||
() => Object.fromEntries(displayServers.map((s) => [s.id, s])),
|
||||
@@ -218,7 +224,8 @@ export default function VxlanPage() {
|
||||
<ServerRailLayout
|
||||
items={railItems}
|
||||
selectedId={effectiveServerId}
|
||||
onSelect={setSelectedServerId}
|
||||
preserveSelection
|
||||
onSelect={onSelectServer}
|
||||
showAll
|
||||
allCount={displayServers.length}
|
||||
loading={isLive && loading && displayServers.length === 0}
|
||||
@@ -246,14 +253,27 @@ export default function VxlanPage() {
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
{selectedTunnelId !== null && (
|
||||
<ContextAlert variant={liveError || ((!isLive || !loading) && !selectedTunnel) ? "warning" : "info"} role="status" aria-label="VXLAN по ссылке">
|
||||
<ContextTitle className="line-clamp-none wrap-anywhere">{selectedTunnel ? "Выбран VXLAN: " + selectedTunnel.name : "VXLAN по ссылке"}</ContextTitle>
|
||||
<ContextDescription>
|
||||
<p className="wrap-anywhere">{isLive && loading ? "Загрузка туннеля…" : isLive && liveError ? "Не удалось обновить VXLAN. Повторите обновление; сохранённые данные могут быть устаревшими." : selectedTunnel ? (serverById[selectedServerId]?.name ?? selectedServerId) + " · VNI " + selectedTunnel.vni + ". Список и сводка ограничены выбранным туннелем." : "Туннель не получен для указанного сервера. Проверьте ссылку и доступ к устройству: туннель может отсутствовать или устройство не ответило на опрос."}</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>
|
||||
</ContextDescription>
|
||||
</ContextAlert>
|
||||
)}
|
||||
|
||||
{isLive && liveError && (
|
||||
|
||||
{isLive && liveError && selectedTunnelId === null && (
|
||||
<Alert variant="warning" className="py-2">
|
||||
<AlertCircleIcon />
|
||||
<AlertDescription className="text-xs">Ошибка загрузки: {liveError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{isLive && !loading && displayTunnels.length === 0 && !liveError && (
|
||||
{selectedTunnelId === null && isLive && !loading && displayTunnels.length === 0 && !liveError && (
|
||||
<div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
На опрошенных серверах нет VXLAN-интерфейсов
|
||||
</div>
|
||||
@@ -387,3 +407,23 @@ export default function VxlanPage() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function VxlanRoute() {
|
||||
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 = "/vxlan" + (rest.size ? "?" + rest.toString() : "")
|
||||
return <VxlanPageContent 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("/vxlan" + (next.size ? "?" + next.toString() : ""))
|
||||
}} />
|
||||
}
|
||||
|
||||
export default function VxlanPage() {
|
||||
return <Suspense fallback={<div role="status">Загрузка VXLAN…</div>}><VxlanRoute /></Suspense>
|
||||
}
|
||||
@@ -259,7 +259,7 @@ function VxlanDataGrid({ tunnels, servers, onExport }: VxlanDataGridProps) {
|
||||
manualPagination: true,
|
||||
data: tunnels,
|
||||
columns,
|
||||
getRowId: (row) => row.id,
|
||||
getRowId: (row) => JSON.stringify([row.serverId, row.id]),
|
||||
})
|
||||
|
||||
if (tunnels.length === 0) {
|
||||
|
||||
+13
-1
@@ -20,7 +20,7 @@
|
||||
| 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже |
|
||||
| 4 | Остальные таблицы, удаление старых API, общая регрессия | Завершён 2026-09-20; границы приёмки ниже |
|
||||
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
||||
| 6 | Навигация и дашборд | В работе: готовы переходы к серверам, проверкам, GRE и WireGuard; остаётся VXLAN |
|
||||
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат |
|
||||
| 8 | Трафик и статистика | Не начат |
|
||||
| 9 | VPN, Firewall и Filters | Не начат |
|
||||
@@ -457,6 +457,18 @@ Production build успешен: контракты, TypeScript, 34 страни
|
||||
|
||||
Квота: 0% использовано на старте нового окна, 25% перед сборкой (недельной 51%). Блок сохранить локальным коммитом без push. Следующий шаг — VXLAN и итоговая приёмка переходов этапа 6.
|
||||
|
||||
## Этап 6 — блок 8: VXLAN и итоговая приёмка (2026-09-20)
|
||||
|
||||
Готов переход `/vxlan?server=<serverId>&tunnel=<id>`. Выбор точной пары ограничивает список и сводку, сохраняется после reload/Back, снимается при ручной смене сервера. Остальные query сохраняются. Использованы установленный ReUI Alert, ServerRail preserveSelection и существующая Frame-композиция. Ключи очереди и строк таблицы учитывают сервер. Проверен экспорт .rsc выбранного объекта, без автоматических запросов записи при навигации.
|
||||
|
||||
Ошибка HTTP сохраняет последние данные и показывает предупреждение. Backend VXLAN сейчас возвращает пустой список при неуспешном опросе устройства: listVxlanTunnels и listVxlanTunnelsForServer подавляют исключения. Поэтому сообщение по ссылке говорит «туннель не получен» и перечисляет возможные причины, не утверждая, что объект удалён. Исправление этого API, полной достоверности сетевых KPI, форм и существующих незавершённых действий VXLAN — этап 9; сетевые алгоритмы в этом блоке не менялись.
|
||||
|
||||
Проверки блока: 8 новых vxlan-links, прежний vxlan-table и 6 dashboard-data — 15 passed. Итоговая приёмка этапа 6: **68 E2E passed** (page-header, navigation, dashboard-data, servers, uptime-links, gre-links, wireguard-links, vxlan-links, wireguard-tables, vxlan-table). Покрыты клавиатура/Escape, темы навигации, шапка 320/390/1024/1440 px, все пять типов объектов очереди, точный контекст, совпадающие ID, возврат, HTTP-ошибки и повтор операций. Контекст VXLAN просмотрен на 390 и 1440 px, WireGuard — на 390 px. Все изменённые TS-файлы WireGuard/VXLAN проходят ESLint. Финальные production build, TypeScript и генерация 34 страниц успешны; для сборки уточнён локальный тип существующего serverId в ответе VXLAN. ReUI audit checklist сверён: компоненты переиспользованы, ссылки настоящие, состояния объявляются role=status, vendored-компоненты/тема/зависимости не менялись.
|
||||
|
||||
**Этап 6 завершён в границах UI и HTTP-моков.** Реальные устройства, серверная авторизация и сетевые операции требуют отдельной приёмки. Это не итоговая приёмка всего продукта: известная обрезка мобильных строк Ping-проб остаётся этапом 7, прежние lint-ошибки общих компонентов перечислены выше. Квота перед фиксацией: использовано 70% пятичасовой и 58% недельной. Сохранить локальным коммитом без push; следующий крупный этап начать отдельным блоком.
|
||||
|
||||
**Следующий точный шаг — этап 7:** составить схему серверов/мониторинга по Data Grid Filtering 2 и Sheet 8, инвентаризировать права действий и текущие формы. Первый ограниченный блок — URL-контекст поиска/типа/Filters на /servers (с сохранением server), проверка прямого URL/Back/reload и возврата к списку. Затем мобильные строки Ping-проб и формы/права по журналу. Не менять сетевые правила в рамках визуальной унификации.
|
||||
|
||||
## Карта миграции (исходная инвентаризация этапа 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. Список ниже сохранён как историческая инвентаризация.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Этап 6: общая оболочка и дашборд
|
||||
|
||||
Рабочая схема от 2026-09-20. Основа — ui-design-contract.md: ReUI PRO, base-nova, Frame, Sidebar 240 px. Поиск вызывается Ctrl+K/⌘K, тема остаётся в NavUser. Весь этап ещё не завершён.
|
||||
Рабочая схема от 2026-09-20. Основа — ui-design-contract.md: ReUI PRO, base-nova, Frame, Sidebar 240 px. Поиск вызывается Ctrl+K/⌘K, тема остаётся в NavUser. Этап завершён в границах локальной приёмки; ограничения указаны ниже.
|
||||
|
||||
| Область | Референс | Результат |
|
||||
| --- | --- | --- |
|
||||
@@ -45,8 +45,12 @@ HTTP-моки проверяют интерфейс и запросы. Реал
|
||||
|
||||
## Состояние реализации
|
||||
|
||||
Шапка и навигация завершены. Иерархия дашборда, подписи и полные счётчики проблем проверены. Остаются ссылки очереди на конкретные объекты и их приёмка на принимающих страницах. Общая регрессия текущего состояния: 65 сценариев passed; production build успешен. Подробнее — ux-ui-progress.md.
|
||||
Шапка, навигация, иерархия дашборда и переходы ко всем типам объектов очереди завершены. Итоговая приёмка этапа: 68 E2E-сценариев passed; production build и TypeScript успешны. Проверены клавиатура, светлая/тёмная навигация, ширины шапки 320/390/1024/1440, контекст туннелей на ПК/телефоне, ошибки HTTP и возврат. Подробнее — ux-ui-progress.md.
|
||||
|
||||
### Переходы к объектам
|
||||
|
||||
Готовы и проверены: /servers?server=<id>, /uptime?probe=<id>, /gre?server=<serverId>&tunnel=<id>, /wireguard?server=<serverId>&interface=<id>. Идентификаторы кодируются encodeURIComponent, целевые страницы выбирают точный ID (для туннелей — пару сервер/объект) и сообщают об отсутствии/ошибке загрузки. Снятие выбора сохраняет остальные query-параметры. GRE и WireGuard используют preserveSelection серверной панели, чтобы не сбрасывать URL при загрузке/отсутствии устройства. Ещё не реализованы ссылки на конкретные VXLAN.
|
||||
Готовы и проверены: /servers?server=<id>, /uptime?probe=<id>, /gre?server=<serverId>&tunnel=<id>, /wireguard?server=<serverId>&interface=<id>, /vxlan?server=<serverId>&tunnel=<id>. Идентификаторы кодируются encodeURIComponent, целевые страницы выбирают точный ID (для туннелей — пару сервер/объект) и сообщают об отсутствии/ошибке загрузки. Снятие выбора сохраняет остальные query-параметры. GRE, WireGuard и VXLAN используют preserveSelection серверной панели, чтобы не сбрасывать URL при загрузке/отсутствии устройства. При отсутствии VXLAN сообщение не делает вывода об удалении: backend скрывает ошибки отдельных опросов. Исправление этого API и достоверность сетевых KPI — этап 9.
|
||||
|
||||
### Границы приёмки
|
||||
|
||||
HTTP-моки подтверждают UI и запросы; RouterOS и реальные права требуют отдельного окружения. Полная адаптивность профильных таблиц/форм не входит в эту приёмку: известная обрезка строк Ping-проб — этап 7, сетевые формы и действия — этап 9. Прежние lint-замечания общих компонентов перечислены в журнале; чистый lint всего проекта не заявляется. ReUI Frame/base-nova, установленные Alert, DataGrid, Command/Dialog и ServerRail сохранены; новых зависимостей и изменений vendored-компонентов нет.
|
||||
@@ -224,10 +224,10 @@ function mockOverlayItems(): OverlayItem[] {
|
||||
})),
|
||||
)
|
||||
const vx: OverlayItem[] = mockVxlan.map((t) => ({
|
||||
id: `vx-${t.id}`,
|
||||
id: JSON.stringify(["vxlan", t.serverId, t.id]),
|
||||
name: t.name,
|
||||
kind: "vxlan",
|
||||
href: "/vxlan",
|
||||
href: "/vxlan?server=" + encodeURIComponent(t.serverId) + "&tunnel=" + encodeURIComponent(t.id),
|
||||
status: t.status,
|
||||
}))
|
||||
return [...gre, ...wg, ...vx]
|
||||
@@ -282,7 +282,7 @@ export function useDashboardLive() {
|
||||
apiFetch<{ tunnels?: ApiGreTunnelRow[] }>("/api/filters/gre-tunnels"),
|
||||
listWireGuard(backendUrl),
|
||||
apiFetch<{ servers?: TrafficServerRow[] }>("/api/traffic/servers?range=1h"),
|
||||
apiFetch<{ tunnels?: Array<{ id: string; name: string; status: OverlayItem["status"] }> }>("/api/vxlan"),
|
||||
apiFetch<{ tunnels?: Array<{ id: string; serverId: string; name: string; status: OverlayItem["status"] }> }>("/api/vxlan"),
|
||||
])
|
||||
|
||||
if (!current()) return
|
||||
@@ -340,10 +340,10 @@ export function useDashboardLive() {
|
||||
const vxItems: OverlayItem[] =
|
||||
vxRes.status === "fulfilled"
|
||||
? (vxRes.value.tunnels ?? []).map((t) => ({
|
||||
id: `vx-${t.id}`,
|
||||
id: JSON.stringify(["vxlan", t.serverId, t.id]),
|
||||
name: t.name,
|
||||
kind: "vxlan" as const,
|
||||
href: "/vxlan",
|
||||
href: "/vxlan?server=" + encodeURIComponent(t.serverId) + "&tunnel=" + encodeURIComponent(t.id),
|
||||
status: t.status === "up" ? "up" : "down",
|
||||
}))
|
||||
: []
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
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: "vx-same", vni: Number(serverId) * 100 + (id === "other" ? 1 : 0), port: 0, dstPort: 8472, vtepIp: "192.0.2.1", remoteVteps: ["192.0.2.22"], l2mtu: 1500, arpProxy: false, macLearning: 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 (!["GET", "HEAD"].includes(request.method())) control.posts.push({ path, body: request.postDataJSON() })
|
||||
if (path === "/api/vxlan" && 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/vxlan" ? { tunnels }
|
||||
: path === "/api/filters/gre-tunnels" ? { 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("VXLAN: переход из очереди выбирает сервер и точный ID без записи", async ({ page }) => {
|
||||
const control = await setup(page, "/dashboard")
|
||||
const link = page.getByRole("region", { name: "Требует внимания", exact: true }).locator('a[href="/vxlan?server=2&tunnel=*%2F2"]')
|
||||
await expect(link).toBeVisible()
|
||||
await link.click()
|
||||
const context = page.getByRole("status", { name: "VXLAN по ссылке" })
|
||||
await expect(context).toContainText("Второй сервер")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
|
||||
await expect(page.getByRole("status", { name: "VXLAN по ссылке" })).toContainText("VNI 200")
|
||||
await page.screenshot({ path: "test-results/vxlan-link-desktop.png", animations: "disabled" })
|
||||
await page.getByRole("button", { name: "Действия: vx-same", exact: true }).click()
|
||||
await page.getByRole("menuitem", { name: "Экспорт .rsc", exact: true }).click()
|
||||
await expect(page.getByRole("dialog")).toContainText("Второй сервер")
|
||||
await expect(page.getByRole("dialog")).toContainText("vni=200")
|
||||
await page.keyboard.press("Escape")
|
||||
expect(control.posts).toEqual([])
|
||||
await context.getByRole("link", { name: "К дашборду", exact: true }).click()
|
||||
await expect(link).toBeVisible()
|
||||
})
|
||||
|
||||
test("VXLAN: прямая ссылка, сброс, назад, reload и ошибка обновления", async ({ page }) => {
|
||||
const control = await setup(page, "/vxlan?server=2&tunnel=*%2F2&keep=1")
|
||||
const context = page.getByRole("status", { name: "VXLAN по ссылке" })
|
||||
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("Список и сводка ограничены")
|
||||
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/vxlan-link-mobile.png", animations: "disabled" })
|
||||
})
|
||||
|
||||
test("VXLAN: ручной выбор сервера снимает выбор туннеля и сохраняет query", async ({ page }) => {
|
||||
const control = await setup(page, "/vxlan?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: "VXLAN по ссылке" })).toHaveCount(0)
|
||||
await page.goBack()
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
|
||||
await expect(page.getByRole("status", { name: "VXLAN по ссылке" })).toContainText("VNI 200")
|
||||
expect(control.posts).toEqual([])
|
||||
})
|
||||
|
||||
for (const query of ["server=missing&tunnel=*%2F2", "server=2&tunnel=missing", "tunnel=*%2F2"]) {
|
||||
test("VXLAN: нет подмены объекта " + query, async ({ page }) => {
|
||||
const control = await setup(page, "/vxlan?" + query)
|
||||
await expect(page.getByRole("status", { name: "VXLAN по ссылке" })).toContainText("Туннель не получен для указанного сервера")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
|
||||
expect(control.posts).toEqual([])
|
||||
})
|
||||
}
|
||||
for (const status of [403, 500]) {
|
||||
test("VXLAN: ошибка API " + status, async ({ page }) => {
|
||||
const control = await setup(page, "/vxlan?server=2&tunnel=*%2F2", status)
|
||||
await expect(page.getByRole("status", { name: "VXLAN по ссылке" })).toContainText("Не удалось обновить VXLAN")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
|
||||
expect(control.posts).toEqual([])
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user