fix(ui): ограничить трассировку и защитить повторную симуляцию

This commit is contained in:
Denozordec committed 2026-10-04 16:45:39 +07:00
1 parent 9231597d88
commit 21dc646553
7 files changed
+96 -18

No files matched your search

+46 -17
View File
@@ -988,7 +988,7 @@ function SimStepRow({ step, index }: { step: SimStep; index: number }) {
type="button"
onClick={() => isClickable && setExpanded(e => !e)}
className={cn(
"w-full flex items-center gap-3 px-5 py-2.5 text-left transition-colors",
"w-full min-h-11 flex flex-wrap sm:flex-nowrap items-center gap-3 px-5 py-2.5 text-left transition-colors",
isClickable ? "hover:bg-muted/30 cursor-pointer" : "cursor-default",
step.status === "pending" && "opacity-40",
step.status === "disabled" && "opacity-25",
@@ -999,7 +999,7 @@ function SimStepRow({ step, index }: { step: SimStep; index: number }) {
</span>
<div className="shrink-0"><ChainBadge chain={step.rule.chain} /></div>
<div className="shrink-0"><ActionBadge action={step.rule.action} /></div>
<span className="text-xs font-mono text-muted-foreground flex-1 truncate min-w-0">
<span className="text-xs font-mono text-muted-foreground flex-1 basis-full sm:basis-auto truncate min-w-0">
{step.rule.src || "any"} → {step.rule.dst || "any"}
{step.rule.proto && step.rule.proto !== "all" && (
<span className="opacity-50"> {step.rule.proto}</span>
@@ -1015,7 +1015,7 @@ function SimStepRow({ step, index }: { step: SimStep; index: number }) {
)}
{/* Status badge */}
<div className="shrink-0 min-w-[130px] flex justify-end">
<div className="shrink-0 min-w-0 sm:min-w-[130px] flex justify-end">
{step.status === "pending" && (
<span className="text-[10px] text-muted-foreground/40">ожидание</span>
)}
@@ -1030,7 +1030,7 @@ function SimStepRow({ step, index }: { step: SimStep; index: number }) {
</span>
)}
{step.status === "passthrough" && (
<span className="text-[10px] text-violet-500 font-mono font-semibold flex items-center gap-1">
<span className="text-[10px] text-violet-600 dark:text-violet-400 font-mono font-semibold flex items-center gap-1">
<SkipForwardIcon className="size-3" />passthrough
</span>
)}
@@ -1355,9 +1355,12 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
const [activePreset, setPreset] = useState<string | null>("https")
const [simState, setSimState] = useState<"idle" | "running" | "done">("idle")
const [steps, setSteps] = useState<SimStep[]>([])
const [tracePage, setTracePage] = useState(0)
const [speed, setSpeed] = useState<SimSpeed>("normal")
const [verdict, setVerdict] = useState<{ action: string; ruleIndex: number | null } | null>(null)
const cancelRef = useRef(false)
const runEpoch = useRef(0)
useEffect(() => () => { runEpoch.current += 1; cancelRef.current = true }, [])
// ── Scenarios state ─────────────────────────────────────────────────────────
const [scenarios, setScenarios] = useState<SimScenario[]>(() => {
@@ -1392,9 +1395,11 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
// ── Handlers ────────────────────────────────────────────────────────────────
function resetSim() {
runEpoch.current += 1
cancelRef.current = true
setSimState("idle")
setSteps([])
setTracePage(0)
setVerdict(null)
}
@@ -1402,18 +1407,22 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
setActiveScenarioId(null)
setPreset(p.id)
setPacket({ ...DEFAULT_PKT, ...p.pkt })
runEpoch.current += 1
cancelRef.current = true
setSimState("idle")
setSteps([])
setTracePage(0)
setVerdict(null)
}
function activateScenario(id: string | null) {
setActiveScenarioId(id)
setPreset(null)
runEpoch.current += 1
cancelRef.current = true
setSimState("idle")
setSteps([])
setTracePage(0)
setVerdict(null)
if (id) {
const sc = scenarios.find(s => s.id === id)
@@ -1444,7 +1453,9 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
return
}
const epoch = ++runEpoch.current
cancelRef.current = false
setTracePage(0)
setSimState("running")
setVerdict(null)
setSteps(rules.map((r, i) => ({ rule: r, index: i, status: "pending", checks: [] })))
@@ -1453,10 +1464,10 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
let finalVerdict: { action: string; ruleIndex: number | null } | null = null
for (let i = 0; i < rules.length; i++) {
if (cancelRef.current) return
if (cancelRef.current || epoch !== runEpoch.current) return
setSteps(prev => prev.map((s, idx) => idx === i ? { ...s, status: "evaluating" } : s))
if (delay > 0) await new Promise<void>(res => setTimeout(res, delay))
if (cancelRef.current) return
if (cancelRef.current || epoch !== runEpoch.current) return
const result = evalRule(rules[i], packet)
setSteps(prev => prev.map((s, idx) =>
@@ -1471,12 +1482,21 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
if (delay > 0) await new Promise<void>(res => setTimeout(res, Math.max(delay * 0.25, 40)))
}
if (!cancelRef.current) {
if (!cancelRef.current && epoch === runEpoch.current) {
setVerdict(finalVerdict ?? { action: "accept (default)", ruleIndex: null })
setSimState("done")
}
}
const tracePageCount = Math.max(1, Math.ceil(steps.length / 50))
const currentTracePage = Math.min(tracePage, tracePageCount - 1)
const traceOffset = currentTracePage * 50
const traceChainCounts = useMemo(() => {
const counts = new Map<string, number>()
for (const step of steps) counts.set(step.rule.chain, (counts.get(step.rule.chain) ?? 0) + 1)
return counts
}, [steps])
const verdictColors = verdict ? getVerdictColors(verdict.action) : null
return (
@@ -1690,7 +1710,7 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
</Button>
{simState === "running" ? (
<Button size="sm" variant="destructive"
onClick={() => { cancelRef.current = true; setSimState("done") }}>
onClick={() => { runEpoch.current += 1; cancelRef.current = true; setSimState("done") }}>
<SquareIcon className="size-4" />Стоп
</Button>
) : (
@@ -1730,8 +1750,8 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
{/* ── Trace list ──────────────────────────────────────────────────────── */}
{steps.length > 0 && (
<Frame dense className="w-full">
<FramePanel className="p-0">
<div className="px-5 py-3 border-b flex items-center justify-between">
<FramePanel className="p-0" role="region" aria-label="Трассировка Firewall">
<div className="px-5 py-3 border-b flex items-center justify-between flex-wrap gap-2">
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Трассировка · цепочка: <span className="text-foreground normal-case font-mono">{packet.chain}</span>
{activeScenario && (
@@ -1745,23 +1765,24 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
</span>
</div>
<div className="divide-y divide-border/60">
{steps.map((step, i) => {
{steps.slice(traceOffset, traceOffset + 50).map((step, pageIndex) => {
const i = traceOffset + pageIndex
const prevChain = i > 0 ? steps[i - 1].rule.chain : null
const showHeader = step.rule.chain !== prevChain
const showHeader = pageIndex === 0 || step.rule.chain !== prevChain
return (
<div key={step.rule.id}>
<div key={step.rule.id} data-firewall-trace-step={i + 1}>
{showHeader && (
<div className="px-5 py-1.5 bg-muted/40 border-b border-border/50 flex items-center gap-2">
<span className="text-[10px] font-mono font-semibold text-foreground/70 uppercase tracking-wider">
<span className="text-[10px] font-mono font-semibold text-foreground uppercase tracking-wider">
{step.rule.chain}
</span>
{CHAIN_TABLE_HINT[step.rule.chain] && (
<span className="text-[10px] text-muted-foreground/50">
<span className="text-[10px] text-muted-foreground">
· {CHAIN_TABLE_HINT[step.rule.chain]}
</span>
)}
<span className="text-[10px] text-muted-foreground/40 ml-auto">
{steps.filter(s => s.rule.chain === step.rule.chain).length} правил
<span className="text-[10px] text-muted-foreground ml-auto">
{traceChainCounts.get(step.rule.chain)} правил
</span>
</div>
)}
@@ -1770,6 +1791,14 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
)
})}
</div>
<div className="flex items-center justify-between flex-wrap gap-2 border-t px-5 py-3">
<span role="status" className="text-xs text-muted-foreground">Шаги {traceOffset + 1}–{Math.min(traceOffset + 50, steps.length)} из {steps.length}</span>
<div className="flex items-center flex-wrap gap-2">
<Button type="button" variant="outline" className="min-h-11" aria-label="Предыдущая страница трассировки" disabled={currentTracePage === 0} onClick={() => setTracePage(currentTracePage - 1)}>Назад</Button>
<Button type="button" variant="outline" className="min-h-11" aria-label="Следующая страница трассировки" disabled={currentTracePage >= tracePageCount - 1} onClick={() => setTracePage(currentTracePage + 1)}>Далее</Button>
{verdict?.ruleIndex != null && <Button type="button" variant="outline" className="min-h-11" onClick={() => setTracePage(Math.floor(verdict.ruleIndex! / 50))}>К правилу результата</Button>}
</div>
</div>
</FramePanel>
</Frame>
)}
+2
View File
@@ -87,3 +87,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич
12.3.44: stale операции Firewall заблокированы до успешного read/после retry, ограничения12.3.43 по полному GET failure исправлены.29+2 main/context4/type/build прошли;4 исходных ESLint errors страницы подтверждены сравнением с HEAD.11 manualPagination:true остаются.
12.3.45: локальный сценарий1000→50 rows/1847 modal nodes; pagination/full draft999/скрытый verdict#999/320px/axe проверены,6 browser/type/grid lint/build прошли. Трассировка999 остаётся15529 page nodes, требует12.3.46; ошибки записи localStorage не проверены.10 manualPagination:true остаются.
12.3.46: trace999/15529→1293 page nodes,50 steps/whole verdict#999/страницы/jump/stop→restart epoch/reset/320px/axe проверены;6+1 browser/type/test lint/build прошли. Полный steps в памяти и O(N²) обновления instant не менялись; общий CPU performance не принят.10 manualPagination:true остаются.
+1 -1
View File
@@ -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 принLine truncated
Этап 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 принLine truncated
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
+8
View File
@@ -31,6 +31,14 @@
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | В работе;12.1/12.2.1–12.2.10 и12.3.1–12.3.5 и12.3.7–12.3.12 приняты в описанных локальных границах;12.4.1 main регрессия и профильный повтор записаны;12.4.2 context205 и unit28 прошли;12.3.13–12.3.25 приняты локально в описанных границах;12.4.4 context213 и12.4.5 main1110 прошли, 12.3.26–12.3.33 приняты локально; 12.3.34 VXLAN и12.3.35 GRE tunnels приняты; остаются network grids/общая регрессия после delta/внешняя приёмка |
## 12.3.46 — ограниченная трассировка Firewall (2026-10-04)
Trace показывает50 шагов, общий verdict/counts/номера относятся ко всему расчёту. Предыдущая/следующая страница и переход к правилу результата44px; новая симуляция/reset/preset/scenario начинают с первой страницы. Строки переносятся на320px, исправлен контраст passthrough и пояснений. runEpoch защищает от старого таймера после stop→новый run; unmount инвалидирует запуск.
6/6 browser на начальной pagination и финальный large1/1 после mobile/epoch/axe прошли: trace999 уменьшен15529→1293 page DOM nodes,50 steps на странице/49 на последней. Full verdict#999, page2, keyboard jump951–999,320px/axe, stop→instant run и продвижение clock1000ms без старого evaluating, reset проверены. TypeScript без incremental/ESLint теста/production build прошли; ESLint API baseline/current подтверждает те же4 исходных ошибки страницы и2 warnings. [Трассировка320px](ux-ui/screens/large-firewall-trace-320.png) просмотрена. Алгоритм вычисления не изменён: instant по-прежнему делает обновления полного steps на каждом правиле, CPU complexity не принята нагрузочным тестом. Данные остаются полными в памяти.
Следующий блок12.3.47: Firewall lint cleanup четырёх старых эффектов без потери draft/URL/filter/focus. Затем OSPF таблица+topology/optimizer; ошибки localStorage и большой multi-server sort отдельно.10 manualPagination:true остаются. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push.
## 12.3.45 — большой редактор сценария Firewall (2026-10-04)
ReUI pagination50/clamp/ScrollArea, закреплённые номер и chain, действия44px без hover; номера и up/down края считаются по полному draft. ScenarioSheet320px занимает всю ширину viewport. Кнопки редактирования/удаления сценария получили44px и доступные имена. Локальный сценарий не отправляет операции RouterOS.
+9
View File
@@ -560,3 +560,12 @@ ReUI pagination50/clamp/ScrollArea, закреплённые номер и chain
Обнаружено: trace999 создаёт15529 page DOM nodes; ограничение редактора не означает принятие всего симулятора. Следующий блок12.3.46: страницы трассировки/полный verdict/переход к результату и сохранение отмены/повторного запуска. Затем Firewall lint cleanup, OSPF таблица+topology/optimizer.10 manualPagination:true specialized grids остаются. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push.
## 12.3.46 — ограниченная трассировка Firewall (2026-10-04)
Trace показывает50 шагов, общий verdict/counts/номера относятся ко всему расчёту. Предыдущая/следующая страница и переход к правилу результата44px; новая симуляция/reset/preset/scenario начинают с первой страницы. Строки переносятся на320px, исправлен контраст passthrough и пояснений. runEpoch защищает от старого таймера после stop→новый run; unmount инвалидирует запуск.
6/6 browser на начальной pagination и финальный large1/1 после mobile/epoch/axe прошли: trace999 уменьшен15529→1293 page DOM nodes,50 steps на странице/49 на последней. Full verdict#999, page2, keyboard jump951–999,320px/axe, stop→instant run и продвижение clock1000ms без старого evaluating, reset проверены. TypeScript без incremental/ESLint теста/production build прошли; ESLint API baseline/current подтверждает те же4 исходных ошибки страницы и2 warnings. [Трассировка320px](ux-ui/screens/large-firewall-trace-320.png) просмотрена. Алгоритм вычисления не изменён: instant по-прежнему делает обновления полного steps на каждом правиле, CPU complexity не принята нагрузочным тестом. Данные остаются полными в памяти.
Следующий блок12.3.47: Firewall lint cleanup четырёх старых эффектов без потери draft/URL/filter/focus. Затем OSPF таблица+topology/optimizer; ошибки localStorage и большой multi-server sort отдельно.10 manualPagination:true остаются. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push.
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

+30
View File
@@ -52,6 +52,36 @@ test("Сценарий1000: граница страниц, полный draft и
await page.getByRole("button", { name: "Запустить", exact: true }).click()
await expect(page.getByText("— правило #999", { exact: true })).toBeVisible()
await expect(page.getByText("Пакет заблокирован", { exact: false })).toBeVisible()
const trace = page.getByRole("region", { name: "Трассировка Firewall", exact: true })
const traceRows = trace.locator("[data-firewall-trace-step]")
await expect(traceRows).toHaveCount(50)
await expect(traceRows.first()).toHaveAttribute("data-firewall-trace-step", "1")
console.log("Scenario simulation nodes", await page.locator("*").count())
await trace.getByRole("button", { name: "Следующая страница трассировки", exact: true }).click()
await expect(traceRows.first()).toHaveAttribute("data-firewall-trace-step", "51")
await trace.getByRole("button", { name: "К правилу результата", exact: true }).focus(); await page.keyboard.press("Enter")
await expect(traceRows).toHaveCount(49)
await expect(traceRows.last()).toHaveAttribute("data-firewall-trace-step", "999")
await expect(trace.getByRole("status")).toHaveText("Шаги 951–999 из 999")
await expect(trace.getByRole("button", { name: "Следующая страница трассировки", exact: true })).toBeDisabled()
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(320)
expect((await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[aria-label="Трассировка Firewall"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))).violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) }))).toEqual([])
await traceRows.last().scrollIntoViewIfNeeded()
await page.screenshot({ path: info.outputPath("trace-320.png"), animations: "disabled" })
await page.clock.install()
await page.getByRole("button", { name: "Быстро", exact: true }).click()
await page.getByRole("button", { name: "Снова", exact: true }).click()
await expect(traceRows.first()).toHaveAttribute("data-firewall-trace-step", "1")
await page.getByRole("button", { name: "Стоп", exact: true }).click()
await page.getByRole("button", { name: "Мгновенно", exact: true }).click()
await page.getByRole("button", { name: "Снова", exact: true }).click()
await expect(page.getByText("— правило #999", { exact: true })).toBeVisible()
await expect(traceRows.first()).toHaveAttribute("data-firewall-trace-step", "1")
await page.clock.runFor(1000)
await expect(trace.getByText("проверяется…", { exact: true })).toHaveCount(0)
await expect(page.getByText("— правило #999", { exact: true })).toBeVisible()
await page.getByRole("button", { name: "Сбросить", exact: true }).click()
await expect(trace).toHaveCount(0)
await expect(page.getByText("— правило #999", { exact: true })).toHaveCount(0)
expect(writes).toBe(0)
})