fix(ui): проверять данные загрузки Filters
This commit is contained in:
1 parent
1fba16e1f2
commit
1c7ffa07a8
6 files changed
+143
-44
No files matched your search
+46
-42
@@ -14,6 +14,8 @@ import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { filtersResponseSchema, filtersMutationSchema } from "@/lib/filters-response"
|
||||
import { firewallServersSchema } from "@/lib/firewall-response"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import {
|
||||
servers, greTunnels, serverFilterRulesets, filters as mockFiltersCatalog,
|
||||
@@ -1248,27 +1250,6 @@ function CopyRulesSheet({
|
||||
|
||||
// ── page ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface BackendServer {
|
||||
id: number
|
||||
name: string
|
||||
host: string
|
||||
type: "jump-host" | "exit-node" | "home-router"
|
||||
site: string
|
||||
country: string
|
||||
asn: string
|
||||
enabled: boolean
|
||||
status: "online" | "offline" | null
|
||||
latency: number | null
|
||||
}
|
||||
|
||||
interface LiveFiltersResponse {
|
||||
rulesets: ServerFilterRuleset[]
|
||||
greTunnels: GreTunnel[]
|
||||
live?: boolean
|
||||
stale?: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
function buildRulesets(serverList: Server[], sourceRulesets: ServerFilterRuleset[]): ServerFilterRuleset[] {
|
||||
return serverList.map(s => ({
|
||||
serverId: s.id,
|
||||
@@ -1318,12 +1299,15 @@ export default function FiltersPage() {
|
||||
const [greByServer, setGreByServer] = useState<Record<string, GreTunnel[]>>({})
|
||||
const [recRoutesByServer, setRecRoutesByServer] = useState<Record<string, RecursiveRouteLite[]>>({})
|
||||
const [rulesets, setRulesets] = useState<ServerFilterRuleset[]>(
|
||||
() => buildRulesets(servers, serverFilterRulesets),
|
||||
() => isLive ? [] : buildRulesets(servers, serverFilterRulesets),
|
||||
)
|
||||
const [liveLoadState, setLiveLoadState] = useState<"idle" | "loading" | "error">(
|
||||
() => (mode === "live" ? "loading" : "idle"),
|
||||
)
|
||||
|
||||
const [liveLoadError, setLiveLoadError] = useState<string | null>(null)
|
||||
const [loadAttempt, setLoadAttempt] = useState(0)
|
||||
|
||||
const ensureGreTunnels = useCallback(async (serverId: string) => {
|
||||
if (!isLive) return
|
||||
try {
|
||||
@@ -1386,12 +1370,21 @@ export default function FiltersPage() {
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
setLiveLoadState("loading")
|
||||
setLiveLoadError(null)
|
||||
Promise.all([
|
||||
apiFetch<BackendServer[]>("/api/servers"),
|
||||
apiFetch<LiveFiltersResponse>("/api/filters/rules"),
|
||||
apiFetch<unknown>("/api/servers"),
|
||||
apiFetch<unknown>("/api/filters/rules"),
|
||||
])
|
||||
.then(([backendServers, liveFilters]) => {
|
||||
.then(([serverPayload, filtersPayload]) => {
|
||||
if (cancelled) return
|
||||
const serverResult = firewallServersSchema.safeParse(serverPayload)
|
||||
const filtersResult = filtersResponseSchema.safeParse(filtersPayload)
|
||||
if (!serverResult.success || !filtersResult.success) throw new Error("Сервер вернул некорректные данные Filters")
|
||||
const backendServers = serverResult.data, liveFilters = filtersResult.data
|
||||
if (liveFilters.stale) throw new Error("Актуальные правила недоступны")
|
||||
if (backendServers.some(server => server.enabled && !liveFilters.rulesets.some(rs => rs.serverId === String(server.id)))) throw new Error("Сервер не вернул наборы правил всех устройств")
|
||||
const mappedServers: Server[] = backendServers.map(s => ({
|
||||
id: String(s.id),
|
||||
name: s.name || s.host,
|
||||
@@ -1416,21 +1409,23 @@ export default function FiltersPage() {
|
||||
))
|
||||
setLiveLoadState("idle")
|
||||
})
|
||||
.catch(() => {
|
||||
setLiveServers([])
|
||||
setGreByServer({})
|
||||
setRulesets(buildRulesets(servers, serverFilterRulesets))
|
||||
setSelectedServerId(servers[0]?.id ?? "")
|
||||
.catch(error => {
|
||||
if (cancelled) return
|
||||
setLiveLoadError(error instanceof Error ? error.message : "Не удалось загрузить Filters")
|
||||
setLiveLoadState("error")
|
||||
})
|
||||
}, [isLive, apiFetch])
|
||||
return () => { cancelled = true }
|
||||
}, [isLive, apiFetch, loadAttempt])
|
||||
|
||||
const loadLiveRules = useCallback(async (serverId: string) => {
|
||||
if (!isLive || !serverId) return false
|
||||
try {
|
||||
const fresh = await apiFetch<LiveFiltersResponse>(
|
||||
const payload = await apiFetch<unknown>(
|
||||
`/api/filters/rules?serverId=${encodeURIComponent(serverId)}`,
|
||||
)
|
||||
const parsed = filtersResponseSchema.safeParse(payload)
|
||||
if (!parsed.success) throw new Error("Сервер вернул некорректные правила Filters")
|
||||
const fresh = parsed.data
|
||||
const ruleset = fresh.rulesets.find(r => r.serverId === serverId)
|
||||
if (!ruleset || fresh.stale) throw new Error("Не удалось получить актуальные правила выбранного устройства")
|
||||
const liveRules = ruleset.rules
|
||||
@@ -1440,9 +1435,6 @@ export default function FiltersPage() {
|
||||
return prev.map((rs) => rs.serverId === serverId ? { ...rs, rules: liveRules } : rs)
|
||||
})
|
||||
setLiveStale(Boolean(fresh.stale))
|
||||
if (fresh.greTunnels?.length) {
|
||||
setGreByServer((prev) => ({ ...prev, [serverId]: fresh.greTunnels }))
|
||||
}
|
||||
return true
|
||||
} catch (err) {
|
||||
setLiveStale(true)
|
||||
@@ -1461,8 +1453,8 @@ export default function FiltersPage() {
|
||||
])
|
||||
}, [isLive, selectedServerId, liveLoadState, liveServers, ensureGreTunnels, ensureRecursiveRoutes, loadLiveRules])
|
||||
|
||||
const allServers = !isLive || liveLoadState === "error" ? servers : liveServers
|
||||
const allTunnels = !isLive || liveLoadState === "error" ? greTunnels : (greByServer[selectedServerId] ?? [])
|
||||
const allServers = !isLive ? servers : liveServers
|
||||
const allTunnels = !isLive ? greTunnels : (greByServer[selectedServerId] ?? [])
|
||||
const selectedServer = allServers.find(s => s.id === selectedServerId) ?? allServers[0]
|
||||
const totalRules = rulesets.reduce((s, r) => s + r.rules.length, 0)
|
||||
const mutationsLocked = movePending || (isLive && (applyBusy || historyRestoring || liveStale || liveLoadState !== "idle"))
|
||||
@@ -1525,11 +1517,13 @@ export default function FiltersPage() {
|
||||
applyLock.current = true
|
||||
setApplyBusy(true)
|
||||
try {
|
||||
const res = await apiFetch<{ ok: boolean; rules?: FilterRule[] }>("/api/filters/rules", {
|
||||
const payload = await apiFetch<unknown>("/api/filters/rules", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ serverId, rules: nextRules, source }),
|
||||
})
|
||||
if (res?.ok !== true) throw new Error("Сервер не подтвердил применение правил")
|
||||
const parsed = filtersMutationSchema.safeParse(payload)
|
||||
if (!parsed.success) throw new Error("Сервер не подтвердил применение правил")
|
||||
const res = parsed.data
|
||||
setRulesets(p => p.map(rs => rs.serverId === serverId ? { ...rs, rules: res.rules ?? nextRules } : rs))
|
||||
toast.success("Правила применены на роутер")
|
||||
return { ok: true }
|
||||
@@ -1587,11 +1581,13 @@ export default function FiltersPage() {
|
||||
setApplyBusy(true)
|
||||
setHistoryRestoring(true)
|
||||
try {
|
||||
const res = await apiFetch<{ ok: boolean; rules?: FilterRule[] }>(
|
||||
const payload = await apiFetch<unknown>(
|
||||
`/api/filters/revisions/${encodeURIComponent(id)}/restore`,
|
||||
{ method: "POST", body: JSON.stringify({ serverId: selectedServerId }) },
|
||||
)
|
||||
if (res?.ok !== true) throw new Error("Сервер не подтвердил восстановление")
|
||||
const parsed = filtersMutationSchema.safeParse(payload)
|
||||
if (!parsed.success) throw new Error("Сервер не подтвердил восстановление")
|
||||
const res = parsed.data
|
||||
if (res.rules) {
|
||||
setRulesets((p) => p.map((rs) => rs.serverId === selectedServerId ? { ...rs, rules: res.rules ?? [] } : rs))
|
||||
} else {
|
||||
@@ -1711,6 +1707,13 @@ export default function FiltersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (isLive && liveLoadState === "error" && liveServers.length === 0) {
|
||||
return <div className="flex h-full flex-col items-center justify-center gap-3 p-6">
|
||||
<p role="alert" className="text-sm text-destructive">Не удалось загрузить Filters: {liveLoadError}</p>
|
||||
<Button variant="outline" onClick={() => setLoadAttempt(n => n + 1)}>Повторить загрузку</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (isLive && liveLoadState === "loading") {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 text-sm text-muted-foreground">
|
||||
@@ -1792,7 +1795,8 @@ export default function FiltersPage() {
|
||||
{isLive && liveLoadState === "error" && (
|
||||
<div className="shrink-0 border-b border-destructive/30 bg-destructive/10 px-6 py-2.5 text-xs text-destructive flex items-center gap-2">
|
||||
<AlertCircleIcon className="size-3.5 shrink-0" />
|
||||
Бекенд недоступен — показаны демо-данные из lib/data. Проверьте URL бекенда в настройках.
|
||||
<span role="alert">Не удалось загрузить Filters: {liveLoadError}. Сохранены последние данные.</span>
|
||||
<Button variant="outline" size="sm" onClick={() => setLoadAttempt(n => n + 1)}>Повторить загрузку</Button>
|
||||
</div>
|
||||
)}
|
||||
{isLive && liveStale && liveLoadState !== "error" && (
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ flowchart LR
|
||||
|
||||
Ближайший оставшийся объём:
|
||||
|
||||
1. Этап 9: карта разделов и схема сохранены; удаление, импорт, создание, переключение, контекст экспорта и выбор пира WireGuard исправлены в 9.1–9.7; в 9.8 проверено подтверждение восстановления. В 9.9 исправлены разметка и сохранение подтверждения при ошибке у всех шести потребителей. В 9.10 исправлены ошибки и поздние ответы загрузки истории WireGuard. В 9.11 подтверждение восстановления проверено у остальных пяти потребителей. В 9.12 обработаны ошибка/повтор/поздние ответы истории IPsec. В 9.13 подключены ошибка/повтор/защита поздних ответов остальных четырёх разделов и общая валидация ревизий всех шести. В 9.14 исправлены подтверждение инициализации IPsec, сохранение формы и защита повторной отправки. В 9.15 проверены создание/редактирование пользователей IPsec и сохранение формы. В 9.16 проверены получение/повторный экспорт сертификатов, поздние ответы и соответствие отображаемого пароля архиву. В 9.17 подтверждены ответы удаления четырёх типов объектов IPsec и исправлен диалог повторов. В 9.18 исправлено подтверждение сохранения peer IPsec и поведение формы. В 9.19 исправлено подтверждение удаления GRE и сохранение диалога при ошибке. В 9.20 исправлено подтверждение включения/выключения GRE и защита повторов. В 9.21 исправлены подтверждение создания/сохранения GRE и защита формы. В 9.22 исправлены контейнер ответа, первоначальные KPI и сохранение данных при ошибке VXLAN. В 9.23 backend сообщает частичные сбои VXLAN, UI сохраняет данные неответивших устройств и показывает неизвестное состояние. В 9.24 добавлены схемы payload и тесты гонок VXLAN. В 9.25 подтверждены включение/отключение правил Firewall и защита повторов. В 9.26 подтверждены включение/отключение address-list с общим замком переключений. В 9.27 удаление правил/address-list перенесено в диалог с подтверждением API и защитой повторов. В 9.28 создание/редактирование правил и address-list требуют подтверждения API, сохраняют ввод/ошибку и блокируют повторы/закрытие. В 9.29 перемещение Firewall требует ok=true, откатывает порядок при ошибке и использует общий замок. В 9.30 восстановление истории подключено к общему замку, ручное обновление и открытие истории заблокированы во время записи. В 9.31 добавлены схемы ответа Firewall/серверов, сохранение последних данных при ошибке и поколение чтения с отменой применения при начале записи. В 9.32 backend сообщает частичные сбои по устройству/семейству/таблице, UI сохраняет недоступные срезы и блокирует изменения неполного среза. В 9.33 состояние изолировано по backend/режиму; поздние операции старого экрана не запускают чтение и не показывают уведомления в новом контексте. В 9.34 показано время получения срезов Firewall: успешные обновляются, недоступные сохраняют прежнее время или остаются неизвестными. В 9.35 сохранение Filters ожидает ok=true, сохраняет ввод при ошибке и блокирует повторную отправку/закрытие; live-спLine truncated
|
||||
1. Этап 9: карта разделов и схема сохранены; удаление, импорт, создание, переключение, контекст экспорта и выбор пира WireGuard исправлены в 9.1–9.7; в 9.8 проверено подтверждение восстановления. В 9.9 исправлены разметка и сохранение подтверждения при ошибке у всех шести потребителей. В 9.10 исправлены ошибки и поздние ответы загрузки истории WireGuard. В 9.11 подтверждение восстановления проверено у остальных пяти потребителей. В 9.12 обработаны ошибка/повтор/поздние ответы истории IPsec. В 9.13 подключены ошибка/повтор/защита поздних ответов остальных четырёх разделов и общая валидация ревизий всех шести. В 9.14 исправлены подтверждение инициализации IPsec, сохранение формы и защита повторной отправки. В 9.15 проверены создание/редактирование пользователей IPsec и сохранение формы. В 9.16 проверены получение/повторный экспорт сертификатов, поздние ответы и соответствие отображаемого пароля архиву. В 9.17 подтверждены ответы удаления четырёх типов объектов IPsec и исправлен диалог повторов. В 9.18 исправлено подтверждение сохранения peer IPsec и поведение формы. В 9.19 исправлено подтверждение удаления GRE и сохранение диалога при ошибке. В 9.20 исправлено подтверждение включения/выключения GRE и защита повторов. В 9.21 исправлены подтверждение создания/сохранения GRE и защита формы. В 9.22 исправлены контейнер ответа, первоначальные KPI и сохранение данных при ошибке VXLAN. В 9.23 backend сообщает частичные сбои VXLAN, UI сохраняет данные неответивших устройств и показывает неизвестное состояние. В 9.24 добавлены схемы payload и тесты гонок VXLAN. В 9.25 подтверждены включение/отключение правил Firewall и защита повторов. В 9.26 подтверждены включение/отключение address-list с общим замком переключений. В 9.27 удаление правил/address-list перенесено в диалог с подтверждением API и защитой повторов. В 9.28 создание/редактирование правил и address-list требуют подтверждения API, сохраняют ввод/ошибку и блокируют повторы/закрытие. В 9.29 перемещение Firewall требует ok=true, откатывает порядок при ошибке и использует общий замок. В 9.30 восстановление истории подключено к общему замку, ручное обновление и открытие истории заблокированы во время записи. В 9.31 добавлены схемы ответа Firewall/серверов, сохранение последних данных при ошибке и поколение чтения с отменой применения при начале записи. В 9.32 backend сообщает частичные сбои по устройству/семейству/таблице, UI сохраняет недоступные срезы и блокирует изменения неполного среза. В 9.33 состояние изолировано по backend/режиму; поздние операции старого экрана не запускают чтение и не показывают уведомления в новом контексте. В 9.34 показано время получения срезов Firewall: успешные обновляются, недоступные сохраняют прежнее время или остаются неизвестными. В 9.35 сохранение Filters ожидает ok=true, сохраняет ввод при ошибке и блокирует повторную отправку/закрытие; live-спLine truncated
|
||||
2. Сохранять контекст устройства, введённые значения при ошибке и подтверждать результат операций по API; локальные моки проверяют клиентские сценарии, реальные сетевые операции требуют тестового окружения.
|
||||
3. В этапе 12 выполнить полный аудит доступности (скринридер, контраст), производительности и сквозную регрессию. Ограничения этапа 8 — удалённые серверы месячного каталога, отсутствие экспорта и автоматического переподключения SSE — остаются явно зафиксированными, без заявления о реализации.
|
||||
|
||||
|
||||
+10
-1
@@ -25,12 +25,21 @@
|
||||
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||
| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.39 — IPsec/GRE/VXLAN, Firewall и операции Filters |
|
||||
| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.40 — IPsec/GRE/VXLAN, Firewall и операции Filters |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
|
||||
## Блок 9.40 — схемы и первоначальная загрузка Filters (2026-09-30)
|
||||
|
||||
- lib/filters-response.ts проверяет обязательные поля правила, action, уникальность ID в наборе и serverId в контейнере. Схемы используются в первоначальном GET, чтении устройства, PUT применения и POST восстановления. Ответ с ok=true, но повреждёнными rules отклоняется без замены таблицы/закрытия формы. Для каталога устройств переиспользуется общий существующий контракт firewallServersSchema; не добавлены новые зависимости.
|
||||
- В live исходные rulesets пусты; ошибка загрузки больше не переключает каталог/правила/GRE на встроенные моки. Есть явный alert и повтор первоначальной загрузки; успешная пустая конфигурация отличается от ошибки. Повторный сбой чтения устройства сохраняет последние строки и блокирует изменения. Проверяется наличие ruleset каждого включённого устройства: backend начальный GET исключает отключённые устройства. Первоначальный запрос имеет cleanup от позднего завершения; общей защиты поколениями остальных чтений ещё нет.
|
||||
- GRE из GET правил больше не применяется без схемы: используются отдельные существующие ensureGreTunnels/ensureRecursiveRoutes. Их payload/ошибки пока не валидируются и могут маскироваться пустыми списками; это оставшийся объём. GET без serverId возвращает backend-кэш (live=false), поэтому достоверность готовности к записи до чтения выбранного устройства требует следующего блока. Также остаются гонки смены backend/режима/устройства и старого чтения после мутации, неизвестные срезы отключённых устройств и свежесть.
|
||||
- Все 18 профильных E2E прошли: пять новых сценариев повреждённого контейнера/правила/дублей/сервера с первоначальной ошибкой, повтором, последующим отказом/сохранением данных и повреждённым подтверждением сохранения; 13 прежних операций/замка/таблицы. TypeScript и production build прошли (34 страницы). Новые схема/тест lint-чистые; исходные 2 ошибки/13 предупреждений страницы совпадают с HEAD. Diff-check прошёл. Последний backend build — 9.32, backend не изменён.
|
||||
- Следующий точный шаг: поколения загрузки Filters, инвалидировать старое чтение перед мутацией, изолировать backend/режим и устройство; не разрешать запись до подтверждённого актуального чтения selectedServerId. Проверить поздний успех/ошибку, смену устройства и первоначальный backend-кэш. Затем схемы/ошибки шлюзов и свежесть. Реальные RouterOS/JWT/БД не проверены; этап 9 открыт.
|
||||
- Квота: 69% пятичасовой/11% недельной на старте, 72%/11% после профильных проверок. Локальный коммит без push; чужие изменения сохранены.
|
||||
|
||||
## Блок 9.39 — общий замок обновления и восстановления Filters (2026-09-30)
|
||||
|
||||
- refreshFromRouter и restoreRevision используют общий синхронный applyLock. Восстановление удерживает его до чтения правил (если ответ не содержит rules) и истории. Повторы/конфликты отклоняются, выбор устройства и действия таблицы удерживаются; finally освобождает замок после успеха/ошибки. Открытие/повтор загрузки истории также проверяют замок.
|
||||
|
||||
@@ -197,3 +197,7 @@ Backend сообщает failures по срезу, неполный snapshot о
|
||||
## Блок 9.39 — общий замок Filters (2026-09-30)
|
||||
|
||||
Обновление/восстановление используют applyLock, восстановление держит его до чтения правил/истории. Ошибка чтения сохраняет подтверждение и последние строки; чужой ruleset и stale отклоняются. 15 разных E2E в нескольких прогонах и TypeScript прошли; новых lint замечаний нет. Далее — схемы/первоначальная загрузка без моковой подмены, затем гонки и свежесть. Реальные устройства не проверены; frontend build — 9.31, backend — 9.32.
|
||||
|
||||
## Блок 9.40 — загрузка Filters (2026-09-30)
|
||||
|
||||
Схемы валидируют правила/наборы/подтверждения, live-ошибка больше не подставляет моки, есть первоначальная ошибка/повтор и сохранение последних правил. 18 E2E, TypeScript и frontend build (34 страницы) прошли; новых lint замечаний нет. Далее — поколения чтения/изоляция контекста и блокировка записи до свежего чтения выбранного устройства. Начальный GET отдаёт backend-кэш; GRE/recursive payload и ошибки ещё не приняты. Реальные устройства не проверены; backend build — 9.32.
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const ruleSchema = z.object({
|
||||
id: z.string().min(1), community: z.string(), communityName: z.string().optional(),
|
||||
action: z.enum(["route", "blackhole"]).optional(), gateway: z.string(), gatewayTunnelId: z.string(), description: z.string(),
|
||||
})
|
||||
export const filterRulesSchema = z.array(ruleSchema).refine(rows => new Set(rows.map(r => r.id)).size === rows.length)
|
||||
export const filtersResponseSchema = z.object({
|
||||
rulesets: z.array(z.object({ serverId: z.string().min(1), rules: filterRulesSchema }))
|
||||
.refine(rows => new Set(rows.map(r => r.serverId)).size === rows.length),
|
||||
live: z.boolean().optional(), stale: z.boolean().optional(), error: z.string().optional(),
|
||||
})
|
||||
export const filtersMutationSchema = z.object({ ok: z.literal(true), rules: filterRulesSchema.optional() })
|
||||
@@ -0,0 +1,69 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
for (const invalid of ["container", "rule", "duplicate", "server"] as const) {
|
||||
test(`Filters: повреждённый ответ ${invalid}, первый сбой, повтор и сохранение данных`, async ({ page }) => {
|
||||
let phase = 0
|
||||
const rule = { id: "rule-1", community: "65001:101", gateway: "", gatewayTunnelId: "", action: "blackhole", description: "Подтверждённые данные" }
|
||||
const server = { id: 1, name: "Реальный каталог тест", host: "192.0.2.1", type: "exit-node", enabled: true, country: "RU", site: "TEST", asn: "", status: "online", latency: 1 }
|
||||
const good = { rulesets: [{ serverId: "1", rules: [rule] }], greTunnels: [], live: true }
|
||||
const bad = invalid === "container" ? { rulesets: {} } : invalid === "rule" ? { ...good, rulesets: [{ serverId: "1", rules: [{ ...rule, description: null }] }] } : { ...good, rulesets: [{ serverId: "1", rules: [rule, rule] }] }
|
||||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||
await page.route("**/api/**", route => {
|
||||
const path = new URL(route.request().url()).pathname
|
||||
if (path === "/api/filters/rules" && phase === 2 && invalid === "server") return route.fulfill({ status: 503, json: { error: "Опрос недоступен тест" } })
|
||||
return route.fulfill({ json: path === "/api/filters/rules" ? phase !== 1 && invalid !== "server" ? bad : good
|
||||
: path === "/api/servers" ? [phase === 0 && invalid === "server" ? { ...server, host: null } : server]
|
||||
: path === "/api/auth/config" ? { required: false }
|
||||
: path === "/api/filters/gre-tunnels" ? { tunnels: [] }
|
||||
: path === "/api/recursive-routes" ? { routes: [] } : {} })
|
||||
})
|
||||
await page.goto("/filters")
|
||||
await expect(page.getByRole("alert").filter({ hasText: "Не удалось загрузить Filters" })).toBeVisible()
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
|
||||
await expect(page.getByPlaceholder("Community, gateway, описание…")).toHaveCount(0)
|
||||
await expect(page.getByRole("button", { name: "Новое правило", exact: true })).toHaveCount(0)
|
||||
phase = 1
|
||||
await page.getByRole("button", { name: "Повторить загрузку", exact: true }).click()
|
||||
const rows = page.locator("tr[data-row-id]")
|
||||
await expect(rows).toHaveCount(1)
|
||||
await expect(rows.first()).toContainText("Подтверждённые данные")
|
||||
await expect(page.getByRole("option", { name: /Реальный каталог тест/ })).toHaveCount(1)
|
||||
await expect(page.getByRole("button", { name: "Новое правило", exact: true })).toBeEnabled()
|
||||
phase = 2
|
||||
const refresh = page.getByRole("button", { name: "Обновить", exact: true })
|
||||
await refresh.click()
|
||||
await expect(refresh).toBeEnabled()
|
||||
await expect(page.getByRole("button", { name: "Новое правило", exact: true })).toBeDisabled()
|
||||
await expect(rows).toHaveCount(1)
|
||||
await expect(rows.first()).toContainText("Подтверждённые данные")
|
||||
phase = 1
|
||||
await refresh.click()
|
||||
await expect(page.getByRole("button", { name: "Новое правило", exact: true })).toBeEnabled()
|
||||
})
|
||||
}
|
||||
|
||||
test("Filters: повреждённые подтверждённые правила не закрывают форму и не заменяют таблицу", async ({ page }) => {
|
||||
const rule = { id: "rule-1", community: "65001:101", gateway: "", gatewayTunnelId: "", action: "blackhole", description: "Прежние данные" }
|
||||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||
await page.route("**/api/**", route => {
|
||||
const path = new URL(route.request().url()).pathname
|
||||
if (path === "/api/filters/rules" && route.request().method() === "PUT") return route.fulfill({ json: { ok: true, rules: [{ ...rule, gateway: null }] } })
|
||||
return route.fulfill({ json: path === "/api/filters/rules" ? { rulesets: [{ serverId: "1", rules: [rule] }], greTunnels: [], live: true }
|
||||
: path === "/api/servers" ? [{ id: 1, name: "Роутер", host: "192.0.2.1", type: "exit-node", enabled: true, country: "RU", site: "TEST", asn: "", status: "online", latency: 1 }]
|
||||
: path === "/api/auth/config" ? { required: false }
|
||||
: path === "/api/filters/gre-tunnels" ? { tunnels: [] }
|
||||
: path === "/api/recursive-routes" ? { routes: [] } : {} })
|
||||
})
|
||||
await page.goto("/filters")
|
||||
const rows = page.locator("tr[data-row-id]")
|
||||
await expect(rows).toHaveCount(1)
|
||||
await rows.first().hover()
|
||||
await rows.first().getByRole("button", { name: "Редактировать правило", exact: true }).click()
|
||||
const dialog = page.getByRole("dialog")
|
||||
await dialog.getByRole("textbox", { name: "Описание правила" }).fill("Сохранить ввод")
|
||||
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
|
||||
await expect(dialog.getByRole("alert")).toContainText("Сервер не подтвердил применение правил")
|
||||
await expect(dialog.getByRole("textbox", { name: "Описание правила" })).toHaveValue("Сохранить ввод")
|
||||
await expect(rows.first()).toContainText("Прежние данные")
|
||||
await expect(page.getByText("Правила применены на роутер", { exact: true })).toHaveCount(0)
|
||||
})
|
||||
Reference in new issue
Block a user