diff --git a/app/(main)/wireguard/page.tsx b/app/(main)/wireguard/page.tsx index dc9acc75..91160dcc 100644 --- a/app/(main)/wireguard/page.tsx +++ b/app/(main)/wireguard/page.tsx @@ -3,6 +3,8 @@ import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react" import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" +import { useWireGuardUrlState } from "@/hooks/use-wireguard-url-state" +import type { WireGuardTab as WgWorkspaceTab } from "@/lib/wireguard-url-state" import { PageHeader } from "@/components/page-header" import { servers as mockServers } from "@/lib/data" import type { Server } from "@/lib/data" @@ -69,8 +71,6 @@ import { Trash2Icon, CodeXmlIcon, AlertCircleIcon, HistoryIcon, } from "lucide-react" -type WgWorkspaceTab = "interfaces" | "peers" | "cli" -type WgStatusFilter = "all" | "up" | "down" type WgFailure = { serverId: string; serverName?: string; error: string } type PendingDelete = | { kind: "iface"; iface: WgIfaceWithServer } @@ -192,9 +192,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf const [togglePending, setTogglePending] = useState(null) const [toggleError, setToggleError] = useState(null) - const [workspaceTab, setWorkspaceTab] = useState("interfaces") - const [statusFilter, setStatusFilter] = useState("all") - const [search, setSearch] = useState("") + const { tab: workspaceTab, setTab: setWorkspaceTab, status: statusFilter, setStatus: setStatusFilter, search, setSearch, error: urlError, resetFilters } = useWireGuardUrlState() const [createOpen, setCreateOpen] = useState(false) const [importOpen, setImportOpen] = useState(false) const formLock = useRef(false) @@ -271,7 +269,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf const displayServers = isLive ? liveServers : mockServers.filter((s) => s.enabled) const effectiveServerId = selectedServerId - const selectedSourceError = isLive && (loadError || failures.some(f => f.serverId === selectedServerId)) + const selectedSourceError = !!urlError || isLive && (loadError || failures.some(f => f.serverId === selectedServerId)) const historyServerId = effectiveServerId === ALL_SERVERS_ID ? null : effectiveServerId @@ -315,10 +313,11 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf }, [isLive, historyServerId, backendUrl, loadLive, loadRevisions]) const scopedIfaces = useMemo(() => { + if (urlError) return [] 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, selectedInterfaceId]) + }, [displayIfaces, effectiveServerId, selectedInterfaceId, urlError]) const selectedInterface = selectedInterfaceId === null ? undefined : scopedIfaces[0] @@ -418,7 +417,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf }, [displayServers, displayIfaces]) async function handleCreate(form: WgCreateFormState) { - if (formLock.current) return + if (urlError || formLock.current) return if (!isLive) { toast.info("Создание на роутер доступно только в live-режиме") return @@ -502,7 +501,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf } function sourceUnavailable(serverId: string | undefined) { - const unavailable = isLive && (loadError || failures.some(f => f.serverId === serverId)) + const unavailable = !!urlError || isLive && (loadError || failures.some(f => f.serverId === serverId)) if (unavailable) toast.error("Обновите данные сервера перед изменением объекта") return unavailable } @@ -652,7 +651,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf } const emptyCreateAction = ( - @@ -700,11 +699,11 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf )} - - @@ -714,6 +713,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf } >
+ {urlError && Проверьте ссылку

{urlError}

} {isLive && loadError && selectedInterfaceId === null && Не удалось обновить WireGuardПоказаны последние полученные данные. Повторите обновление перед изменением объектов.} {selectedInterfaceId !== null && ( diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 7158943b..f4ded251 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-04 -Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. 12.3.18 принят локально:users1000/поиск всего списка/детали и ошибка refresh/32 browser/build. 12.3.19 принят локально:backups1000/все мобильные действия/возврат focus/39+4 browser/build. 12.3.20 принят локально:независимые speed GET/cache/freshness/58+4 browser/build. 12.3.21 принят локально:strict history DTO/null rates от DB до UI/60+5 browser/unit4/builds. 12.4.3 принят локально:users/backups delta context36 на a558412. 12.3.22 принят локально:shared speed-probes DTO/context8/60+6+47 browser/unit5/builds. 12.3.23 принят локально:strict write DTO/atomic replacement/confirmed scheduler warning/65 browser/HTTP DI JWT/builds. 12.3.24 принят локально:OSPF routes1000/50 DOM/page20/cache/320px/22+1 browser/build. 12.3.25 принят локально:BFD1000/50 DOM/cache/320px/12 browser/build. 12.4.4 принят локально:context213/25 файлов и unit36 прошли на5b2a291. 12.4.5 принят локально:main1110/171 прошли на21859be. 12.3.26 принят локально:strict atomic Ping write/подтверждение каталога и сохранения/история удаления/62 browser/context11/DI6/builds. 12.3.27 принят локально:WireGuard zero/missing counters/UI/backend mapper/29 browser/unit/builds. 12.3.28 принят локально:WireGuard interfaces1000/50 DOM/pinning/actions/cache/29+1 browser/build. 12.3.29 принят локально:WG peers1000/50 DOM/pinning/точный delete target/29+3 browser/build. 12.3.30 принят локально:WG detail1000/50 DOM/320px/клавиатура/31+1 browser/build. 12.3.31 принят локально:WG handshake без ложного online/unknown/7+1 browser/build. Следующий блок12.3.32: WireGuard URL search/tab/status; далее network grids и полная main/context регрессия.12.5 отдельно. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. +Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. 12.3.18 принят локально:users1000/поиск всего списка/детали и ошибка refresh/32 browser/build. 12.3.19 принят локально:backups1000/все мобильные действия/возврат focus/39+4 browser/build. 12.3.20 принят локально:независимые speed GET/cache/freshness/58+4 browser/build. 12.3.21 принят локально:strict history DTO/null rates от DB до UI/60+5 browser/unit4/builds. 12.4.3 принят локально:users/backups delta context36 на a558412. 12.3.22 принят локально:shared speed-probes DTO/context8/60+6+47 browser/unit5/builds. 12.3.23 принят локально:strict write DTO/atomic replacement/confirmed scheduler warning/65 browser/HTTP DI JWT/builds. 12.3.24 принят локально:OSPF routes1000/50 DOM/page20/cache/320px/22+1 browser/build. 12.3.25 принят локально:BFD1000/50 DOM/cache/320px/12 browser/build. 12.4.4 принят локально:context213/25 файлов и unit36 прошли на5b2a291. 12.4.5 принят локально:main1110/171 прошли на21859be. 12.3.26 принят локально:strict atomic Ping write/подтверждение каталога и сохранения/история удаления/62 browser/context11/DI6/builds. 12.3.27 принят локально:WireGuard zero/missing counters/UI/backend mapper/29 browser/unit/builds. 12.3.28 принят локально:WireGuard interfaces1000/50 DOM/pinning/actions/cache/29+1 browser/build. 12.3.29 принят локально:WG peers1000/50 DOM/pinning/точный delete target/29+3 browser/build. 12.3.30 принят локально:WG detail1000/50 DOM/320px/клавиатура/31+1 browser/build. 12.3.31 принят локально:WG handshake без ложного online/unknown/7+1 browser/build. 12.3.32 принят локально:WG URL search/tab/status/reload/история/34+4 browser/unit/build. Следующий блок12.3.33: WireGuard read/cache/source context; далее network grids и полная main/context регрессия.12.5 отдельно. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index c0368766..bb1d6df6 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -36,6 +36,14 @@ +## 12.3.32 — контекст фильтров WireGuard в URL (2026-10-04) + +Поиск q, вкладка tab и статус интерфейса status сохраняются в URL; reload/назад/вперёд восстанавливают срез. Native history updates сохраняют server/interface, посторонние параметры и hash; ввод поиска использует replace, смена вкладки/статуса — push. Статус относится к интерфейсам, peer view сохраняет его для возврата. Неверные/повторяющиеся фильтры и поиск длиннее200 не расширяют список: показываются предупреждение и явный сброс только фильтров с сохранением устройства. Создание/импорт заблокированы до исправления ссылки. + +Проверки:34 browser профильных/large/URL прошли; четыре новых invalid-filter сценария выявили пропущенную блокировку header-кнопки, после исправления повтор4/4. Pure URL1/1, TypeScript без incremental/ESLint и production frontend build прошли. Профили1000 интерфейсов/пиров/деталей включены. DTO server/interface context не расширен и его legacy parse не заявлен новым strict контрактом. Общие правила ReUI/base-nova сохранены. + +Следующий блок12.3.33: WireGuard чтение при смене backend/mode и поздних ответах, первоначально неизвестный каталог/KPI, cache/freshness и запрет изменения неизвестного источника. Затем оставшиеся17 specialized grids. Общая main/context после delta и внешняя12.5 остаются. Коммит локальный без push. + ## 12.3.31 — рукопожатие не подменяет доступность WireGuard (2026-10-04) Убрана оценка «пиров онлайн» по presence старого last-handshake. KPI «Данные рукопожатия» считает непустые сведения в ответе и явно сообщает, что доступность пиров не проверена. Колонка «Пиры» показывает число записей. Старое время рукопожатия отображается без зелёного вывода о доступности; отсутствующее/пустое значение — «Нет данных». В мобильных деталях видна подпись Endpoint. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index cd8f0098..7952e2ab 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -434,3 +434,12 @@ Peer grid теперь использует client pagination50, clamp и сох Следующий блок12.3.32: WireGuard URL search/tab/status — reload/история/сохранение server/interface/неверные параметры без расширения среза. Первоначально неизвестные общие KPI WireGuard и freshness/source switching требуют отдельного аудита; этот блок исправляет именно смысл handshake.17 остальных specialized grids/общая регрессия после delta/внешняя12.5 остаются. Коммит локальный без push. + +## 12.3.32 — контекст фильтров WireGuard в URL (2026-10-04) + +Поиск q, вкладка tab и статус интерфейса status сохраняются в URL; reload/назад/вперёд восстанавливают срез. Native history updates сохраняют server/interface, посторонние параметры и hash; ввод поиска использует replace, смена вкладки/статуса — push. Статус относится к интерфейсам, peer view сохраняет его для возврата. Неверные/повторяющиеся фильтры и поиск длиннее200 не расширяют список: показываются предупреждение и явный сброс только фильтров с сохранением устройства. Создание/импорт заблокированы до исправления ссылки. + +Проверки:34 browser профильных/large/URL прошли; четыре новых invalid-filter сценария выявили пропущенную блокировку header-кнопки, после исправления повтор4/4. Pure URL1/1, TypeScript без incremental/ESLint и production frontend build прошли. Профили1000 интерфейсов/пиров/деталей включены. DTO server/interface context не расширен и его legacy parse не заявлен новым strict контрактом. Общие правила ReUI/base-nova сохранены. + +Следующий блок12.3.33: WireGuard чтение при смене backend/mode и поздних ответах, первоначально неизвестный каталог/KPI, cache/freshness и запрет изменения неизвестного источника. Затем оставшиеся17 specialized grids. Общая main/context после delta и внешняя12.5 остаются. Коммит локальный без push. + diff --git a/hooks/use-wireguard-url-state.ts b/hooks/use-wireguard-url-state.ts new file mode 100644 index 00000000..50e54e41 --- /dev/null +++ b/hooks/use-wireguard-url-state.ts @@ -0,0 +1,18 @@ +"use client" +import { useMemo } from "react" +import { useSearchParams } from "next/navigation" +import { readWireGuardUrl, patchWireGuardUrl, type WireGuardTab, type WireGuardStatus } from "@/lib/wireguard-url-state" +export function useWireGuardUrlState() { + const raw = useSearchParams().toString() + const state = useMemo(() => readWireGuardUrl(new URLSearchParams(raw)), [raw]) + function write(changes: Record, replace = false) { + const href = patchWireGuardUrl(new URLSearchParams(window.location.search), changes) + window.location.hash + if (href === window.location.pathname + window.location.search + window.location.hash) return + if (replace) window.history.replaceState(null, "", href) + else window.history.pushState(null, "", href) + } + return { ...state, setTab: (tab: WireGuardTab) => write({tab:tab === "interfaces" ? null : tab}), + setSearch: (q: string) => write({q}, true), setStatus: (status: WireGuardStatus) => write({status:status === "all" ? null : status}), + resetFilters: () => write({q:null,status:null,tab:null}), + } +} diff --git a/lib/wireguard-url-state.test.ts b/lib/wireguard-url-state.test.ts new file mode 100644 index 00000000..3e88ea29 --- /dev/null +++ b/lib/wireguard-url-state.test.ts @@ -0,0 +1,11 @@ +import { test } from "node:test" +import assert from "node:assert/strict" +import { readWireGuardUrl, patchWireGuardUrl } from "./wireguard-url-state" +test("WireGuard URL keeps context and validates all owned filters", () => { + assert.deepEqual(readWireGuardUrl(new URLSearchParams()), {tab:"interfaces",status:"all",search:"",error:null}) + const params = new URLSearchParams("server=2&interface=*%2F2&keep=1&tab=peers&status=down&q=key%2B1") + assert.deepEqual(readWireGuardUrl(params), {tab:"peers",status:"down",search:"key+1",error:null}) + for (const raw of ["tab=bad", "status=bad", "q="+"a".repeat(201), "q=a&q=b", "tab=peers&tab=cli", "status=up&status=down"]) assert.ok(readWireGuardUrl(new URLSearchParams(raw)).error) + const next=new URL(patchWireGuardUrl(params,{q:null,status:null,tab:null}),"https://test.local") + assert.equal(next.searchParams.get("server"),"2");assert.equal(next.searchParams.get("interface"),"*/2");assert.equal(next.searchParams.get("keep"),"1");assert.equal(next.searchParams.has("q"),false) +}) diff --git a/lib/wireguard-url-state.ts b/lib/wireguard-url-state.ts new file mode 100644 index 00000000..7f42fec9 --- /dev/null +++ b/lib/wireguard-url-state.ts @@ -0,0 +1,18 @@ +export type WireGuardTab = "interfaces" | "peers" | "cli" +export type WireGuardStatus = "all" | "up" | "down" +const tabs = ["interfaces", "peers", "cli"] +const statuses = ["all", "up", "down"] +export function readWireGuardUrl(params: URLSearchParams) { + const tab = params.get("tab") ?? "interfaces", status = params.get("status") ?? "all", search = params.get("q") ?? "" + let error: string | null = null + if (["tab", "status", "q"].some(key => params.getAll(key).length > 1)) error = "В ссылке повторяются фильтры WireGuard." + else if (!tabs.includes(tab)) error = "В ссылке указана неизвестная вкладка WireGuard." + else if (!statuses.includes(status)) error = "В ссылке указан неизвестный статус WireGuard." + else if (search.length > 200) error = "Поиск WireGuard в ссылке слишком длинный." + return { tab: (tabs.includes(tab) ? tab : "interfaces") as WireGuardTab, status: (statuses.includes(status) ? status : "all") as WireGuardStatus, search, error } +} +export function patchWireGuardUrl(params: URLSearchParams, changes: Record) { + const next = new URLSearchParams(params) + for (const [key, value] of Object.entries(changes)) if (!value) next.delete(key); else next.set(key, value) + return "/wireguard" + (next.size ? "?" + next.toString() : "") +} diff --git a/tests/e2e/wireguard-links.spec.ts b/tests/e2e/wireguard-links.spec.ts index 479c0fb5..6cd2c9f1 100644 --- a/tests/e2e/wireguard-links.spec.ts +++ b/tests/e2e/wireguard-links.spec.ts @@ -527,3 +527,29 @@ for (const oldFails of [false, true]) { await expect(sheet.getByRole("button", { name: "Откатить", exact: true })).toBeVisible() }) } + +test("WireGuard URL filters: search/tab/status reload and history preserve selected device", async ({ page }) => { + const control=await setup(page,"/wireguard?server=2&interface=*%2F2&keep=1&status=down&q=wg-same#context") + const rows=page.locator("tr[data-row-id]") + await expect(rows).toHaveCount(1);await expect(page.getByPlaceholder("Поиск по имени, серверу, IP…")).toHaveValue("wg-same") + await page.getByRole("tab",{name:/Пиры/}).click() + await expect(page).toHaveURL(url=>url.searchParams.get("tab")==="peers"&&url.searchParams.get("status")==="down"&&url.hash==="#context") + const search=page.getByPlaceholder("Поиск по ключу, IP, endpoint…");await search.fill("10.0.2.1") + await expect(page).toHaveURL(url=>url.searchParams.get("q")==="10.0.2.1"&&url.searchParams.get("server")==="2"&&url.searchParams.get("interface")==="*/2") + await page.reload();await expect(search).toHaveValue("10.0.2.1");await expect(rows).toHaveCount(1) + await page.getByRole("tab",{name:/Интерфейсы/}).click();await expect(rows).toHaveCount(1) + await page.goBack();await expect(page.getByRole("tab",{name:/Пиры/})).toHaveAttribute("aria-selected","true") + await page.goForward();await page.getByRole("button",{name:/^UP/}).click();await expect(rows).toHaveCount(0) + await expect(page).toHaveURL(url=>url.searchParams.get("status")==="up"&&url.searchParams.get("keep")==="1") + await page.reload();await expect(rows).toHaveCount(0);expect(control.posts).toEqual([]) +}) +for(const invalid of ["tab=bad","status=bad","q=a&q=b","tab=peers&tab=cli"])test("WireGuard URL rejects invalid filters without widening/"+invalid,async({page})=>{ + const control=await setup(page,"/wireguard?server=2&interface=*%2F2&keep=1&"+invalid) + const warning=page.getByRole("alert",{name:"Фильтры WireGuard",exact:true}) + await expect(warning).toBeVisible();await expect(page.locator("tr[data-row-id]")).toHaveCount(0) + await expect(page.getByRole("button",{name:"Новый интерфейс",exact:true}).first()).toBeDisabled() + await warning.getByRole("button",{name:"Сбросить фильтры",exact:true}).click() + await expect(warning).toHaveCount(0);await expect(page.locator("tr[data-row-id]")).toHaveCount(1) + await expect(page).toHaveURL(url=>url.searchParams.get("server")==="2"&&url.searchParams.get("interface")==="*/2"&&url.searchParams.get("keep")==="1") + expect(control.posts).toEqual([]) +})