fix(ui): открывать интерфейс WireGuard с контекстом сервера
This commit is contained in:
1 parent
67a152b7c8
commit
af84f0135d
7 files changed
+219
-29
No files matched your search
@@ -1,6 +1,8 @@
|
||||
"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 { PageHeader } from "@/components/page-header"
|
||||
import { servers as mockServers } from "@/lib/data"
|
||||
import type { Server } from "@/lib/data"
|
||||
@@ -169,17 +171,22 @@ function peerRowId(iface: WgIfaceWithServer, peer: WgIfaceWithServer["peers"][nu
|
||||
return peer.id ?? peer.rosId ?? `${iface.id}-${peer.publicKey}-${index}`
|
||||
}
|
||||
|
||||
export default function WireGuardPage() {
|
||||
function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterfacesHref, onSelectServer }: {
|
||||
selectedServerId: string
|
||||
selectedInterfaceId: string | null
|
||||
allInterfacesHref: string
|
||||
onSelectServer: (id: string) => void
|
||||
}) {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
|
||||
const [liveIfaces, setLiveIfaces] = useState<WgIfaceWithServer[]>([])
|
||||
const [liveServers, setLiveServers] = useState<Server[]>([])
|
||||
const [failures, setFailures] = useState<WgFailure[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadError, setLoadError] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID)
|
||||
const [workspaceTab, setWorkspaceTab] = useState<WgWorkspaceTab>("interfaces")
|
||||
const [statusFilter, setStatusFilter] = useState<WgStatusFilter>("all")
|
||||
const [search, setSearch] = useState("")
|
||||
@@ -212,6 +219,7 @@ export default function WireGuardPage() {
|
||||
setLiveIfaces(wg.interfaces.map(dtoToRow))
|
||||
setLiveServers(servers.filter((s) => s.enabled).map(mapBackendServer))
|
||||
setFailures(wg.failures ?? [])
|
||||
setLoadError(false)
|
||||
if (wg.failures?.length) {
|
||||
toast.warning(
|
||||
`Не удалось опросить: ${wg.failures.map((f) => f.serverName ?? f.serverId).join(", ")}`,
|
||||
@@ -219,8 +227,7 @@ export default function WireGuardPage() {
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Ошибка загрузки WireGuard")
|
||||
setLiveIfaces([])
|
||||
setFailures([])
|
||||
setLoadError(true)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -243,10 +250,8 @@ export default function WireGuardPage() {
|
||||
const displayIfaces = isLive ? liveIfaces : collectMockInterfaces()
|
||||
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 selectedSourceError = isLive && (loadError || failures.some(f => f.serverId === selectedServerId))
|
||||
|
||||
const historyServerId = effectiveServerId === ALL_SERVERS_ID ? null : effectiveServerId
|
||||
|
||||
@@ -287,9 +292,12 @@ export default function WireGuardPage() {
|
||||
}, [isLive, historyServerId, backendUrl, loadLive, loadRevisions])
|
||||
|
||||
const scopedIfaces = useMemo(() => {
|
||||
if (selectedInterfaceId !== null) return displayIfaces.filter(i => i.serverId === effectiveServerId && i.id === selectedInterfaceId)
|
||||
if (effectiveServerId === ALL_SERVERS_ID) return displayIfaces
|
||||
return displayIfaces.filter((i) => i.serverId === effectiveServerId)
|
||||
}, [displayIfaces, effectiveServerId])
|
||||
}, [displayIfaces, effectiveServerId, selectedInterfaceId])
|
||||
|
||||
const selectedInterface = selectedInterfaceId === null ? undefined : scopedIfaces[0]
|
||||
|
||||
const statusCounts = useMemo(() => {
|
||||
let up = 0
|
||||
@@ -458,7 +466,14 @@ export default function WireGuardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function sourceUnavailable(serverId: string | undefined) {
|
||||
const unavailable = isLive && (loadError || failures.some(f => f.serverId === serverId))
|
||||
if (unavailable) toast.error("Обновите данные сервера перед изменением объекта")
|
||||
return unavailable
|
||||
}
|
||||
|
||||
async function handleToggle(iface: WgIfaceWithServer) {
|
||||
if (sourceUnavailable(iface.serverId)) return
|
||||
if (!isLive || !iface.rosId) {
|
||||
toast.info("Доступно только в live-режиме")
|
||||
return
|
||||
@@ -476,6 +491,7 @@ export default function WireGuardPage() {
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!pendingDelete) return
|
||||
if (sourceUnavailable(pendingDelete.iface.serverId)) return
|
||||
if (!isLive) {
|
||||
toast.info("Доступно только в live-режиме")
|
||||
setPendingDelete(null)
|
||||
@@ -501,6 +517,7 @@ export default function WireGuardPage() {
|
||||
}
|
||||
|
||||
async function handleAddPeer(form: WgPeerFormState) {
|
||||
if (sourceUnavailable(peerIface?.serverId)) return
|
||||
if (!isLive || !peerIface) {
|
||||
toast.info("Доступно только в live-режиме")
|
||||
return
|
||||
@@ -577,7 +594,8 @@ export default function WireGuardPage() {
|
||||
<ServerRailLayout
|
||||
items={railItems}
|
||||
selectedId={effectiveServerId}
|
||||
onSelect={setSelectedServerId}
|
||||
preserveSelection
|
||||
onSelect={onSelectServer}
|
||||
showAll
|
||||
allCount={displayIfaces.length}
|
||||
loading={isLive && loading && displayServers.length === 0}
|
||||
@@ -627,6 +645,19 @@ export default function WireGuardPage() {
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
{isLive && loadError && selectedInterfaceId === null && <Alert variant="warning"><AlertTitle>Не удалось обновить WireGuard</AlertTitle><AlertDescription>Показаны последние полученные данные. Повторите обновление перед изменением объектов.</AlertDescription></Alert>}
|
||||
{selectedInterfaceId !== null && (
|
||||
<Alert variant={selectedSourceError || ((!isLive || !loading) && !selectedInterface) ? "warning" : "info"} role="status" aria-label="WireGuard по ссылке">
|
||||
<AlertTitle className="line-clamp-none wrap-anywhere">{selectedInterface ? "Выбран интерфейс: " + selectedInterface.name : "WireGuard по ссылке"}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p className="wrap-anywhere">{isLive && loading ? "Загрузка интерфейса…" : selectedSourceError ? "Не удалось получить данные выбранного сервера. Повторите обновление; сохранённые данные могут быть устаревшими." : selectedInterface ? selectedInterface.serverName + " · " + (selectedInterface.address ?? selectedInterface.id) + ". Список, пиры и сводка ограничены выбранным интерфейсом." : "Интерфейс не найден или недоступен на указанном сервере. Проверьте ссылку и доступ к устройству."}</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link href={allInterfacesHref} className="underline underline-offset-2">Все интерфейсы сервера</Link>
|
||||
<Link href="/dashboard" className="underline underline-offset-2">К дашборду</Link>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<KpiStatGrid
|
||||
aria-label="Сводка WireGuard"
|
||||
items={[
|
||||
@@ -723,10 +754,10 @@ export default function WireGuardPage() {
|
||||
compactServer={compactServer}
|
||||
emptyAction={emptyCreateAction}
|
||||
onExport={(iface) => openExport(iface, "rsc")}
|
||||
onAddPeer={setPeerIface}
|
||||
onToggle={handleToggle}
|
||||
onDelete={(iface) => setPendingDelete({ kind: "iface", iface })}
|
||||
onDeletePeer={(iface, peerId) => setPendingDelete({ kind: "peer", iface, peerId })}
|
||||
onAddPeer={selectedSourceError ? undefined : setPeerIface}
|
||||
onToggle={selectedSourceError ? undefined : handleToggle}
|
||||
onDelete={selectedSourceError ? undefined : (iface) => setPendingDelete({ kind: "iface", iface })}
|
||||
onDeletePeer={selectedSourceError ? undefined : (iface, peerId) => setPendingDelete({ kind: "peer", iface, peerId })}
|
||||
onExportPeer={(iface, peerId) => openExport(iface, "peer", peerId)}
|
||||
/>
|
||||
</DataPageCard>
|
||||
@@ -745,19 +776,19 @@ export default function WireGuardPage() {
|
||||
compactServer={compactServer}
|
||||
emptyAction={
|
||||
scopedIfaces.length === 1 ? (
|
||||
<Button size="sm" onClick={() => setPeerIface(scopedIfaces[0])}>
|
||||
<Button size="sm" disabled={selectedSourceError} onClick={() => setPeerIface(scopedIfaces[0])}>
|
||||
<PlusIcon className="size-4" />
|
||||
Добавить пира
|
||||
</Button>
|
||||
) : emptyCreateAction
|
||||
}
|
||||
onDeletePeer={(row) => {
|
||||
const iface = scopedIfaces.find((i) => i.id === row.ifaceId)
|
||||
onDeletePeer={selectedSourceError ? undefined : (row) => {
|
||||
const iface = scopedIfaces.find((i) => i.id === row.ifaceId && i.serverId === row.serverId)
|
||||
if (!iface) return
|
||||
setPendingDelete({ kind: "peer", iface, peerId: row.id })
|
||||
}}
|
||||
onExportPeer={(row) => {
|
||||
const iface = scopedIfaces.find((i) => i.id === row.ifaceId)
|
||||
const iface = scopedIfaces.find((i) => i.id === row.ifaceId && i.serverId === row.serverId)
|
||||
if (!iface) return
|
||||
openExport(iface, "peer", row.id)
|
||||
}}
|
||||
@@ -895,3 +926,23 @@ export default function WireGuardPage() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function WireGuardRoute() {
|
||||
const params = useSearchParams()
|
||||
const router = useRouter()
|
||||
const serverId = params.get("server") ?? ALL_SERVERS_ID
|
||||
const interfaceId = params.get("interface")
|
||||
const rest = new URLSearchParams(params.toString())
|
||||
rest.delete("interface")
|
||||
const allInterfacesHref = "/wireguard" + (rest.size ? "?" + rest.toString() : "")
|
||||
return <WireGuardPageContent key={JSON.stringify([serverId, interfaceId])} selectedServerId={serverId} selectedInterfaceId={interfaceId} allInterfacesHref={allInterfacesHref} onSelectServer={id => {
|
||||
const next = new URLSearchParams(rest)
|
||||
if (id === ALL_SERVERS_ID) next.delete("server")
|
||||
else next.set("server", id)
|
||||
router.push("/wireguard" + (next.size ? "?" + next.toString() : ""))
|
||||
}} />
|
||||
}
|
||||
|
||||
export default function WireGuardPage() {
|
||||
return <Suspense fallback={<div role="status">Загрузка WireGuard…</div>}><WireGuardRoute /></Suspense>
|
||||
}
|
||||
@@ -273,7 +273,7 @@ function WireguardDataGrid({
|
||||
manualPagination: true,
|
||||
data: interfaces,
|
||||
columns,
|
||||
getRowId: (row) => row.id,
|
||||
getRowId: (row) => JSON.stringify([row.serverId, row.id]),
|
||||
getRowCanExpand: () => true,
|
||||
})
|
||||
|
||||
@@ -293,7 +293,7 @@ function WireguardDataGrid({
|
||||
<DataGridShell
|
||||
table={table}
|
||||
recordCount={interfaces.length}
|
||||
onRowClick={(row) => table.getRow(row.id).toggleExpanded()}
|
||||
onRowClick={(row) => table.getRow(JSON.stringify([row.serverId, row.id])).toggleExpanded()}
|
||||
tableClassNames={{
|
||||
headerRow: "border-b border-border",
|
||||
bodyRow: cn("group/row", "[&[data-disabled=true]]:opacity-50"),
|
||||
|
||||
@@ -238,7 +238,7 @@ function WireguardPeersGrid({
|
||||
manualPagination: true,
|
||||
data: peers,
|
||||
columns,
|
||||
getRowId: (row) => row.id,
|
||||
getRowId: (row) => JSON.stringify([row.serverId, row.ifaceId, row.id]),
|
||||
})
|
||||
|
||||
if (peers.length === 0) {
|
||||
|
||||
+11
-1
@@ -20,7 +20,7 @@
|
||||
| 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже |
|
||||
| 4 | Остальные таблицы, удаление старых API, общая регрессия | Завершён 2026-09-20; границы приёмки ниже |
|
||||
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
||||
| 6 | Навигация и дашборд | В работе: готовы переходы к серверам, проверкам и GRE; остаются WireGuard/VXLAN |
|
||||
| 6 | Навигация и дашборд | В работе: готовы переходы к серверам, проверкам, GRE и WireGuard; остаётся VXLAN |
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат |
|
||||
| 8 | Трафик и статистика | Не начат |
|
||||
| 9 | VPN, Firewall и Filters | Не начат |
|
||||
@@ -447,6 +447,16 @@ Production build успешен: контракты, TypeScript, 34 страни
|
||||
|
||||
Квота: на старте использовано 69% пятичасовой, перед заключительными проверками 86% (недельной 45%). Следующий блок в этом окне не начинать. Сохранить локальным коммитом без push. **Следующий шаг: WireGuard, затем VXLAN** — URL с сервером и точным ID, запрет fallback на другой сервер, переиспользовать preserveSelection; проверить одинаковые имена/ID, прямой URL, Back, ошибки и отсутствие автоматических мутаций. Этап 6 остаётся открытым до завершения этих переходов и приёмки.
|
||||
|
||||
## Этап 6 — блок 7: переход к интерфейсу WireGuard (2026-09-20)
|
||||
|
||||
Готов `/wireguard?server=<serverId>&interface=<id>` из очереди проблем, включая встроенные моки. Выбирается точная пара; таблица, пиры и сводка ограничены интерфейсом. Alert показывает сервер и адрес, ошибки и отсутствие объекта. Снятие выбора сохраняет сервер и остальные query; смена сервера снимает interface, Back/reload восстанавливают выбор. Используется preserveSelection без подстановки другого сервера.
|
||||
|
||||
При ошибке HTTP последние данные сохраняются с предупреждением; изменяющие действия для устаревших интерфейсов/пиров блокируются. Частичные failures выбранного сервера показаны отдельно. Полная достоверность KPI при частичных сбоях и UX форм остаются этапом 9. Ключи строк интерфейсов включают сервер, пиров — сервер/интерфейс; раскрытие строк и поиск интерфейса для действий пира используют тот же контекст.
|
||||
|
||||
Проверки: 9 новых сценариев wireguard-links и 2 прежних wireguard-tables passed; 6 сценариев dashboard-data passed. Проверены одинаковые ID/имена, отдельные строки и раскрытие, вкладка пиров, URL/Back/reload, 403/500/503, частичный сбой, восстановление и отсутствие запросов записи. Мобильный контекст 390 px просмотрен. ESLint всех изменённых TS-файлов успешен; production build/TypeScript, 34 страницы — успешно. Реальный RouterOS не проверен, используются HTTP-моки по схеме контрактов.
|
||||
|
||||
Квота: 0% использовано на старте нового окна, 25% перед сборкой (недельной 51%). Блок сохранить локальным коммитом без push. Следующий шаг — VXLAN и итоговая приёмка переходов этапа 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> и /gre?server=<serverId>&tunnel=<id>. Идентификаторы кодируются encodeURIComponent, целевые страницы выбирают точный ID (для GRE — пару сервер/туннель) и сообщают об отсутствии/ошибке загрузки. Снятие выбора сохраняет остальные query-параметры. GRE использует preserveSelection серверной панели, чтобы не сбрасывать URL при загрузке/отсутствии устройства. Ещё не реализованы ссылки на конкретные WireGuard/VXLAN; для них нужен контекст сервера вместе с ID объекта.
|
||||
Готовы и проверены: /servers?server=<id>, /uptime?probe=<id>, /gre?server=<serverId>&tunnel=<id>, /wireguard?server=<serverId>&interface=<id>. Идентификаторы кодируются encodeURIComponent, целевые страницы выбирают точный ID (для туннелей — пару сервер/объект) и сообщают об отсутствии/ошибке загрузки. Снятие выбора сохраняет остальные query-параметры. GRE и WireGuard используют preserveSelection серверной панели, чтобы не сбрасывать URL при загрузке/отсутствии устройства. Ещё не реализованы ссылки на конкретные VXLAN.
|
||||
@@ -216,10 +216,10 @@ function mockOverlayItems(): OverlayItem[] {
|
||||
}))
|
||||
const wg: OverlayItem[] = mockServers.flatMap((s) =>
|
||||
(s.wireGuardIfaces ?? []).map((iface) => ({
|
||||
id: `wg-${iface.id}`,
|
||||
id: JSON.stringify(["wg", s.id, iface.id]),
|
||||
name: iface.name,
|
||||
kind: "wg" as const,
|
||||
href: "/wireguard",
|
||||
href: "/wireguard?server=" + encodeURIComponent(s.id) + "&interface=" + encodeURIComponent(iface.id),
|
||||
status: iface.status,
|
||||
})),
|
||||
)
|
||||
@@ -330,10 +330,10 @@ export function useDashboardLive() {
|
||||
const wgItems: OverlayItem[] =
|
||||
wgRes.status === "fulfilled"
|
||||
? wgRes.value.interfaces.map((iface) => ({
|
||||
id: `wg-${iface.id}`,
|
||||
id: JSON.stringify(["wg", iface.serverId, iface.id]),
|
||||
name: iface.name,
|
||||
kind: "wg" as const,
|
||||
href: "/wireguard",
|
||||
href: "/wireguard?server=" + encodeURIComponent(iface.serverId) + "&interface=" + encodeURIComponent(iface.id),
|
||||
status: iface.status,
|
||||
}))
|
||||
: []
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { test, expect, type Page } from "@playwright/test"
|
||||
import { wgListResponseSchema } from "../../packages/contracts/src/wireguard"
|
||||
|
||||
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 interfaces = wgListResponseSchema.parse({ interfaces: ["1", "2"].flatMap(serverId => ["*/2", "other"].map(id => ({ id, rosId: id, serverId, serverName: serverId === "1" ? "Источник" : "Второй сервер", name: "wg-same", address: "192.0.2." + (serverId === "1" ? "11" : "22"), listenPort: 13231, mtu: 1420, comment: "", enabled: true, status: "down", peers: [{ id: "peer", rosId: "*P", publicKey: "fixture-key", allowedIps: [serverId === "1" ? "10.0.1.1/32" : "10.0.2.1/32"] }] }))) }).interfaces
|
||||
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/wireguard" && 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/wireguard" ? { interfaces: control.partial ? interfaces.filter(t => t.serverId !== "2") : interfaces, failures: control.partial ? [{ serverId: "2", error: "Unavailable" }] : [] }
|
||||
: path === "/api/filters/gre-tunnels" ? { tunnels: [] }
|
||||
: path === "/api/vxlan" ? { tunnels: [] }
|
||||
: 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("WireGuard: переход из очереди выбирает сервер и точный ID без записи", async ({ page }) => {
|
||||
const control = await setup(page, "/dashboard")
|
||||
const link = page.getByRole("region", { name: "Требует внимания", exact: true }).locator('a[href="/wireguard?server=2&interface=*%2F2"]')
|
||||
await expect(link).toBeVisible()
|
||||
await link.click()
|
||||
const context = page.getByRole("status", { name: "WireGuard по ссылке" })
|
||||
await expect(context).toContainText("Второй сервер")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
|
||||
await expect(page.getByRole("status", { name: "WireGuard по ссылке" })).toContainText("192.0.2.22")
|
||||
await page.getByRole("tab", { name: /Пиры/ }).click()
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
|
||||
await expect(page.locator("tr[data-row-id]")).toContainText("10.0.2.1/32")
|
||||
expect(control.posts).toEqual([])
|
||||
await context.getByRole("link", { name: "К дашборду", exact: true }).click()
|
||||
await expect(link).toBeVisible()
|
||||
})
|
||||
|
||||
test("WireGuard: прямая ссылка, сброс, назад, reload и ошибка обновления", async ({ page }) => {
|
||||
const control = await setup(page, "/wireguard?server=2&interface=*%2F2&keep=1")
|
||||
const context = page.getByRole("status", { name: "WireGuard по ссылке" })
|
||||
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)
|
||||
await page.locator("tr[data-row-id]").getByRole("button", { name: "Действия: wg-same" }).click()
|
||||
await expect(page.getByRole("menuitem", { name: /Удалить/ })).toHaveCount(0)
|
||||
await page.keyboard.press("Escape")
|
||||
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/wireguard-link-mobile.png", animations: "disabled" })
|
||||
})
|
||||
|
||||
test("WireGuard: ручной выбор сервера снимает выбор туннеля и сохраняет query", async ({ page }) => {
|
||||
const control = await setup(page, "/wireguard?server=2&interface=*%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: "WireGuard по ссылке" })).toHaveCount(0)
|
||||
await page.goBack()
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
|
||||
await expect(page.getByRole("status", { name: "WireGuard по ссылке" })).toContainText("192.0.2.22")
|
||||
expect(control.posts).toEqual([])
|
||||
})
|
||||
|
||||
test("WireGuard: одинаковые ID сохраняют отдельные строки интерфейсов и пиров", async ({ page }) => {
|
||||
await setup(page, "/wireguard")
|
||||
const rows = page.locator("tr[data-row-id]")
|
||||
await expect(rows).toHaveCount(4)
|
||||
expect(new Set(await rows.evaluateAll(elements => elements.map(el => el.getAttribute("data-row-id")))).size).toBe(4)
|
||||
await rows.nth(2).click()
|
||||
await expect(page.getByText("10.0.2.1/32", { exact: true })).toBeVisible()
|
||||
await expect(page.getByText("10.0.1.1/32", { exact: true })).toHaveCount(0)
|
||||
await page.getByRole("tab", { name: /Пиры/ }).click()
|
||||
await expect(rows).toHaveCount(4)
|
||||
expect(new Set(await rows.evaluateAll(elements => elements.map(el => el.getAttribute("data-row-id")))).size).toBe(4)
|
||||
})
|
||||
|
||||
for (const query of ["server=missing&interface=*%2F2", "server=2&interface=missing", "interface=*%2F2"]) {
|
||||
test("WireGuard: нет подмены объекта " + query, async ({ page }) => {
|
||||
const control = await setup(page, "/wireguard?" + query)
|
||||
await expect(page.getByRole("status", { name: "WireGuard по ссылке" })).toContainText("Интерфейс не найден или недоступен")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
|
||||
expect(control.posts).toEqual([])
|
||||
})
|
||||
}
|
||||
for (const status of [403, 500]) {
|
||||
test("WireGuard: ошибка API " + status, async ({ page }) => {
|
||||
const control = await setup(page, "/wireguard?server=2&interface=*%2F2", status)
|
||||
await expect(page.getByRole("status", { name: "WireGuard по ссылке" })).toContainText("Не удалось получить данные выбранного сервера")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
|
||||
expect(control.posts).toEqual([])
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user